* feat(invoices): Betald-stämpel i faktura-PDF + betalningsbekräftelse till kund Closes #1693. A paid faktura re-renders with a BETALD banner (paid date and amount) and "Betalt: X" followed by "Att betala: 0"; partially_paid gets the Betalt / Att betala (remaining) rows without a banner. Credit notes and proformas are unchanged. Labels in sv and en. The paid copy is its own document, a betalningsbekräftelse, never the archived original: GET /api/invoices/[id]/pdf?variant=paid refuses anything but status paid (409 INVOICE_PAYMENT_CONFIRMATION_NOT_PAID), names the file Betalningsbekraftelse-<nr>.pdf and never reads or replaces the delivery archive. invoice-pdf-source gains the 'payment_confirmation' re-render reason so the UI caveats it like any re-render. POST /api/invoices/[id]/send-payment-confirmation emails the paid PDF with a dedicated subject/body through the existing email service and recipient routing, without touching status, sent_at, journal entries or invoice_deliveries (no kind column there; logged via the route logger instead). Detail page: the two actions sit inside the Betald card (download paid copy, send confirmation with an up-front confirm dialog), not in the header row. No migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): one-line hint for the betalningsbekräftelse actions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { invoicePdfFilename, paymentConfirmationPdfFilename } from '../pdf-filename'
|
|
|
|
describe('invoicePdfFilename', () => {
|
|
it('includes company, customer, document type, number, and invoice date', () => {
|
|
expect(invoicePdfFilename({
|
|
companyName: 'Oppy',
|
|
customerName: 'Kund AB',
|
|
invoiceNumber: '2621',
|
|
invoiceDate: '2026-07-21',
|
|
})).toBe('Oppy x Kund AB Faktura nr 2621 20260721.pdf')
|
|
})
|
|
|
|
it('uses the correct label for credit notes and other document types', () => {
|
|
const base = {
|
|
companyName: 'Oppy',
|
|
customerName: 'Kund AB',
|
|
invoiceNumber: '42',
|
|
invoiceDate: '2026-07-21',
|
|
}
|
|
|
|
expect(invoicePdfFilename({ ...base, isCreditNote: true }))
|
|
.toContain('Kreditfaktura nr 42')
|
|
expect(invoicePdfFilename({ ...base, documentType: 'proforma' }))
|
|
.toContain('Proformafaktura nr 42')
|
|
expect(invoicePdfFilename({ ...base, documentType: 'delivery_note' }))
|
|
.toContain('Följesedel nr 42')
|
|
})
|
|
|
|
it('keeps drafts identifiable without inventing an invoice number', () => {
|
|
expect(invoicePdfFilename({
|
|
companyName: 'Oppy',
|
|
customerName: 'Kund AB',
|
|
invoiceId: 'bbbbbbbb-1111-2222-3333-cccccccccccc',
|
|
invoiceDate: '2026-07-21',
|
|
})).toBe('Oppy x Kund AB Faktura utkast-bbbbbbbb 20260721.pdf')
|
|
})
|
|
|
|
it('removes characters that are unsafe in cross-platform filenames', () => {
|
|
expect(invoicePdfFilename({
|
|
companyName: 'Oppy / Sverige',
|
|
customerName: 'Kund: "Nord" * AB',
|
|
invoiceNumber: '../26/21',
|
|
invoiceDate: '2026-07-21',
|
|
})).toBe('Oppy Sverige x Kund Nord AB Faktura nr .. 26 21 20260721.pdf')
|
|
})
|
|
|
|
it('falls back when company and customer names are empty', () => {
|
|
expect(invoicePdfFilename({
|
|
companyName: ' ',
|
|
customerName: null,
|
|
invoiceNumber: '2621',
|
|
invoiceDate: '2026-07-21',
|
|
})).toBe('Företag x Kund Faktura nr 2621 20260721.pdf')
|
|
})
|
|
|
|
it('keeps multibyte filenames within common filesystem byte limits', () => {
|
|
const filename = invoicePdfFilename({
|
|
companyName: '🚀'.repeat(60),
|
|
customerName: '漢'.repeat(60),
|
|
invoiceNumber: '2621',
|
|
invoiceDate: '2026-07-21',
|
|
})
|
|
|
|
expect(Buffer.byteLength(filename, 'utf8')).toBeLessThanOrEqual(255)
|
|
expect(filename).toMatch(/Faktura nr 2621 20260721\.pdf$/)
|
|
})
|
|
})
|
|
|
|
// #1693: the paid copy is named as a betalningsbekräftelse, never as the
|
|
// invoice, so the file cannot be mistaken for the one that was sent.
|
|
describe('paymentConfirmationPdfFilename', () => {
|
|
it('names the file after the invoice number', () => {
|
|
expect(paymentConfirmationPdfFilename('2621')).toBe('Betalningsbekraftelse-2621.pdf')
|
|
})
|
|
|
|
it('sanitizes unsafe characters and spaces in the number', () => {
|
|
expect(paymentConfirmationPdfFilename('F 2026/0042')).toBe('Betalningsbekraftelse-F-2026-0042.pdf')
|
|
})
|
|
|
|
it('falls back when the number is missing', () => {
|
|
expect(paymentConfirmationPdfFilename(null)).toBe('Betalningsbekraftelse-okand.pdf')
|
|
})
|
|
})
|