Fix/usr fdbck ch (#1105)

* fix(privacy): mask voucher amounts in session replays

* fix: persist transaction source filter

* fix: clarify invoice filenames and booking previews

* fix: truncate long uploaded filenames

* feat: add invoice delivery history

* fix: harden invoice delivery history

* fix: include invoice deliveries in full archive
This commit is contained in:
Mattsson
2026-07-22 18:49:57 +02:00
committed by GitHub
parent 3e1ea29d02
commit 321e684523
58 changed files with 3742 additions and 285 deletions
@@ -78,6 +78,98 @@ describe('proposeSendLines', () => {
})
})
it('does not create zero-value revenue rows for informational invoice items', () => {
const lines = proposeSendLines({
invoice: makeInvoiceInput({
items: [
makeItem(),
makeItem({
id: 'text-1',
line_type: 'text',
description: 'Information shown on the invoice',
quantity: 0,
unit_price: 0,
line_total: 0,
vat_rate: 0,
vat_amount: 0,
}),
],
}),
entityType: 'enskild_firma',
})
expect(lines.map((line) => line.account_number)).toEqual(['1510', '3001', '2611'])
expect(lines.some((line) =>
(parseFloat(line.debit_amount) || 0) === 0
&& (parseFloat(line.credit_amount) || 0) === 0
)).toBe(false)
})
it('ignores informational rows when selecting the legacy invoice VAT treatment', () => {
const lines = proposeSendLines({
invoice: makeInvoiceInput({
items: [
makeItem({ vat_rate: undefined }),
makeItem({
id: 'text-1',
line_type: 'text',
quantity: 0,
unit_price: 0,
line_total: 0,
vat_rate: 0,
vat_amount: 0,
}),
],
}),
entityType: 'enskild_firma',
})
expect(lines.map((line) => line.account_number)).toEqual(['1510', '3001', '2611'])
})
it('returns no booking proposal for an invoice containing only informational rows', () => {
const lines = proposeSendLines({
invoice: makeInvoiceInput({
total: 0,
subtotal: 0,
vat_amount: 0,
items: [
makeItem({
line_type: 'text',
quantity: 0,
unit_price: 0,
line_total: 0,
vat_rate: 0,
vat_amount: 0,
}),
],
}),
entityType: 'enskild_firma',
})
expect(lines).toEqual([])
})
it('rejects a non-zero invoice containing only informational rows', () => {
const lines = proposeSendLines({
invoice: makeInvoiceInput({
items: [
makeItem({
line_type: 'text',
quantity: 0,
unit_price: 0,
line_total: 0,
vat_rate: 0,
vat_amount: 0,
}),
],
}),
entityType: 'enskild_firma',
})
expect(lines).toEqual([])
})
it('credit note uses positive amounts on the reversed sides', () => {
const lines = proposeSendLines({
invoice: makeInvoiceInput({
+30 -9
View File
@@ -121,14 +121,22 @@ function buildSendLines(
// Build credit lines per VAT rate group
const creditLines: FormLine[] = []
const accountingItems = (invoice.items ?? []).filter((item) => item.line_type !== 'text')
if (invoice.items && invoice.items.length > 0) {
const hasPerLineVat = invoice.items.some((item) => item.vat_rate !== undefined && item.vat_rate !== null)
// Existing informational rows are never a valid source for an invoice-level
// amount. Returning no proposal keeps an inconsistent text-only invoice from
// producing a debit-only entry; the user must correct its economic rows.
if (accountingItems.length === 0 && (invoice.items?.length ?? 0) > 0) {
return []
}
if (accountingItems.length > 0) {
const hasPerLineVat = accountingItems.some((item) => item.vat_rate !== undefined && item.vat_rate !== null)
if (!hasPerLineVat) {
// Legacy: single rate from invoice level
const revenueAccount = getRevenueAccount(invoice.vat_treatment, entityType)
const subtotal = invoice.items.reduce((sum, item) => sum + item.line_total, 0)
const subtotal = accountingItems.reduce((sum, item) => sum + item.line_total, 0)
creditLines.push({
account_number: revenueAccount,
debit_amount: '',
@@ -136,7 +144,7 @@ function buildSendLines(
line_description: desc,
})
const totalVat = invoice.items.reduce((sum, item) => sum + (item.vat_amount || 0), 0)
const totalVat = accountingItems.reduce((sum, item) => sum + (item.vat_amount || 0), 0)
if (totalVat > 0) {
const vatAccount = getOutputVatAccount(invoice.vat_treatment)
creditLines.push({
@@ -149,7 +157,7 @@ function buildSendLines(
} else {
// Group items by vat_rate
const rateGroups = new Map<number, { subtotal: number; vatAmount: number }>()
for (const item of invoice.items) {
for (const item of accountingItems) {
const rate = item.vat_rate ?? 0
const group = rateGroups.get(rate) || { subtotal: 0, vatAmount: 0 }
group.subtotal += item.line_total
@@ -182,7 +190,7 @@ function buildSendLines(
}
}
}
} else {
} else if (!invoice.items || invoice.items.length === 0) {
// Fallback: invoice-level amounts
const revenueAccount = getRevenueAccount(invoice.vat_treatment, entityType)
const subtotalSek = resolveSekAmount(invoice.subtotal, invoice.subtotal_sek, invoice.currency, invoice.exchange_rate)
@@ -207,7 +215,7 @@ function buildSendLines(
const deductionLines: FormLine[] = []
let deductionTotal = 0
for (const item of invoice.items ?? []) {
for (const item of accountingItems) {
if (!item.deduction_type) continue
const deduction = computeDeduction({
unit_price: item.unit_price,
@@ -225,13 +233,26 @@ function buildSendLines(
})
}
// Text-only and other informational invoice rows carry zero totals. They
// must not become misleading 30xx rows in the booking preview.
const nonZeroCreditLines = creditLines.filter(
(line) => roundOre(parseFloat(line.credit_amount) || 0) !== 0,
)
// Debit: 1510 customer portion plus 1513 Skatteverket portion.
const totalCredits = creditLines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0)
const totalCredits = nonZeroCreditLines.reduce(
(sum, line) => sum + (parseFloat(line.credit_amount) || 0),
0,
)
const debitAmount = isForeign
? Math.round(totalCredits * 100) / 100
: resolveSekAmount(invoice.total, invoice.total_sek, invoice.currency, invoice.exchange_rate)
const customerReceivable = roundOre(debitAmount - deductionTotal)
if (customerReceivable === 0 && deductionLines.length === 0 && nonZeroCreditLines.length === 0) {
return []
}
lines.push({
account_number: '1510',
debit_amount: toFormAmount(customerReceivable),
@@ -240,7 +261,7 @@ function buildSendLines(
})
lines.push(...deductionLines)
lines.push(...creditLines)
lines.push(...nonZeroCreditLines)
return lines
}