/** * interests.service yacht-ownership validation integration tests. * * Covers: * - createInterest with yachtId succeeds when yacht is owned by the client * - createInterest with yachtId rejects when yacht belongs to a different client * - createInterest with yachtId succeeds when client is member of owning company * - createInterest without yachtId succeeds (stage=open is allowed) * - changeInterestStage rejects moving out of "open" when yachtId is null * - changeInterestStage succeeds when yachtId is set * - updateInterest validates yacht ownership when changing yachtId * * Uses dynamic imports (PR 8 pattern) so env is loaded before service modules * touch `db`. */ import { describe, it, expect, beforeAll } from 'vitest'; describe('interests.service — yacht ownership validation', () => { let createInterest: typeof import('@/lib/services/interests.service').createInterest; let updateInterest: typeof import('@/lib/services/interests.service').updateInterest; let changeInterestStage: typeof import('@/lib/services/interests.service').changeInterestStage; let makePort: typeof import('../helpers/factories').makePort; let makeClient: typeof import('../helpers/factories').makeClient; let makeYacht: typeof import('../helpers/factories').makeYacht; let makeCompany: typeof import('../helpers/factories').makeCompany; let makeMembership: typeof import('../helpers/factories').makeMembership; let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta; beforeAll(async () => { const svc = await import('@/lib/services/interests.service'); createInterest = svc.createInterest; updateInterest = svc.updateInterest; changeInterestStage = svc.changeInterestStage; const factories = await import('../helpers/factories'); makePort = factories.makePort; makeClient = factories.makeClient; makeYacht = factories.makeYacht; makeCompany = factories.makeCompany; makeMembership = factories.makeMembership; makeAuditMeta = factories.makeAuditMeta; }); it('createInterest with yachtId succeeds when yacht is owned by the client', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const yacht = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: client.id, }); const interest = await createInterest( port.id, { clientId: client.id, yachtId: yacht.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false, }, makeAuditMeta({ portId: port.id }), ); expect(interest.yachtId).toBe(yacht.id); expect(interest.clientId).toBe(client.id); }); it('createInterest with yachtId rejects when yacht belongs to a different client', async () => { const port = await makePort(); const clientA = await makeClient({ portId: port.id }); const clientB = await makeClient({ portId: port.id }); const yacht = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: clientA.id, }); await expect( createInterest( port.id, { clientId: clientB.id, yachtId: yacht.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false, }, makeAuditMeta({ portId: port.id }), ), ).rejects.toThrow(/yacht does not belong to this client/); }); it('createInterest with yachtId succeeds when client is member of owning company', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const company = await makeCompany({ portId: port.id }); await makeMembership({ companyId: company.id, clientId: client.id, role: 'director', endDate: null, }); const yacht = await makeYacht({ portId: port.id, ownerType: 'company', ownerId: company.id, }); const interest = await createInterest( port.id, { clientId: client.id, yachtId: yacht.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false, }, makeAuditMeta({ portId: port.id }), ); expect(interest.yachtId).toBe(yacht.id); }); it('createInterest without yachtId succeeds (stage=open is allowed)', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const interest = await createInterest( port.id, { clientId: client.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false }, makeAuditMeta({ portId: port.id }), ); expect(interest.yachtId).toBeNull(); expect(interest.pipelineStage).toBe('open'); }); it('changeInterestStage rejects moving out of "open" when yachtId is null', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const interest = await createInterest( port.id, { clientId: client.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false }, makeAuditMeta({ portId: port.id }), ); await expect( changeInterestStage( interest.id, port.id, { pipelineStage: 'details_sent' }, makeAuditMeta({ portId: port.id }), ), ).rejects.toThrow(/yachtId is required before leaving stage=open/); }); it('changeInterestStage succeeds when yachtId is set', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const yacht = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: client.id, }); const interest = await createInterest( port.id, { clientId: client.id, yachtId: yacht.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false, }, makeAuditMeta({ portId: port.id }), ); const updated = await changeInterestStage( interest.id, port.id, { pipelineStage: 'details_sent' }, makeAuditMeta({ portId: port.id }), ); expect(updated.pipelineStage).toBe('details_sent'); }); it('updateInterest validates yacht ownership when changing yachtId', async () => { const port = await makePort(); const clientA = await makeClient({ portId: port.id }); const clientB = await makeClient({ portId: port.id }); // Interest is owned by clientA; yacht belongs to clientB. const interest = await createInterest( port.id, { clientId: clientA.id, pipelineStage: 'open', tagIds: [], reminderEnabled: false }, makeAuditMeta({ portId: port.id }), ); const yachtOfB = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: clientB.id, }); await expect( updateInterest( interest.id, port.id, { yachtId: yachtOfB.id }, makeAuditMeta({ portId: port.id }), ), ).rejects.toThrow(/yacht does not belong to this client/); // ... and succeeds when swapping in a yacht that clientA actually owns. const yachtOfA = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: clientA.id, }); const updated = await updateInterest( interest.id, port.id, { yachtId: yachtOfA.id }, makeAuditMeta({ portId: port.id }), ); expect(updated.yachtId).toBe(yachtOfA.id); }); });