feat(webshop-orders): Orders page with per-store, per-payment-method booking (#1525)
* feat(webshop-orders): schema, types and error codes for the orders surface webshop_orders (order/refund rows, financial-freeze trigger, member select/update RLS, no DELETE) + webshop_store_settings (per-store payment method -> account map), source_type 'webshop_order', multi-store index drop, customer_country, and a one-time woo cursor reset so the switch-over backfills and cross-marks existing feed rows. Tables classified in the full-archive export; pg-real coverage for RLS, freeze and CHECK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(webshop-orders): core service (ingest, booking lines) upsertWebshopOrders: two-phase order/refund upsert with FX enrichment, legacy-feed cross-marking, frozen-row protection and field-wise jsonb comparisons (Postgres does not preserve object key order). Booking-line builder: per-rate VAT split with SIGNED buckets (discounts book as revenue reductions), refund mirroring, 3740 residual, per-store account prefill, and advisory export/EU + OSS warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(webshop-orders): API routes for list, booking, invoicing and mapping Booking is draft -> atomic claim -> commit (conditional link-back closes the concurrent double-book race; a lost claim cancels the voucher-free draft). Legacy-feed guard honors transactions.is_ignored on both the book and create-invoice paths. Invoice conversion reuses buildInvoiceWriteData for an unnumbered draft with dominant-rate fallback and drift-safe unit prices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(webshop-orders): Orders page, booking/invoice dialogs and gated nav /orders lists per-store orders with status tabs (server-side filters), exception chips and one action per row. Booking dialog prefills from the per-store payment-method mapping with an opt-in remember; invoice dialog converts to a draft kundfaktura. The Order nav item renders only for companies with an active WooCommerce connection or existing order rows (Shopify deliberately excluded until its sync writes webshop_orders). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(woocommerce): switch the order sync to webshop_orders, multi-store The sync maps rich wc/v3 payloads (billing, line/shipping/fee taxes, refund allocations with parent-prorated VAT fallback) and upserts order rows instead of transactions-inbox rows; already-imported feed rows stay bookable and get cross-marked. Multi-store: several active connections per company, per-store panel cards with the account-mapping editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(webshop-orders): decision log entries and ratchet baseline Baseline moves DOWN only: naive-ore-round 638 -> 637 via roundOre adoption; hand-rolled invariants stay at 115 (ACCOUNT_NUMBER_RE imported, not inlined). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(webshop-orders): resolve PR #1525 review findings and CI failures Review batch (Superagent, CodeRabbit, Swedish compliance review): - Mutual-exclusion claims: booking guards invoice_id, invoice link-back guards journal_entry_id AND treats zero matched rows as the conflict it is (409 + rollback), closing both TOCTOU races. - Freeze v2 migration (20260812124858): the link columns themselves are protected: invoice links immutable, journal links clearable only while the entry is still a draft (the booking rollback path). - Scraped orgnr no longer auto-written to customers.org_number; rate fallback applies only on single-VAT-bucket orders; refunds get their own WEBSHOP_ORDER_REFUND_NOT_CONVERTIBLE code; VAT advisories outrank the invoice-mode hint in the booking dialog. - Ingest compares every synced field (billing corrections no longer drop as unchanged); sync guards absent refunds arrays; /sync aggregates per-store results; panel disables all cards while a request runs; orders page separates load failure from empty; account field explains itself. CI: regenerated skills/accounted-api; pg tests restructured for transaction-abort/rollback semantics + freeze-link coverage; unresolvable- expression ceiling 375 -> 378 with documented reason (partial-update payloads in ingest, shapes covered by unit tests). Declined: CodeRabbit docstring-coverage advisory (house style: comments only where the code cannot say it). 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
89cfa82f2f
commit
c35b2547fb
@@ -3343,6 +3343,80 @@ const NETWORK_TRANSIENT_ENTRY: StructuredErrorEntry = {
|
||||
retryable: true,
|
||||
}
|
||||
|
||||
const WEBSHOP_ORDERS: Record<string, StructuredErrorEntry> = {
|
||||
WEBSHOP_ORDER_NOT_FOUND: {
|
||||
httpStatus: 404,
|
||||
message_sv: 'Ordern hittades inte.',
|
||||
message_en: 'The order was not found.',
|
||||
},
|
||||
WEBSHOP_ORDER_ALREADY_BOOKED: {
|
||||
httpStatus: 409,
|
||||
message_sv: 'Ordern är redan bokförd.',
|
||||
message_en: 'The order is already booked.',
|
||||
},
|
||||
WEBSHOP_ORDER_ALREADY_INVOICED: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Ordern är kopplad till en kundfaktura. Bokföringen sker via fakturaflödet, inte direkt från ordern.',
|
||||
message_en:
|
||||
'The order is linked to a customer invoice. Bookkeeping happens through the invoice flow, not directly from the order.',
|
||||
},
|
||||
WEBSHOP_ORDER_NOT_PAID: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Ordern är inte betald ännu. Obetalda ordrar bokförs när betalningen kommer, eller faktureras via Skapa faktura.',
|
||||
message_en:
|
||||
'The order is not paid yet. Unpaid orders are booked when payment arrives, or invoiced via Create invoice.',
|
||||
},
|
||||
WEBSHOP_ORDER_LEGACY_TRANSACTION_OPEN: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Samma order ligger redan som en obokförd transaktion under Transaktioner (importerad av det tidigare orderflödet). Bokför eller ignorera den transaktionen först, så att samma affärshändelse inte bokförs två gånger.',
|
||||
message_en:
|
||||
'The same order already exists as an unbooked transaction under Transactions (imported by the previous order feed). Book or ignore that transaction first so the same business event is not booked twice.',
|
||||
},
|
||||
WEBSHOP_ORDER_LEGACY_TRANSACTION_BOOKED: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Ordern är redan bokförd via en transaktion under Transaktioner (importerad av det tidigare orderflödet).',
|
||||
message_en:
|
||||
'The order is already booked via a transaction under Transactions (imported by the previous order feed).',
|
||||
},
|
||||
WEBSHOP_ORDER_FX_UNRESOLVED: {
|
||||
httpStatus: 422,
|
||||
message_sv:
|
||||
'Växelkursen för orderns valuta kunde inte hämtas ännu. Försök igen om en stund; ordern kan inte bokföras i SEK utan kurs.',
|
||||
message_en:
|
||||
'The exchange rate for the order currency could not be fetched yet. Try again shortly; the order cannot be booked in SEK without a rate.',
|
||||
},
|
||||
WEBSHOP_ORDER_REFUND_NOT_CONVERTIBLE: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Återbetalningar kan inte omvandlas till fakturor. Hantera återbetalningen med en kreditfaktura från kundfakturan, eller bokför återbetalningsraden direkt.',
|
||||
message_en:
|
||||
'Refunds cannot be converted to invoices. Handle the refund with a credit note from the customer invoice, or book the refund row directly.',
|
||||
},
|
||||
WEBSHOP_ORDER_REFUND_PARENT_INVOICED: {
|
||||
httpStatus: 409,
|
||||
message_sv:
|
||||
'Ordern fakturerades via en kundfaktura. Återbetalningen hanteras med en kreditfaktura, inte genom att bokföra återbetalningsraden direkt.',
|
||||
message_en:
|
||||
'The order was invoiced through a customer invoice. Handle the refund with a credit note instead of booking the refund row directly.',
|
||||
},
|
||||
WEBSHOP_ORDER_CREATE_INVOICE_CUSTOMER_FAILED: {
|
||||
httpStatus: 500,
|
||||
message_sv: 'Kunden kunde inte skapas från orderns uppgifter.',
|
||||
message_en: 'The customer could not be created from the order data.',
|
||||
},
|
||||
WEBSHOP_ORDER_CREATE_INVOICE_MISSING_CUSTOMER: {
|
||||
httpStatus: 422,
|
||||
message_sv:
|
||||
'Ordern saknar kunduppgifter. Välj en befintlig kund att fakturera.',
|
||||
message_en:
|
||||
'The order has no customer data. Choose an existing customer to invoice.',
|
||||
},
|
||||
}
|
||||
|
||||
const NODE_SYSTEM: Record<string, StructuredErrorEntry> = {
|
||||
ECONNREFUSED: NETWORK_TRANSIENT_ENTRY,
|
||||
ECONNRESET: NETWORK_TRANSIENT_ENTRY,
|
||||
@@ -3396,6 +3470,7 @@ const REGISTRY: Record<string, StructuredErrorEntry> = {
|
||||
...BOLAGSVERKET,
|
||||
...ASSETS,
|
||||
...DIMENSION,
|
||||
...WEBSHOP_ORDERS,
|
||||
...NODE_SYSTEM,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user