fix(invoices): force 0% VAT on recurring and bulk-created invoices when the company is not VAT registered (#1838)
Issue #1719: moms lands on an invoice even though momskrysset (company_settings.vat_registered) is off. The web and v1 create/update routes, the MCP commit, and the webshop route all zero every line via buildInvoiceWriteData, but two paths insert invoices directly and never consult vat_registered: 1. executeRecurringSchedule (cron + run-now): the schedule dialog defaults template lines to 25%, stores vat_rate with no gate, and the spawn falls back to the customer default (25% for Swedish customers) for null-rate lines. The generated invoice carried 25% output VAT and could be auto-emailed to the customer and booked against 2611. 2. POST /api/v1/.../invoices/bulk-create: same fallback, same direct insert. Both now mirror buildInvoiceWriteData: when vat_registered is false, every line is forced to 0% at spawn/create time, and the header lands as treatment 'exempt' with moms_ruta and reverse_charge_text null. Self-billed received invoices deliberately keep their stated VAT: the counterparty issued that document, and the books must mirror it (ML 16 kap 23 §). Credit notes keep mirroring the invoice they credit. Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
64119d30bc
commit
c93a97bb4e
@@ -331,6 +331,49 @@ describe('POST /api/v1/companies/:companyId/invoices/bulk-create', () => {
|
|||||||
expect(insertedInvoice).toBe(false)
|
expect(insertedInvoice).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('forces every line to 0% when the company is not VAT registered (issue #1719)', async () => {
|
||||||
|
// Momskrysset off (company_settings.vat_registered = false): the invoice
|
||||||
|
// must come out momsfri whether the payload sends an explicit rate or
|
||||||
|
// relies on the customer-default fallback (25% for a Swedish customer).
|
||||||
|
mockServiceClient.mockReturnValue(
|
||||||
|
makeFlexibleSupabase({
|
||||||
|
company_members: { data: { company_id: COMPANY_ID, role: 'owner' }, error: null },
|
||||||
|
customers: { data: VALID_CUSTOMER, error: null },
|
||||||
|
company_settings: { data: { vat_registered: false }, error: null },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await bulkCreate(
|
||||||
|
makeRequest(`https://x.test/api/v1/companies/${COMPANY_ID}/invoices/bulk-create?dry_run=true`, {
|
||||||
|
invoices: [
|
||||||
|
{
|
||||||
|
customer_id: CUSTOMER_ID,
|
||||||
|
invoice_date: '2026-05-12',
|
||||||
|
due_date: '2026-06-11',
|
||||||
|
currency: 'SEK',
|
||||||
|
items: [
|
||||||
|
{ ...SAMPLE_ITEM('Explicit 25'), vat_rate: 25 },
|
||||||
|
SAMPLE_ITEM('Fallback rate'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
companyParams(COMPANY_ID),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.data.preview.summary.succeeded).toBe(1)
|
||||||
|
const preview = body.data.preview.results[0].data.preview
|
||||||
|
expect(preview.subtotal).toBe(2000)
|
||||||
|
expect(preview.vat_amount).toBe(0)
|
||||||
|
expect(preview.total).toBe(2000)
|
||||||
|
for (const item of preview.items) {
|
||||||
|
expect(item.vat_rate).toBe(0)
|
||||||
|
expect(item.vat_amount).toBe(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects all_or_nothing: true with 501 NOT_IMPLEMENTED', async () => {
|
it('rejects all_or_nothing: true with 501 NOT_IMPLEMENTED', async () => {
|
||||||
mockServiceClient.mockReturnValue(
|
mockServiceClient.mockReturnValue(
|
||||||
makeFlexibleSupabase({
|
makeFlexibleSupabase({
|
||||||
|
|||||||
@@ -194,6 +194,21 @@ async function createOneInvoice(
|
|||||||
)
|
)
|
||||||
const allowedRates = new Set(permittedRates.map((r) => r.rate))
|
const allowedRates = new Set(permittedRates.map((r) => r.rate))
|
||||||
|
|
||||||
|
// VAT registration gate, mirroring buildInvoiceWriteData (issue #1719): a
|
||||||
|
// non-momsregistrerad company books no output VAT, so every line is forced
|
||||||
|
// to 0% (momsfri) server-side no matter what the batch payload carries,
|
||||||
|
// explicitly or via the customer-default fallback below. 0% is a permitted
|
||||||
|
// rate for every customer type, so the allowedRates gate still passes.
|
||||||
|
const { data: vatSettings } = await supabase
|
||||||
|
.from('company_settings')
|
||||||
|
.select('vat_registered')
|
||||||
|
.eq('company_id', companyId)
|
||||||
|
.maybeSingle()
|
||||||
|
const notVatRegistered = vatSettings?.vat_registered === false
|
||||||
|
if (notVatRegistered && documentType !== 'delivery_note') {
|
||||||
|
for (const item of input.items) item.vat_rate = 0
|
||||||
|
}
|
||||||
|
|
||||||
const subtotal = input.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0)
|
const subtotal = input.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0)
|
||||||
let vatAmount = 0
|
let vatAmount = 0
|
||||||
if (documentType !== 'delivery_note') {
|
if (documentType !== 'delivery_note') {
|
||||||
@@ -315,10 +330,13 @@ async function createOneInvoice(
|
|||||||
total,
|
total,
|
||||||
total_sek: documentType === 'delivery_note' ? null : totalSek,
|
total_sek: documentType === 'delivery_note' ? null : totalSek,
|
||||||
remaining_amount: documentType === 'invoice' ? total : 0,
|
remaining_amount: documentType === 'invoice' ? total : 0,
|
||||||
vat_treatment: vatRules.treatment,
|
// Header VAT fields mirror buildInvoiceWriteData: a not-VAT-registered
|
||||||
|
// company stamps the sale as momsfri (treatment 'exempt', no ruta, no
|
||||||
|
// reverse-charge notation); every line rate is already zeroed above.
|
||||||
|
vat_treatment: notVatRegistered ? 'exempt' : vatRules.treatment,
|
||||||
vat_rate: headerVatRate,
|
vat_rate: headerVatRate,
|
||||||
moms_ruta: vatRules.momsRuta,
|
moms_ruta: notVatRegistered ? null : vatRules.momsRuta,
|
||||||
reverse_charge_text: vatRules.reverseChargeText || null,
|
reverse_charge_text: notVatRegistered ? null : (vatRules.reverseChargeText || null),
|
||||||
your_reference: input.your_reference,
|
your_reference: input.your_reference,
|
||||||
our_reference: input.our_reference,
|
our_reference: input.our_reference,
|
||||||
notes: input.notes,
|
notes: input.notes,
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ describe('executeRecurringSchedule auto-send', () => {
|
|||||||
/** Queue for the full happy path (see call order in the service). */
|
/** Queue for the full happy path (see call order in the service). */
|
||||||
function enqueueHappyPath() {
|
function enqueueHappyPath() {
|
||||||
enqueue({ data: customer, error: null }) // customers select
|
enqueue({ data: customer, error: null }) // customers select
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: makeInsertedInvoice(), error: null }) // invoices insert
|
enqueue({ data: makeInsertedInvoice(), error: null }) // invoices insert
|
||||||
enqueue({ data: null, error: null }) // invoice_items insert
|
enqueue({ data: null, error: null }) // invoice_items insert
|
||||||
enqueue({ data: makeCompleteInvoice(), error: null }) // re-fetch with relations
|
enqueue({ data: makeCompleteInvoice(), error: null }) // re-fetch with relations
|
||||||
@@ -491,6 +492,7 @@ describe('executeRecurringSchedule auto-send', () => {
|
|||||||
it('does not reserve a delivery when the customer email is blank', async () => {
|
it('does not reserve a delivery when the customer email is blank', async () => {
|
||||||
const customerWithoutEmail = { ...customer, email: ' ' }
|
const customerWithoutEmail = { ...customer, email: ' ' }
|
||||||
enqueue({ data: customerWithoutEmail, error: null })
|
enqueue({ data: customerWithoutEmail, error: null })
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: makeInsertedInvoice(), error: null })
|
enqueue({ data: makeInsertedInvoice(), error: null })
|
||||||
enqueue({ data: null, error: null })
|
enqueue({ data: null, error: null })
|
||||||
enqueue({
|
enqueue({
|
||||||
@@ -508,6 +510,7 @@ describe('executeRecurringSchedule auto-send', () => {
|
|||||||
|
|
||||||
it('does not reserve an auto-send delivery when configured recipients exceed the limit', async () => {
|
it('does not reserve an auto-send delivery when configured recipients exceed the limit', async () => {
|
||||||
enqueue({ data: customer, error: null })
|
enqueue({ data: customer, error: null })
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: makeInsertedInvoice(), error: null })
|
enqueue({ data: makeInsertedInvoice(), error: null })
|
||||||
enqueue({ data: null, error: null })
|
enqueue({ data: null, error: null })
|
||||||
enqueue({ data: makeCompleteInvoice(), error: null })
|
enqueue({ data: makeCompleteInvoice(), error: null })
|
||||||
@@ -534,9 +537,10 @@ describe('executeRecurringSchedule auto-send', () => {
|
|||||||
|
|
||||||
it('never auto-sends from a sandbox company; invoice stays a numbered draft', async () => {
|
it('never auto-sends from a sandbox company; invoice stays a numbered draft', async () => {
|
||||||
mockIsSandbox.mockResolvedValue(true)
|
mockIsSandbox.mockResolvedValue(true)
|
||||||
// Sandbox bails before company_settings/payment-link/render/email, so the
|
// Sandbox bails before the send path's company_settings/payment-link/
|
||||||
// queue only covers invoice creation.
|
// render/email, so the queue only covers invoice creation.
|
||||||
enqueue({ data: customer, error: null })
|
enqueue({ data: customer, error: null })
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: makeInsertedInvoice(), error: null })
|
enqueue({ data: makeInsertedInvoice(), error: null })
|
||||||
enqueue({ data: null, error: null })
|
enqueue({ data: null, error: null })
|
||||||
enqueue({ data: makeCompleteInvoice(), error: null })
|
enqueue({ data: makeCompleteInvoice(), error: null })
|
||||||
@@ -556,6 +560,7 @@ describe('executeRecurringSchedule auto-send', () => {
|
|||||||
// isSandboxCompany resolution, so sending is suppressed even before the
|
// isSandboxCompany resolution, so sending is suppressed even before the
|
||||||
// service-internal sandbox check runs. Invoice creation is unaffected.
|
// service-internal sandbox check runs. Invoice creation is unaffected.
|
||||||
enqueue({ data: customer, error: null })
|
enqueue({ data: customer, error: null })
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: makeInsertedInvoice(), error: null })
|
enqueue({ data: makeInsertedInvoice(), error: null })
|
||||||
enqueue({ data: null, error: null })
|
enqueue({ data: null, error: null })
|
||||||
enqueue({ data: makeCompleteInvoice(), error: null })
|
enqueue({ data: makeCompleteInvoice(), error: null })
|
||||||
@@ -634,6 +639,7 @@ describe('executeRecurringSchedule VAT rate gate', () => {
|
|||||||
|
|
||||||
it('generates the invoice for a 12% schedule to a validated EU business', async () => {
|
it('generates the invoice for a 12% schedule to a validated EU business', async () => {
|
||||||
enqueue({ data: euCustomer, error: null }) // customers select
|
enqueue({ data: euCustomer, error: null }) // customers select
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
||||||
enqueue({ data: null, error: null }) // invoice_items insert
|
enqueue({ data: null, error: null }) // invoice_items insert
|
||||||
enqueue({
|
enqueue({
|
||||||
@@ -649,7 +655,8 @@ describe('executeRecurringSchedule VAT rate gate', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('still throws for a rate that is not a Swedish VAT rate', async () => {
|
it('still throws for a rate that is not a Swedish VAT rate', async () => {
|
||||||
enqueue({ data: euCustomer, error: null }) // customers select; throws before any insert
|
enqueue({ data: euCustomer, error: null }) // customers select
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate; throws before any insert
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
executeRecurringSchedule(client, makeScheduleWithRate(10), today, { suppressAutoSend: true }),
|
executeRecurringSchedule(client, makeScheduleWithRate(10), today, { suppressAutoSend: true }),
|
||||||
@@ -702,6 +709,7 @@ describe('executeRecurringSchedule foreign-currency rate fetch', () => {
|
|||||||
|
|
||||||
function enqueueCreateOnlyPath() {
|
function enqueueCreateOnlyPath() {
|
||||||
enqueue({ data: customer, error: null }) // customers select
|
enqueue({ data: customer, error: null }) // customers select
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
||||||
enqueue({ data: null, error: null }) // invoice_items insert
|
enqueue({ data: null, error: null }) // invoice_items insert
|
||||||
enqueue({
|
enqueue({
|
||||||
@@ -834,6 +842,7 @@ describe('executeRecurringSchedule dimension propagation', () => {
|
|||||||
|
|
||||||
function enqueueCreatePath() {
|
function enqueueCreatePath() {
|
||||||
enqueue({ data: customer, error: null }) // customers select
|
enqueue({ data: customer, error: null }) // customers select
|
||||||
|
enqueue({ data: { vat_registered: true }, error: null }) // company_settings VAT gate
|
||||||
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
||||||
enqueue({ data: null, error: null }) // invoice_items insert
|
enqueue({ data: null, error: null }) // invoice_items insert
|
||||||
enqueue({
|
enqueue({
|
||||||
@@ -882,3 +891,154 @@ describe('executeRecurringSchedule dimension propagation', () => {
|
|||||||
expect(itemRows.every((row) => JSON.stringify(row.dimensions) === '{}')).toBe(true)
|
expect(itemRows.every((row) => JSON.stringify(row.dimensions) === '{}')).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('executeRecurringSchedule VAT registration gate (issue #1719)', () => {
|
||||||
|
const { supabase, enqueue, reset } = createQueuedMockSupabase()
|
||||||
|
const client = supabase as unknown as SupabaseClient
|
||||||
|
const today = new Date('2026-07-06T06:30:00Z')
|
||||||
|
|
||||||
|
const customer = makeCustomer({ id: 'cust-1', customer_type: 'swedish_business' })
|
||||||
|
|
||||||
|
// Capture .insert payloads per table (same pattern as the dimension tests:
|
||||||
|
// the queued mock's chain proxy discards call args by design).
|
||||||
|
const originalFrom = supabase.from.getMockImplementation()!
|
||||||
|
const inserted: Record<string, unknown[]> = {}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
reset()
|
||||||
|
eventBus.clear()
|
||||||
|
mockEnsureNumber.mockResolvedValue('F-1')
|
||||||
|
for (const key of Object.keys(inserted)) delete inserted[key]
|
||||||
|
supabase.from.mockImplementation((table: string) => {
|
||||||
|
const chain = originalFrom(table) as object
|
||||||
|
return new Proxy(chain, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (prop === 'insert') {
|
||||||
|
return (rows: unknown) => {
|
||||||
|
;(inserted[table] ??= []).push(rows)
|
||||||
|
return (Reflect.get(target, prop, receiver) as (r: unknown) => unknown)(rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop, receiver)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function makeSchedule() {
|
||||||
|
return {
|
||||||
|
id: 'sched-1',
|
||||||
|
company_id: 'company-1',
|
||||||
|
user_id: 'user-1',
|
||||||
|
customer_id: 'cust-1',
|
||||||
|
name: 'Monthly retainer',
|
||||||
|
day_of_month: 6,
|
||||||
|
send_hour: 8,
|
||||||
|
payment_terms_days: 30,
|
||||||
|
currency: 'SEK',
|
||||||
|
your_reference: null,
|
||||||
|
our_reference: null,
|
||||||
|
notes: null,
|
||||||
|
auto_send: false,
|
||||||
|
status: 'active',
|
||||||
|
next_run_date: '2026-07-06',
|
||||||
|
last_run_at: null,
|
||||||
|
last_invoice_id: null,
|
||||||
|
last_run_warning: null,
|
||||||
|
generated_count: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'si-1',
|
||||||
|
schedule_id: 'sched-1',
|
||||||
|
sort_order: 0,
|
||||||
|
description: 'Konsulttimmar',
|
||||||
|
quantity: 10,
|
||||||
|
unit: 'tim',
|
||||||
|
unit_price: 1000,
|
||||||
|
// The dialog's default for a new template line.
|
||||||
|
vat_rate: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'si-2',
|
||||||
|
schedule_id: 'sched-1',
|
||||||
|
sort_order: 1,
|
||||||
|
description: 'Serviceavgift',
|
||||||
|
quantity: 1,
|
||||||
|
unit: 'st',
|
||||||
|
unit_price: 500,
|
||||||
|
// null = inherit customer default at spawn time (25% for a Swedish
|
||||||
|
// customer), the other leg of the bug.
|
||||||
|
vat_rate: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Parameters<typeof executeRecurringSchedule>[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueCreatePath(vatRegistered: boolean | null) {
|
||||||
|
enqueue({ data: customer, error: null }) // customers select
|
||||||
|
enqueue({
|
||||||
|
data: vatRegistered === null ? null : { vat_registered: vatRegistered },
|
||||||
|
error: null,
|
||||||
|
}) // company_settings VAT gate
|
||||||
|
enqueue({ data: { id: 'inv-1', invoice_number: null, document_type: 'invoice' }, error: null }) // invoices insert
|
||||||
|
enqueue({ data: null, error: null }) // invoice_items insert
|
||||||
|
enqueue({
|
||||||
|
data: { id: 'inv-1', invoice_number: 'F-1', customer, items: [] },
|
||||||
|
error: null,
|
||||||
|
}) // re-fetch with relations
|
||||||
|
}
|
||||||
|
|
||||||
|
it('spawns a momsfri invoice when the company is not VAT registered', async () => {
|
||||||
|
// The reported bug: momskrysset (company_settings.vat_registered) is off,
|
||||||
|
// yet the cron-spawned invoice carried 25% moms, from the stored template
|
||||||
|
// rate and from the customer-default fallback for null-rate lines.
|
||||||
|
enqueueCreatePath(false)
|
||||||
|
|
||||||
|
await executeRecurringSchedule(client, makeSchedule(), today, { suppressAutoSend: true })
|
||||||
|
|
||||||
|
expect(inserted['invoices']).toHaveLength(1)
|
||||||
|
expect(inserted['invoices'][0]).toMatchObject({
|
||||||
|
subtotal: 10500,
|
||||||
|
vat_amount: 0,
|
||||||
|
total: 10500,
|
||||||
|
vat_treatment: 'exempt',
|
||||||
|
vat_rate: 0,
|
||||||
|
moms_ruta: null,
|
||||||
|
reverse_charge_text: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemRows = inserted['invoice_items'][0] as Array<Record<string, unknown>>
|
||||||
|
expect(itemRows).toHaveLength(2)
|
||||||
|
for (const row of itemRows) {
|
||||||
|
expect(row.vat_rate).toBe(0)
|
||||||
|
expect(row.vat_amount).toBe(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps VAT for a registered company', async () => {
|
||||||
|
enqueueCreatePath(true)
|
||||||
|
|
||||||
|
await executeRecurringSchedule(client, makeSchedule(), today, { suppressAutoSend: true })
|
||||||
|
|
||||||
|
expect(inserted['invoices'][0]).toMatchObject({
|
||||||
|
subtotal: 10500,
|
||||||
|
vat_amount: 2625,
|
||||||
|
total: 13125,
|
||||||
|
vat_treatment: 'standard_25',
|
||||||
|
vat_rate: 25,
|
||||||
|
moms_ruta: '05',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats a missing company_settings row as registered (no behavior change)', async () => {
|
||||||
|
enqueueCreatePath(null)
|
||||||
|
|
||||||
|
await executeRecurringSchedule(client, makeSchedule(), today, { suppressAutoSend: true })
|
||||||
|
|
||||||
|
expect(inserted['invoices'][0]).toMatchObject({
|
||||||
|
vat_amount: 2625,
|
||||||
|
vat_treatment: 'standard_25',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -280,6 +280,25 @@ export async function executeRecurringSchedule(
|
|||||||
throw new Error(`schedule ${schedule.id} has no items`)
|
throw new Error(`schedule ${schedule.id} has no items`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VAT registration gate, mirroring buildInvoiceWriteData (issue #1719): a
|
||||||
|
// non-momsregistrerad company books no output VAT, so the spawned invoice
|
||||||
|
// must be momsfri regardless of what the schedule template says. Both a
|
||||||
|
// stored template rate (the dialog defaults new lines to 25%, and older
|
||||||
|
// schedules may predate a deregistration) and the null-rate fallback to the
|
||||||
|
// customer default below (25% for Swedish customers) would otherwise put
|
||||||
|
// VAT on the cron-generated invoice even though momskrysset is off. Zero
|
||||||
|
// every line at spawn time; 0% is a permitted rate for every customer type,
|
||||||
|
// so the allowedRates gate below still passes.
|
||||||
|
const { data: vatSettings } = await supabase
|
||||||
|
.from('company_settings')
|
||||||
|
.select('vat_registered')
|
||||||
|
.eq('company_id', schedule.company_id)
|
||||||
|
.maybeSingle()
|
||||||
|
const notVatRegistered = vatSettings?.vat_registered === false
|
||||||
|
if (notVatRegistered) {
|
||||||
|
for (const item of items) item.vat_rate = 0
|
||||||
|
}
|
||||||
|
|
||||||
const subtotal = items.reduce((sum, it) => sum + it.quantity * it.unit_price, 0)
|
const subtotal = items.reduce((sum, it) => sum + it.quantity * it.unit_price, 0)
|
||||||
let vatAmount = 0
|
let vatAmount = 0
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
@@ -352,10 +371,13 @@ export async function executeRecurringSchedule(
|
|||||||
total,
|
total,
|
||||||
total_sek: totalSek,
|
total_sek: totalSek,
|
||||||
remaining_amount: total,
|
remaining_amount: total,
|
||||||
vat_treatment: vatRules.treatment,
|
// Header VAT fields mirror buildInvoiceWriteData: a not-VAT-registered
|
||||||
|
// company stamps the sale as momsfri (treatment 'exempt', no ruta, no
|
||||||
|
// reverse-charge notation); every line rate is already zeroed above.
|
||||||
|
vat_treatment: notVatRegistered ? 'exempt' : vatRules.treatment,
|
||||||
vat_rate: isMixedRate ? null : (uniqueRates.values().next().value ?? vatRules.rate),
|
vat_rate: isMixedRate ? null : (uniqueRates.values().next().value ?? vatRules.rate),
|
||||||
moms_ruta: vatRules.momsRuta,
|
moms_ruta: notVatRegistered ? null : vatRules.momsRuta,
|
||||||
reverse_charge_text: vatRules.reverseChargeText || null,
|
reverse_charge_text: notVatRegistered ? null : (vatRules.reverseChargeText || null),
|
||||||
your_reference: schedule.your_reference,
|
your_reference: schedule.your_reference,
|
||||||
our_reference: schedule.our_reference,
|
our_reference: schedule.our_reference,
|
||||||
notes: schedule.notes,
|
notes: schedule.notes,
|
||||||
|
|||||||
Reference in New Issue
Block a user