feat(webshop): generate orderunderlag and attach it to the verifikat at booking (#1899)
* feat(webshop): generate orderunderlag PDF and attach it to the verifikat at booking Booked webshop orders only carried the VAT split; the verifikat showed no product lines, customer or payment method although the sync already stores all of it in webshop_orders.line_items (#1881). - lib/webshop-orders/order-underlag.tsx: pure model builder + react-pdf template (order lines, customer, payment method, per-rate VAT summary, SEK conversion facts) + archiveWebshopOrderUnderlag, which renders and archives the PDF on the committed verifikat through uploadDocument (upload_source system, extraction none), mirroring archiveIssuedInvoicePdf. Never throws: the booking is immutable by then. - book route: archive after commitEntry; response gains underlag_archived. FX-retry now also syncs the in-memory row so the underlag shows the resolved SEK facts. - webshop_order added to NEEDS_DOC_SOURCE_TYPES and (new migration 20260825140000) to the verifikat_without_documents needs-doc list, so a failed attach or a historical booking surfaces on the saknar-underlag worklist. transactions_without_documents is deliberately unchanged. - tests: underlag model/render/archive unit tests, book-route archive and failure-isolation cases, pg test extended (per-source-type probe now covers webshop_order; explicit flagged/silenced pair). Fixes #1881 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(migrations): move webshop needs-doc migration after main's 20260825150000 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(webshop): add manually_booked fields to the underlag order fixture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(webshop): skeptic findings on the orderunderlag (#1881) Two refutations from the skeptic pass on PR #1899, both fixed: 1. Correctness: sv-SE Intl emits U+2212 MINUS SIGN for negatives, which Helvetica/WinAnsi PDF fonts drop silently, so refund and discount amounts on the archived underlag rendered as POSITIVE. formatAmount now replaces U+2212 with an ASCII hyphen (same guard as formatPdfCurrency), is exported, and is pinned by a regression test. 2. Regression: NEEDS_DOC_SOURCE_TYPES had two hardcoded copies that missed webshop_order, so flagged rows rendered without the "Underlag saknas" chip, waiver toggle, or batch-exempt selection, and the weekly missing-underlag push cron disagreed with the badge. The constant now lives in dependency-free lib/worklist/types.ts (client-safe), is re-exported from categories.ts, and both JournalEntryList.tsx and push-notifications/notification-scheduler.ts consume it instead of their own copies. Also: "Bokfört i SEK" reworded to "Motsvarande i SEK" (compliance skeptic observation: the dialog's lines are user-editable, so the underlag must state the order's conversion, not claim a booking fact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c6f2bebab9
commit
5fc0be9ed7
@@ -354,6 +354,39 @@ describe('document surfaces unification', () => {
|
||||
expect((res.verifikat ?? []).map((v) => v.journal_entry_id).sort()).toEqual(expected.sort())
|
||||
})
|
||||
|
||||
it('webshop_order: flagged without underlag, silenced by the archived orderunderlag (#1881)', async () => {
|
||||
// The book route archives a generated orderunderlag on the verifikat; a
|
||||
// historical booking (or a failed attach) has no doc and must surface.
|
||||
const s = await seedCompany()
|
||||
const mkWebshopJe = (n: number) =>
|
||||
insertPostedJournalEntry({
|
||||
userId: s.userId,
|
||||
companyId: s.companyId,
|
||||
fiscalPeriodId: s.fiscalPeriodId,
|
||||
voucherNumber: n,
|
||||
entryDate: '2026-06-15',
|
||||
description: `webshop order ${n}`,
|
||||
sourceType: 'webshop_order',
|
||||
lines: [
|
||||
{ accountNumber: '1930', debitAmount: 100 * n, creditAmount: 0 },
|
||||
{ accountNumber: '3001', debitAmount: 0, creditAmount: 100 * n },
|
||||
],
|
||||
})
|
||||
const jeWithUnderlag = await mkWebshopJe(1)
|
||||
const jeWithoutUnderlag = await mkWebshopJe(2)
|
||||
await attachDocument({
|
||||
userId: s.userId,
|
||||
companyId: s.companyId,
|
||||
journalEntryId: jeWithUnderlag,
|
||||
})
|
||||
|
||||
const res = await verifikatSurface(s.companyId)
|
||||
expect(res.ok).toBe(true)
|
||||
const ids = (res.verifikat ?? []).map((v) => v.journal_entry_id)
|
||||
expect(ids).toContain(jeWithoutUnderlag)
|
||||
expect(ids).not.toContain(jeWithUnderlag)
|
||||
})
|
||||
|
||||
it('tenant guard on the transactions surface (NULL + foreign company)', async () => {
|
||||
const { rows } = await getPool().query<{ r: TransactionsResult }>(
|
||||
`SELECT public.transactions_without_documents(NULL, NULL, 20, 0) AS r`,
|
||||
|
||||
Reference in New Issue
Block a user