Fix/invoice delivery and payment accounts (#1116)
* fix: reconcile annual reports with final closing entries * test: cover annual report depreciation and VAT balances * Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch * fix: show exact invoice delivery details * fix: use currency account in invoice emails * fix: address invoice delivery review feedback * fix: harden invoice delivery and payment accounts * test: assert RLS-denied zero-row updates * fix: close remaining invoice compliance gaps * fix: harden invoice archive authorization * fix: close invoice delivery review findings * fix: verify delivery finalization results * fix: cap combined invoice email recipients * fix: close final invoice compliance findings * fix: prevent stale payment account saves * test: prove invoice delivery isolation * fix: close invoice privacy review findings * test: normalize delivery retention dates
This commit is contained in:
@@ -97,6 +97,118 @@ describe('PUT /api/settings', () => {
|
||||
expect(deadlineMocks.regenerate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates invoice email recipients and payment accounts', async () => {
|
||||
const updates = {
|
||||
invoice_email_cc_addresses: ['info@example.com', 'owner@example.com'],
|
||||
invoice_email_bcc_addresses: ['archive@example.com'],
|
||||
invoice_payment_accounts: {
|
||||
EUR: {
|
||||
bank_name: 'Example Bank',
|
||||
iban: 'SE0022222222222222222222',
|
||||
bic: 'EXAMSESS',
|
||||
},
|
||||
},
|
||||
}
|
||||
enqueueMany([
|
||||
{ data: { entity_type: 'aktiebolag', onboarding_complete: true } },
|
||||
{ data: { role: 'admin' } },
|
||||
{ data: { id: 's1', ...updates } },
|
||||
{ data: null, count: 5 },
|
||||
])
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: updates,
|
||||
}), { params: Promise.resolve({}) })
|
||||
const { status, body } = await parseJsonResponse<{ data: typeof updates }>(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data).toMatchObject(updates)
|
||||
})
|
||||
|
||||
it('rejects fixed invoice recipient changes from a regular member', async () => {
|
||||
enqueueMany([
|
||||
{ data: { entity_type: 'aktiebolag', onboarding_complete: true } },
|
||||
{ data: { role: 'member' }, error: null },
|
||||
])
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: { invoice_email_bcc_addresses: ['archive@example.com'] },
|
||||
}), { params: Promise.resolve({}) })
|
||||
const { status, body } = await parseJsonResponse<{
|
||||
error: { code: string; details?: { required_roles?: string[] } }
|
||||
}>(response)
|
||||
|
||||
expect(status).toBe(403)
|
||||
expect(body.error.code).toBe('FORBIDDEN')
|
||||
expect(body.error.details?.required_roles).toEqual(['owner', 'admin'])
|
||||
expect(supabase.from.mock.calls.map(([table]) => table)).toEqual([
|
||||
'company_settings',
|
||||
'company_members',
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects invoice payment instruction changes from a regular member', async () => {
|
||||
enqueueMany([
|
||||
{ data: { entity_type: 'aktiebolag', onboarding_complete: true } },
|
||||
{ data: { role: 'member' }, error: null },
|
||||
])
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
invoice_payment_accounts: {
|
||||
SEK: { bankgiro: '123-4567' },
|
||||
},
|
||||
bankgiro: '123-4567',
|
||||
},
|
||||
}), { params: Promise.resolve({}) })
|
||||
const { status, body } = await parseJsonResponse<{
|
||||
error: { code: string; details?: { required_roles?: string[] } }
|
||||
}>(response)
|
||||
|
||||
expect(status).toBe(403)
|
||||
expect(body.error.code).toBe('FORBIDDEN')
|
||||
expect(body.error.details?.required_roles).toEqual(['owner', 'admin'])
|
||||
expect(supabase.from.mock.calls.map(([table]) => table)).toEqual([
|
||||
'company_settings',
|
||||
'company_members',
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects invalid invoice recipients with otherwise valid payment accounts', async () => {
|
||||
enqueue({ data: { entity_type: 'aktiebolag', onboarding_complete: true } })
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
invoice_email_cc_addresses: ['not-an-email'],
|
||||
invoice_payment_accounts: {
|
||||
EUR: { bank_name: 'Example Bank', iban: 'SE0022222222222222222222' },
|
||||
},
|
||||
},
|
||||
}), { params: Promise.resolve({}) })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(supabase.from).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('rejects a foreign payment account without IBAN with valid recipients', async () => {
|
||||
enqueue({ data: { entity_type: 'aktiebolag', onboarding_complete: true } })
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
invoice_email_cc_addresses: ['billing@example.com'],
|
||||
invoice_payment_accounts: { EUR: { bank_name: 'Example Bank' } },
|
||||
},
|
||||
}), { params: Promise.resolve({}) })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(supabase.from).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('regenerates deadlines when unchanged tax settings are saved', async () => {
|
||||
const settings = {
|
||||
company_id: 'company-1',
|
||||
|
||||
Reference in New Issue
Block a user