/** * invoices.service billing-entity integration tests. * * Covers: * - createInvoice with billingEntity=client snapshots clientName from client.fullName * - createInvoice with billingEntity=company snapshots clientName from company.name * - createInvoice throws ValidationError when client does not exist * - createInvoice throws ValidationError when company is in another tenant (cross-tenant) * - createInvoice uses entity primary email + address when none provided * - createInvoice allows caller to override billingEmail and billingAddress * * Uses dynamic imports (PR 8 pattern) so env is loaded before service modules * touch `db`. */ import { describe, it, expect, beforeAll } from 'vitest'; describe('invoices.service — billing entity', () => { let createInvoice: typeof import('@/lib/services/invoices').createInvoice; let makePort: typeof import('../helpers/factories').makePort; let makeClient: typeof import('../helpers/factories').makeClient; let makeCompany: typeof import('../helpers/factories').makeCompany; let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta; let db: typeof import('@/lib/db').db; let clientContacts: typeof import('@/lib/db/schema/clients').clientContacts; let clientAddresses: typeof import('@/lib/db/schema/clients').clientAddresses; let companyAddresses: typeof import('@/lib/db/schema/companies').companyAddresses; let ValidationError: typeof import('@/lib/errors').ValidationError; beforeAll(async () => { const svc = await import('@/lib/services/invoices'); createInvoice = svc.createInvoice; const factories = await import('../helpers/factories'); makePort = factories.makePort; makeClient = factories.makeClient; makeCompany = factories.makeCompany; makeAuditMeta = factories.makeAuditMeta; const dbMod = await import('@/lib/db'); db = dbMod.db; const clientsSchema = await import('@/lib/db/schema/clients'); clientContacts = clientsSchema.clientContacts; clientAddresses = clientsSchema.clientAddresses; const companiesSchema = await import('@/lib/db/schema/companies'); companyAddresses = companiesSchema.companyAddresses; const errors = await import('@/lib/errors'); ValidationError = errors.ValidationError; }); it('creates an invoice with billingEntity=client and snapshots clientName from client.fullName', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id, overrides: { fullName: 'Alice Admiral' }, }); const invoice = await createInvoice( port.id, { billingEntity: { type: 'client', id: client.id }, dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Dockage', quantity: 1, unitPrice: 500 }], }, makeAuditMeta({ portId: port.id }), ); expect(invoice.id).toBeTruthy(); expect(invoice.billingEntityType).toBe('client'); expect(invoice.billingEntityId).toBe(client.id); expect(invoice.clientName).toBe('Alice Admiral'); expect(invoice.portId).toBe(port.id); }); it('creates an invoice with billingEntity=company and snapshots clientName from company.name', async () => { const port = await makePort(); const company = await makeCompany({ portId: port.id, overrides: { name: 'Poseidon Holdings Ltd' }, }); const invoice = await createInvoice( port.id, { billingEntity: { type: 'company', id: company.id }, dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Marina services', quantity: 2, unitPrice: 1250 }], }, makeAuditMeta({ portId: port.id }), ); expect(invoice.billingEntityType).toBe('company'); expect(invoice.billingEntityId).toBe(company.id); expect(invoice.clientName).toBe('Poseidon Holdings Ltd'); }); it('throws ValidationError when billing entity (client) does not exist', async () => { const port = await makePort(); await expect( createInvoice( port.id, { billingEntity: { type: 'client', id: 'nonexistent-client-id' }, dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Dockage', quantity: 1, unitPrice: 100 }], }, makeAuditMeta({ portId: port.id }), ), ).rejects.toBeInstanceOf(ValidationError); }); it('throws ValidationError when billing entity (company) is in a different port', async () => { const portA = await makePort(); const portB = await makePort(); const companyInB = await makeCompany({ portId: portB.id }); await expect( createInvoice( portA.id, { billingEntity: { type: 'company', id: companyInB.id }, dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Dockage', quantity: 1, unitPrice: 100 }], }, makeAuditMeta({ portId: portA.id }), ), ).rejects.toBeInstanceOf(ValidationError); }); it('uses entity primary email + address when none provided in the request', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id, overrides: { fullName: 'Bob Bosun' }, }); // Add a primary email contact and a primary address await db.insert(clientContacts).values({ clientId: client.id, channel: 'email', value: 'bob@example.com', isPrimary: true, }); await db.insert(clientAddresses).values({ clientId: client.id, portId: port.id, label: 'Primary', streetAddress: '1 Pier Road', city: 'Harbor City', stateProvince: 'CA', postalCode: '90000', country: 'USA', isPrimary: true, }); const invoice = await createInvoice( port.id, { billingEntity: { type: 'client', id: client.id }, dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Dockage', quantity: 1, unitPrice: 100 }], }, makeAuditMeta({ portId: port.id }), ); expect(invoice.billingEmail).toBe('bob@example.com'); expect(invoice.billingAddress).toBe('1 Pier Road, Harbor City, CA, 90000, USA'); }); it('allows caller to override billingEmail and billingAddress', async () => { const port = await makePort(); const company = await makeCompany({ portId: port.id, overrides: { name: 'Nautical Corp', billingEmail: 'billing@nautical.example' }, }); await db.insert(companyAddresses).values({ companyId: company.id, portId: port.id, label: 'Primary', streetAddress: '2 Ocean Blvd', city: 'Portville', stateProvince: 'FL', postalCode: '33101', country: 'USA', isPrimary: true, }); const invoice = await createInvoice( port.id, { billingEntity: { type: 'company', id: company.id }, billingEmail: 'override@example.com', billingAddress: 'Custom address line', dueDate: '2026-12-31', paymentTerms: 'net30', currency: 'USD', lineItems: [{ description: 'Dockage', quantity: 1, unitPrice: 100 }], }, makeAuditMeta({ portId: port.id }), ); expect(invoice.billingEmail).toBe('override@example.com'); expect(invoice.billingAddress).toBe('Custom address line'); // clientName snapshot is still entity-derived (not overridable on create) expect(invoice.clientName).toBe('Nautical Corp'); }); });