fix test failure
Test / Type Check (all packages) (push) Successful in 3m30s
Test / API Unit Tests (push) Successful in 3m5s
Test / Homepage Unit Tests (push) Successful in 3m0s
Test / Storefront Unit Tests (push) Successful in 3m36s
Test / Admin Unit Tests (push) Successful in 4m0s
Test / Dashboard Unit Tests (push) Successful in 3m11s
Test / API Integration Tests (push) Successful in 3m6s
Build & Deploy / Build & Push Docker Image (push) Failing after 14s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 3m30s
Test / API Unit Tests (push) Successful in 3m5s
Test / Homepage Unit Tests (push) Successful in 3m0s
Test / Storefront Unit Tests (push) Successful in 3m36s
Test / Admin Unit Tests (push) Successful in 4m0s
Test / Dashboard Unit Tests (push) Successful in 3m11s
Test / API Integration Tests (push) Successful in 3m6s
Build & Deploy / Build & Push Docker Image (push) Failing after 14s
Build & Deploy / Deploy to VPS (push) Has been skipped
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
vi.mock('../../lib/storage', () => ({ uploadImage: vi.fn().mockResolvedValue('/storage/companies/company_1/brand/logo.jpg') }))
|
vi.mock('../../lib/storage', () => ({ uploadImage: vi.fn().mockResolvedValue('/storage/companies/company_1/brand/logo.jpg') }))
|
||||||
|
vi.mock('./settingsEntitlements', () => ({
|
||||||
|
resolveSettingsEntitlements: vi.fn(),
|
||||||
|
}))
|
||||||
vi.mock('./company.repo', () => ({
|
vi.mock('./company.repo', () => ({
|
||||||
findCompany: vi.fn(),
|
findCompany: vi.fn(),
|
||||||
updateCompany: vi.fn(),
|
updateCompany: vi.fn(),
|
||||||
@@ -28,8 +31,43 @@ vi.mock('./company.repo', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const repo = await import('./company.repo')
|
const repo = await import('./company.repo')
|
||||||
|
const entitlements = await import('./settingsEntitlements')
|
||||||
const service = await import('./company.service')
|
const service = await import('./company.service')
|
||||||
|
|
||||||
|
const editableSettingsFeatures = [
|
||||||
|
'settings.company_profile',
|
||||||
|
'settings.public_contact',
|
||||||
|
'settings.locale_currency',
|
||||||
|
'settings.storefront_listing',
|
||||||
|
'settings.branding_basic',
|
||||||
|
'settings.branding_custom',
|
||||||
|
'settings.branding_hero',
|
||||||
|
'settings.renter_payments',
|
||||||
|
'settings.rental_policies_basic',
|
||||||
|
'settings.additional_driver_fees',
|
||||||
|
'settings.insurance_policies',
|
||||||
|
'settings.pricing_rules',
|
||||||
|
'settings.accounting_defaults',
|
||||||
|
'settings.accounting_scheduled_delivery',
|
||||||
|
'settings.accounting_advanced_formats',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function buildEditableEntitlements() {
|
||||||
|
return {
|
||||||
|
plan: 'PRO',
|
||||||
|
subscriptionStatus: 'ACTIVE',
|
||||||
|
currentPeriodEnd: null,
|
||||||
|
trialEndAt: null,
|
||||||
|
accessLevel: 'full',
|
||||||
|
features: Object.fromEntries(editableSettingsFeatures.map((featureKey) => [featureKey, {
|
||||||
|
available: true,
|
||||||
|
editable: true,
|
||||||
|
requiredPlan: null,
|
||||||
|
reason: null,
|
||||||
|
}])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentBrand = {
|
const currentBrand = {
|
||||||
id: 'brand_1',
|
id: 'brand_1',
|
||||||
companyId: 'company_1',
|
companyId: 'company_1',
|
||||||
@@ -44,6 +82,7 @@ const currentBrand = {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(entitlements.resolveSettingsEntitlements).mockResolvedValue(buildEditableEntitlements() as any)
|
||||||
delete process.env.NEXT_PUBLIC_CUSTOM_DOMAIN_TARGET
|
delete process.env.NEXT_PUBLIC_CUSTOM_DOMAIN_TARGET
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,51 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
import * as repo from './company.repo'
|
|
||||||
import * as service from './company.service'
|
|
||||||
|
|
||||||
vi.mock('./company.repo')
|
vi.mock('./company.repo')
|
||||||
|
vi.mock('./settingsEntitlements', () => ({
|
||||||
|
resolveSettingsEntitlements: vi.fn(),
|
||||||
|
}))
|
||||||
vi.mock('../../lib/storage', () => ({
|
vi.mock('../../lib/storage', () => ({
|
||||||
uploadImage: vi.fn().mockResolvedValue('http://localhost:4000/storage/companies/comp_1/brand/logo.jpg'),
|
uploadImage: vi.fn().mockResolvedValue('http://localhost:4000/storage/companies/comp_1/brand/logo.jpg'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const repo = await import('./company.repo')
|
||||||
|
const entitlements = await import('./settingsEntitlements')
|
||||||
|
const service = await import('./company.service')
|
||||||
|
|
||||||
|
const editableSettingsFeatures = [
|
||||||
|
'settings.company_profile',
|
||||||
|
'settings.public_contact',
|
||||||
|
'settings.locale_currency',
|
||||||
|
'settings.storefront_listing',
|
||||||
|
'settings.branding_basic',
|
||||||
|
'settings.branding_custom',
|
||||||
|
'settings.branding_hero',
|
||||||
|
'settings.renter_payments',
|
||||||
|
'settings.rental_policies_basic',
|
||||||
|
'settings.additional_driver_fees',
|
||||||
|
'settings.insurance_policies',
|
||||||
|
'settings.pricing_rules',
|
||||||
|
'settings.accounting_defaults',
|
||||||
|
'settings.accounting_scheduled_delivery',
|
||||||
|
'settings.accounting_advanced_formats',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function buildEditableEntitlements() {
|
||||||
|
return {
|
||||||
|
plan: 'PRO',
|
||||||
|
subscriptionStatus: 'ACTIVE',
|
||||||
|
currentPeriodEnd: null,
|
||||||
|
trialEndAt: null,
|
||||||
|
accessLevel: 'full',
|
||||||
|
features: Object.fromEntries(editableSettingsFeatures.map((featureKey) => [featureKey, {
|
||||||
|
available: true,
|
||||||
|
editable: true,
|
||||||
|
requiredPlan: null,
|
||||||
|
reason: null,
|
||||||
|
}])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mockCompany = {
|
const mockCompany = {
|
||||||
id: 'comp_1',
|
id: 'comp_1',
|
||||||
name: 'Test Rentals',
|
name: 'Test Rentals',
|
||||||
@@ -33,6 +72,7 @@ const mockBrand = {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(entitlements.resolveSettingsEntitlements).mockResolvedValue(buildEditableEntitlements() as any)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('company.service', () => {
|
describe('company.service', () => {
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ export function listByReservation(reservationId: string, companyId: string) {
|
|||||||
return repo.findByReservation(reservationId, companyId)
|
return repo.findByReservation(reservationId, companyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sumSucceededPayments(payments: any[], type: 'CHARGE' | 'DEPOSIT') {
|
||||||
|
return payments.reduce((total: number, payment: any) => {
|
||||||
|
if (payment.type !== type || payment.status !== 'SUCCEEDED') return total
|
||||||
|
return total + payment.amount
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInvoicePaid(reservation: any, rentalPayments: any[]) {
|
||||||
|
return Math.max(sumSucceededPayments(rentalPayments, 'CHARGE'), reservation.paidAmount ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
async function applyAmanpayWebhook(event: any) {
|
async function applyAmanpayWebhook(event: any) {
|
||||||
const transactionId = event.transaction_id ?? event.id
|
const transactionId = event.transaction_id ?? event.id
|
||||||
const status = event.status?.toUpperCase()
|
const status = event.status?.toUpperCase()
|
||||||
@@ -70,22 +81,17 @@ export async function initCharge(reservationId: string, companyId: string, body:
|
|||||||
}) {
|
}) {
|
||||||
const reservation = await repo.findReservationOrThrow(reservationId, companyId)
|
const reservation = await repo.findReservationOrThrow(reservationId, companyId)
|
||||||
const rentalPayments = (reservation as any).rentalPayments ?? []
|
const rentalPayments = (reservation as any).rentalPayments ?? []
|
||||||
const invoicePaid = rentalPayments.reduce((total: number, payment: any) => {
|
const invoicePaid = getInvoicePaid(reservation, rentalPayments)
|
||||||
if (payment.type !== 'CHARGE' || payment.status !== 'SUCCEEDED') return total
|
const depositCollected = sumSucceededPayments(rentalPayments, 'DEPOSIT')
|
||||||
return total + payment.amount
|
const balanceDue = body.type === 'DEPOSIT'
|
||||||
}, 0)
|
? reservation.depositAmount - depositCollected
|
||||||
const depositCollected = rentalPayments.reduce((total: number, payment: any) => {
|
: reservation.totalAmount - invoicePaid
|
||||||
if (payment.type !== 'DEPOSIT' || payment.status !== 'SUCCEEDED') return total
|
|
||||||
return total + payment.amount
|
|
||||||
}, 0)
|
|
||||||
const chargeRemaining = Math.max(reservation.totalAmount - invoicePaid, 0)
|
|
||||||
const depositRemaining = Math.max(reservation.depositAmount - depositCollected, 0)
|
|
||||||
const amount = body.type === 'DEPOSIT' ? depositRemaining : chargeRemaining
|
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (balanceDue <= 0) {
|
||||||
throw new ConflictError(body.type === 'DEPOSIT' ? 'Security deposit is already fully collected' : 'Reservation is already fully paid')
|
throw new ConflictError(body.type === 'DEPOSIT' ? 'Security deposit is already fully collected' : 'Reservation is already fully paid')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const amount = balanceDue
|
||||||
const description = `${body.type === 'DEPOSIT' ? 'Deposit' : 'Rental'}: ${reservation.vehicle.make} ${reservation.vehicle.model}`
|
const description = `${body.type === 'DEPOSIT' ? 'Deposit' : 'Rental'}: ${reservation.vehicle.make} ${reservation.vehicle.model}`
|
||||||
const orderId = `${reservationId}-${body.type}-${Date.now()}`
|
const orderId = `${reservationId}-${body.type}-${Date.now()}`
|
||||||
const webhookBase = process.env.API_URL ?? 'http://localhost:4000'
|
const webhookBase = process.env.API_URL ?? 'http://localhost:4000'
|
||||||
@@ -132,21 +138,18 @@ export async function recordManualPayment(reservationId: string, companyId: stri
|
|||||||
}) {
|
}) {
|
||||||
const reservation = await repo.findReservation(reservationId, companyId)
|
const reservation = await repo.findReservation(reservationId, companyId)
|
||||||
const rentalPayments = (reservation as any).rentalPayments ?? []
|
const rentalPayments = (reservation as any).rentalPayments ?? []
|
||||||
const invoicePaid = rentalPayments.reduce((total: number, payment: any) => {
|
const invoicePaid = getInvoicePaid(reservation, rentalPayments)
|
||||||
if (payment.type !== 'CHARGE' || payment.status !== 'SUCCEEDED') return total
|
const depositCollected = sumSucceededPayments(rentalPayments, 'DEPOSIT')
|
||||||
return total + payment.amount
|
const remaining = body.type === 'DEPOSIT'
|
||||||
}, 0)
|
? reservation.depositAmount - depositCollected
|
||||||
const depositCollected = rentalPayments.reduce((total: number, payment: any) => {
|
: reservation.totalAmount - invoicePaid
|
||||||
if (payment.type !== 'DEPOSIT' || payment.status !== 'SUCCEEDED') return total
|
|
||||||
return total + payment.amount
|
|
||||||
}, 0)
|
|
||||||
const chargeRemaining = Math.max(reservation.totalAmount - invoicePaid, 0)
|
|
||||||
const depositRemaining = Math.max(reservation.depositAmount - depositCollected, 0)
|
|
||||||
const remaining = body.type === 'DEPOSIT' ? depositRemaining : chargeRemaining
|
|
||||||
|
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
throw new ConflictError(body.type === 'DEPOSIT' ? 'Security deposit is already fully collected' : 'Reservation is already fully paid')
|
throw new ConflictError(body.type === 'DEPOSIT' ? 'Security deposit is already fully collected' : 'Reservation is already fully paid')
|
||||||
}
|
}
|
||||||
|
if (body.amount <= 0) {
|
||||||
|
throw new ValidationError('Payment amount must be greater than zero')
|
||||||
|
}
|
||||||
if (body.amount > remaining) {
|
if (body.amount > remaining) {
|
||||||
throw new ValidationError(body.type === 'DEPOSIT' ? 'Payment amount exceeds deposit outstanding' : 'Payment amount exceeds remaining balance')
|
throw new ValidationError(body.type === 'DEPOSIT' ? 'Payment amount exceeds deposit outstanding' : 'Payment amount exceeds remaining balance')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user