import { createHash, randomUUID } from 'node:crypto' import { describe, expect, it } from 'vitest' import { getPool, runAsServiceRole, withUserContext } from './setup' import { insertAuthUser, insertCompanyMember, seedCompany } from './fixtures' const XML = '20267497' const XML_SHA = createHash('sha256').update(XML).digest('hex') async function insertRegistration(companyId: string, userId: string, identifier: string, status = 'registered') { const id = randomUUID() await getPool().query( `INSERT INTO public.peppol_registrations (id, company_id, user_id, provider, provider_account_reference, participant_scheme, participant_identifier, status, registered_at, deregistered_at) VALUES ($1, $2, $3, 'qvalia', 'SE5595386219', '0007', $4, $5, CASE WHEN $5 = 'registered' THEN now() ELSE NULL END, CASE WHEN $5 = 'deregistered' THEN now() ELSE NULL END)`, [id, companyId, userId, identifier, status], ) return id } async function insertInbound(companyId: string | null, providerDocumentId = randomUUID()) { const id = randomUUID() await getPool().query( `INSERT INTO public.peppol_inbound_documents (id, provider, provider_document_id, document_type, document_id, issue_date, currency, payable_amount, sender_scheme, sender_identifier, sender_name, recipient_scheme, recipient_identifier, company_id, status, xml_payload, xml_sha256) VALUES ($1, 'qvalia', $2, 'Invoice', '20267497', '2026-08-21', 'SEK', 112.00, '0007', '5567321707', 'Qvalia AB', '0007', '5595386219', $3, $4, $5, $6)`, [id, providerDocumentId, companyId, companyId ? 'routed' : 'received', XML, XML_SHA], ) return id } describe('peppol_registrations', () => { it('allows one live registration per participant and per company, history rows aside', async () => { const a = await seedCompany() const b = await seedCompany() await insertRegistration(a.companyId, a.userId, '5595386219') await expect(insertRegistration(b.companyId, b.userId, '5595386219')) .rejects.toThrow(/peppol_registrations_live_participant/) await expect(insertRegistration(a.companyId, a.userId, '5560160680')) .rejects.toThrow(/peppol_registrations_live_company/) // A deregistered history row does not block a new live one. await insertRegistration(b.companyId, b.userId, '5567321707', 'deregistered') await expect(insertRegistration(b.companyId, b.userId, '5567321707')).resolves.toBeTruthy() }) it('is readable by members of the company only and not writable by authenticated users', async () => { const own = await seedCompany() const other = await seedCompany() await insertRegistration(own.companyId, own.userId, '5590000001') await insertRegistration(other.companyId, other.userId, '5590000002') const visible = await withUserContext(own.userId, async (client) => { const { rows } = await client.query( `SELECT participant_identifier FROM public.peppol_registrations ORDER BY participant_identifier`, ) return rows.map((row) => row.participant_identifier as string) }) expect(visible).toEqual(['5590000001']) await expect(withUserContext(own.userId, (client) => client.query( `INSERT INTO public.peppol_registrations (company_id, provider, participant_scheme, participant_identifier) VALUES ($1, 'qvalia', '0007', '5590000003')`, [own.companyId], ), )).rejects.toThrow(/permission denied|row-level security/) }) }) describe('peppol_inbound_documents', () => { it('keeps the received document immutable and undeletable while processing state may change', async () => { const seeded = await seedCompany() const id = await insertInbound(seeded.companyId) await expect(getPool().query( `UPDATE public.peppol_inbound_documents SET xml_payload = '' WHERE id = $1`, [id], )).rejects.toThrow(/payload is immutable/) await expect(getPool().query( `UPDATE public.peppol_inbound_documents SET provider_document_id = 'other' WHERE id = $1`, [id], )).rejects.toThrow(/identity is immutable/) await expect(getPool().query( `DELETE FROM public.peppol_inbound_documents WHERE id = $1`, [id], )).rejects.toThrow(/cannot be deleted/) await expect(getPool().query( `UPDATE public.peppol_inbound_documents SET status = 'converted', processed_at = now(), summary = '{"ok":true}'::jsonb WHERE id = $1`, [id], )).resolves.toBeTruthy() }) it('routes once: company_id may be set from null but never changed afterwards', async () => { const a = await seedCompany() const b = await seedCompany() const id = await insertInbound(null) await getPool().query( `UPDATE public.peppol_inbound_documents SET company_id = $2, status = 'routed' WHERE id = $1`, [id, a.companyId], ) await expect(getPool().query( `UPDATE public.peppol_inbound_documents SET company_id = $2 WHERE id = $1`, [id, b.companyId], )).rejects.toThrow(/cannot be re-routed/) }) it('refuses a second copy of the same provider document and a routed status without a company', async () => { const seeded = await seedCompany() const providerDocumentId = randomUUID() await insertInbound(seeded.companyId, providerDocumentId) await expect(insertInbound(seeded.companyId, providerDocumentId)) .rejects.toThrow(/peppol_inbound_documents_provider_document_unique/) await expect(getPool().query( `INSERT INTO public.peppol_inbound_documents (provider, provider_document_id, document_type, status) VALUES ('qvalia', $1, 'Invoice', 'routed')`, [randomUUID()], )).rejects.toThrow(/peppol_inbound_documents_routed_shape/) }) it('is visible to members of the routed company only, never unrouted rows, and only the service role writes', async () => { const own = await seedCompany() const other = await seedCompany() const outsider = await insertAuthUser() await insertCompanyMember({ companyId: other.companyId, userId: outsider, role: 'owner' }) const ownDoc = await insertInbound(own.companyId) await insertInbound(other.companyId) await insertInbound(null) const visible = await withUserContext(own.userId, async (client) => { const { rows } = await client.query(`SELECT id FROM public.peppol_inbound_documents`) return rows.map((row) => row.id as string) }) expect(visible).toEqual([ownDoc]) await expect(withUserContext(own.userId, (client) => client.query( `INSERT INTO public.peppol_inbound_documents (provider, provider_document_id, document_type) VALUES ('qvalia', $1, 'Invoice')`, [randomUUID()], ), )).rejects.toThrow(/permission denied|row-level security/) const serviceCount = await runAsServiceRole(async (client) => { const { rows } = await client.query( `SELECT count(*)::int AS n FROM public.peppol_inbound_documents WHERE company_id IS NULL`, ) return rows[0].n as number }) expect(serviceCount).toBeGreaterThanOrEqual(1) }) })