From 9ad92765c1ce84496ec02547df30ce87941f8d72 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 30 Jun 2026 00:23:31 -0400 Subject: [PATCH] fix test failure --- .../companies/company.service.edge.test.ts | 39 +++++++++++++++ .../api/src/modules/companies/company.test.ts | 44 ++++++++++++++++- .../src/modules/payments/payment.service.ts | 49 ++++++++++--------- 3 files changed, 107 insertions(+), 25 deletions(-) diff --git a/apps/api/src/modules/companies/company.service.edge.test.ts b/apps/api/src/modules/companies/company.service.edge.test.ts index f78d1b1..fed3b53 100644 --- a/apps/api/src/modules/companies/company.service.edge.test.ts +++ b/apps/api/src/modules/companies/company.service.edge.test.ts @@ -1,6 +1,9 @@ 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('./settingsEntitlements', () => ({ + resolveSettingsEntitlements: vi.fn(), +})) vi.mock('./company.repo', () => ({ findCompany: vi.fn(), updateCompany: vi.fn(), @@ -28,8 +31,43 @@ vi.mock('./company.repo', () => ({ })) 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 currentBrand = { id: 'brand_1', companyId: 'company_1', @@ -44,6 +82,7 @@ const currentBrand = { beforeEach(() => { vi.clearAllMocks() + vi.mocked(entitlements.resolveSettingsEntitlements).mockResolvedValue(buildEditableEntitlements() as any) delete process.env.NEXT_PUBLIC_CUSTOM_DOMAIN_TARGET }) diff --git a/apps/api/src/modules/companies/company.test.ts b/apps/api/src/modules/companies/company.test.ts index 82be24a..7f0b611 100644 --- a/apps/api/src/modules/companies/company.test.ts +++ b/apps/api/src/modules/companies/company.test.ts @@ -1,12 +1,51 @@ 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('./settingsEntitlements', () => ({ + resolveSettingsEntitlements: vi.fn(), +})) vi.mock('../../lib/storage', () => ({ 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 = { id: 'comp_1', name: 'Test Rentals', @@ -33,6 +72,7 @@ const mockBrand = { beforeEach(() => { vi.clearAllMocks() + vi.mocked(entitlements.resolveSettingsEntitlements).mockResolvedValue(buildEditableEntitlements() as any) }) describe('company.service', () => { diff --git a/apps/api/src/modules/payments/payment.service.ts b/apps/api/src/modules/payments/payment.service.ts index 9e6ba0b..b11b947 100644 --- a/apps/api/src/modules/payments/payment.service.ts +++ b/apps/api/src/modules/payments/payment.service.ts @@ -12,6 +12,17 @@ export function listByReservation(reservationId: string, companyId: string) { 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) { const transactionId = event.transaction_id ?? event.id 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 rentalPayments = (reservation as any).rentalPayments ?? [] - const invoicePaid = rentalPayments.reduce((total: number, payment: any) => { - if (payment.type !== 'CHARGE' || payment.status !== 'SUCCEEDED') return total - return total + payment.amount - }, 0) - const depositCollected = rentalPayments.reduce((total: number, payment: any) => { - 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 + const invoicePaid = getInvoicePaid(reservation, rentalPayments) + const depositCollected = sumSucceededPayments(rentalPayments, 'DEPOSIT') + const balanceDue = body.type === 'DEPOSIT' + ? reservation.depositAmount - depositCollected + : reservation.totalAmount - invoicePaid - if (amount <= 0) { + if (balanceDue <= 0) { 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 orderId = `${reservationId}-${body.type}-${Date.now()}` 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 rentalPayments = (reservation as any).rentalPayments ?? [] - const invoicePaid = rentalPayments.reduce((total: number, payment: any) => { - if (payment.type !== 'CHARGE' || payment.status !== 'SUCCEEDED') return total - return total + payment.amount - }, 0) - const depositCollected = rentalPayments.reduce((total: number, payment: any) => { - 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 + const invoicePaid = getInvoicePaid(reservation, rentalPayments) + const depositCollected = sumSucceededPayments(rentalPayments, 'DEPOSIT') + const remaining = body.type === 'DEPOSIT' + ? reservation.depositAmount - depositCollected + : reservation.totalAmount - invoicePaid if (remaining <= 0) { 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) { throw new ValidationError(body.type === 'DEPOSIT' ? 'Payment amount exceeds deposit outstanding' : 'Payment amount exceeds remaining balance') }