Mcp/entry bug (#580)

* feat: restore document version supersession on posted entries

* refactor: update VAT handling logic to remove dependency on seller registration status

* feat: update VAT handling to respect seller registration status across invoice processing

* feat: add no-op migration placeholder for document supersession
This commit is contained in:
Mattsson
2026-05-27 17:07:27 +02:00
committed by GitHub
parent 8979f6eda3
commit 627109b5bd
8 changed files with 146 additions and 33 deletions
+21 -18
View File
@@ -67,11 +67,13 @@ describe('getAvailableVatRates', () => {
expect(rates).toHaveLength(4)
})
it('collapses to single 0% exempt option when seller is NOT VAT-registered', () => {
// ML 1 kap. 1§ — a non-skattskyldig seller may not charge VAT, so the
// picker must offer 0% only, regardless of customer type.
for (const ct of ['individual', 'swedish_business', 'eu_business', 'non_eu_business'] as const) {
const rates = getAvailableVatRates(ct, true, false)
it('collapses to 0%/exempt for non-registered sellers regardless of customer type', () => {
// ML 1 kap. 1§ — only a skattskyldig may charge output VAT. The picker
// must never offer non-zero rates when vat_registered=false; the API
// route (route.ts) and preview-pdf route enforce the same gate so a
// client bypassing the UI also gets rejected.
for (const customerType of ['individual', 'swedish_business', 'eu_business', 'non_eu_business'] as const) {
const rates = getAvailableVatRates(customerType, false, false)
expect(rates).toHaveLength(1)
expect(rates[0]).toEqual({
rate: 0,
@@ -81,10 +83,10 @@ describe('getAvailableVatRates', () => {
}
})
it('defaults vatRegistered to true (current behavior preserved)', () => {
// Existing callers omit the third arg — they must still see the full
// rate set for Swedish customers.
const rates = getAvailableVatRates('swedish_business')
it('defaults vatRegistered to true (legitimate VAT path)', () => {
// Explicit default-arg pin so a future signature change doesn't silently
// flip the default and strip VAT from invoices for registered sellers.
const rates = getAvailableVatRates('swedish_business', false)
expect(rates).toHaveLength(4)
})
})
@@ -177,11 +179,12 @@ describe('getVatRules', () => {
})
})
it('short-circuits to exempt/0/empty momsRuta when seller is NOT VAT-registered', () => {
// ML 1 kap. 1§ — no output VAT, no momsdeklaration row, regardless of
// customer type. Verified for all four customer types.
for (const ct of ['individual', 'swedish_business', 'eu_business', 'non_eu_business'] as const) {
const rules = getVatRules(ct, true, false)
it('short-circuits to exempt / rate 0 / empty momsRuta for non-registered sellers', () => {
// ML 1 kap. 1§ — non-skattskyldig cannot charge output VAT. momsRuta is
// intentionally empty so a downstream momsdeklaration generator never
// mis-files a non-registered seller's revenue into ruta 05.
for (const customerType of ['individual', 'swedish_business', 'eu_business', 'non_eu_business'] as const) {
const rules = getVatRules(customerType, false, false)
expect(rules).toEqual({
treatment: 'exempt',
rate: 0,
@@ -190,12 +193,12 @@ describe('getVatRules', () => {
}
})
it('defaults vatRegistered to true (current behavior preserved)', () => {
// Existing callers omit the third arg — they must still see standard_25
// for Swedish customers.
const rules = getVatRules('swedish_business')
it('defaults vatRegistered to true (legitimate VAT path)', () => {
// Explicit default-arg pin (see corresponding getAvailableVatRates test).
const rules = getVatRules('swedish_business', false)
expect(rules.rate).toBe(25)
expect(rules.treatment).toBe('standard_25')
expect(rules.momsRuta).toBe('05')
})
})
+5 -2
View File
@@ -873,7 +873,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
// charge output VAT, so a "Moms 0%" line would imply VAT
// accounting that doesn't exist. The notice block below the
// payment section explains the absence of VAT.
!(company.vat_registered === false && invoice.vat_amount === 0) && (
company.vat_registered !== false && (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>{L.vatRow(invoice.vat_rate ?? (vatByRate.size === 1 ? (vatByRate.keys().next().value ?? 0) : 0))}</Text>
<Text style={styles.totalValue}>{formatCurrency(invoice.vat_amount, invoice.currency, lang)}</Text>
@@ -1069,7 +1069,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
don't apply, and a single dedicated notice is clearer for the
customer than reusing the exempt notice (which implies the sale
specifically is exempt while the seller is otherwise within the
VAT system). */}
VAT system). Server-side enforcement (api/invoices/route.ts +
preview-pdf/route.ts) coerces vat_amount to 0 for non-registered
sellers, so this branch always lines up with what's in the
totals block. */}
{company.vat_registered === false ? (
<View style={styles.reverseChargeBox}>
<Text style={styles.reverseChargeText}>{L.notVatRegisteredNotice}</Text>
+5 -2
View File
@@ -15,7 +15,9 @@ export interface VatRateOption {
* When the seller is not VAT-registered (`vatRegistered=false`), every customer
* type collapses to a single 0% / exempt option. ML 1 kap. 1§ — only a
* skattskyldig person may charge VAT, so the picker must never offer non-zero
* rates in that mode.
* rates in that mode. ML 16 kap. 23 § (faktureringsmoms) imposes liability for
* VAT erroneously stated on a document, but does NOT grant the right to charge
* it — so we block at source rather than allow + warn.
*/
export function getAvailableVatRates(
customerType: CustomerType,
@@ -82,7 +84,8 @@ export interface VatRule {
* When the seller is not VAT-registered (`vatRegistered=false`), the rules
* short-circuit to `{ treatment: 'exempt', rate: 0, momsRuta: '' }` regardless
* of customer type — ML 1 kap. 1§ bars a non-skattskyldig from charging output
* VAT. `momsRuta` is empty so the invoice doesn't claim a momsdeklaration row.
* VAT. `momsRuta` is empty so a downstream momsdeklaration generator never
* mis-files a non-registered seller's "sales" into ruta 05.
*/
export function getVatRules(
customerType: CustomerType,
+2 -2
View File
@@ -2198,7 +2198,7 @@ async function commitCorrectEntry(
// commit-time gate matches the staging-time signal.
const { data: original, error: origErr } = await supabase
.from('journal_entries')
.select('id, status, entry_date, fiscal_period_id, fiscal_periods!inner(is_closed, locked_at)')
.select('id, status, entry_date, fiscal_period_id, fiscal_periods!journal_entries_fiscal_period_id_fkey!inner(is_closed, locked_at)')
.eq('id', entryId)
.eq('company_id', companyId)
.maybeSingle()
@@ -2281,7 +2281,7 @@ async function commitReverseEntry(
// via resolvePeriodStatusForDate, matching the staging-time signal.
const { data: original, error: origErr } = await supabase
.from('journal_entries')
.select('id, status, entry_date, fiscal_period_id, fiscal_periods!inner(is_closed, locked_at)')
.select('id, status, entry_date, fiscal_period_id, fiscal_periods!journal_entries_fiscal_period_id_fkey!inner(is_closed, locked_at)')
.eq('id', entryId)
.eq('company_id', companyId)
.maybeSingle()