feat(woo): mark an order as already booked outside the integration (#1895)
* feat(woo): mark an order as already booked outside the integration Orders booked by hand before the store was connected sat under Att bokfora forever: the only exits were the book and create-invoice routes. - Migration: manually_booked_at/_by + optional manually_booked_journal_entry_id on webshop_orders (informational link, no financial freeze; the mark produced no accounting objects). - POST/DELETE /api/webshop-orders/[id]/mark-booked: mark with optional posted-verifikat reference (validated per company), conditional claim against concurrent booking/invoicing; unmark is a plain revert. - book and create-invoice routes refuse marked rows (409 WEBSHOP_ORDER_MANUALLY_BOOKED) and exclude them in their atomic claims. - List route: booked/unbooked filters treat a manual mark as a closed exit, so marked rows leave the Att bokfora tab and join Bokforda. - Orders page: row overflow menu with Markera som bokford / Angra markering, MarkOrderBookedDialog with a searchable candidate list of posted entries near the order date, muted status text linking to the referenced verifikat. Fixes #1879 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): close skeptic findings on the manual-booked mark - mark-booked applies the same open-twin gate as book/create-invoice: an OPEN legacy feed transaction blocks the mark (409 WEBSHOP_ORDER_LEGACY_TRANSACTION_OPEN); ignored or booked feed rows unlock it, so no open path to a duplicate remains. - ingest treats manually marked rows as frozen for drift purposes: remote financial deltas set remote_changed_after_freeze (same badge as booked rows) instead of silently refreshing the row under the user's assertion. - re-marking with a journal_entry_id updates the informational link instead of silently dropping it. - dialog: candidate amount computed from the returned lines (the list API does not return total_amount), newest-first ordering, cap hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(migrations): bump webshop manual-booking migration past freshly merged 20260825120000 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): resolve PR review findings in one pass - freeze v3 migration: financial fields are frozen at the DB level while a row is manually marked as booked (review finding: the mark's freeze lived only in ingest.ts, so any other write path could silently mutate a marked row); unmark stays the escape hatch. pg test added. - pass the active locale to getErrorMessage in the orders page and MarkOrderBookedDialog (CodeRabbit: English users got Swedish errors). 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
cd46d936f3
commit
1f9578ca76
@@ -0,0 +1,34 @@
|
||||
-- Manual booking mark for webshop orders (issue #1879).
|
||||
--
|
||||
-- Orders booked by hand BEFORE the integration was connected sit in the
|
||||
-- "Att bokfora" list forever: the only exits are the book and create-invoice
|
||||
-- routes. These columns add a third, non-accounting exit: the user marks the
|
||||
-- row as already handled outside the integration, optionally pointing at the
|
||||
-- existing verifikat.
|
||||
--
|
||||
-- Deliberately separate from journal_entry_id: that column is the claim the
|
||||
-- booking route takes atomically and the freeze trigger treats it as "this
|
||||
-- row PRODUCED that entry" (financial fields freeze, link becomes immutable
|
||||
-- once posted). A manual mark produced nothing; it is a user assertion with
|
||||
-- an optional soft reference, so it stays reversible (unmark) and does not
|
||||
-- freeze the row. The book/create-invoice routes refuse marked rows
|
||||
-- application-side, mirroring the legacy_transaction_id double-booking lock.
|
||||
--
|
||||
-- No RLS change: the existing member UPDATE policy already covers the mark/
|
||||
-- unmark writes. No audit trigger (consistent with the table: accounting-
|
||||
-- relevant events are audited on journal_entries/invoices; the mark keeps
|
||||
-- who/when on the row itself via manually_booked_by/_at).
|
||||
|
||||
alter table public.webshop_orders
|
||||
add column manually_booked_at timestamptz,
|
||||
add column manually_booked_by uuid references auth.users(id) on delete set null,
|
||||
add column manually_booked_journal_entry_id uuid references public.journal_entries(id) on delete set null;
|
||||
|
||||
comment on column public.webshop_orders.manually_booked_at is
|
||||
'When the user marked this row as already booked/handled outside the integration; null = not marked. Marked rows leave the to-book list and the book/create-invoice routes refuse them.';
|
||||
comment on column public.webshop_orders.manually_booked_by is
|
||||
'User who marked the row as manually booked.';
|
||||
comment on column public.webshop_orders.manually_booked_journal_entry_id is
|
||||
'Optional user-chosen reference to the existing verifikat that covers this order. Informational link only: the entry was created outside the order flow, so this never freezes the row.';
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Freeze v3: financial fields are also frozen while a row is MANUALLY marked
|
||||
-- as booked outside the integration (issue #1879, review finding on PR #1895).
|
||||
--
|
||||
-- v2 (20260812124858) froze financials once journal_entry_id/invoice_id was
|
||||
-- set. The manual mark (manually_booked_at) got the same protection only in
|
||||
-- application code (lib/webshop-orders/ingest.ts isFrozen): any other write
|
||||
-- path (browser-client PATCH through the member UPDATE policy, a future
|
||||
-- endpoint, an ad-hoc script) could still silently mutate total/line_items
|
||||
-- under the user's "this row is covered by verifikat X" assertion. Now the
|
||||
-- trigger holds the same line: while marked, the financial fields are
|
||||
-- immutable at the DB level; sync's safe-field updates (status, refund
|
||||
-- summary, labels, remote_changed_after_freeze) still pass, and clearing the
|
||||
-- mark itself stays allowed (the manual columns are not in the protected
|
||||
-- list), which is exactly the unmark route's escape hatch: unmark first,
|
||||
-- then the row is fully mutable again.
|
||||
--
|
||||
-- The link-column protections from v2 are unchanged. CREATE OR REPLACE keeps
|
||||
-- the trigger binding intact.
|
||||
|
||||
create or replace function public.enforce_webshop_order_financial_freeze()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
v_entry_status text;
|
||||
begin
|
||||
-- Link-column protection runs FIRST: it applies even when the row was
|
||||
-- frozen by the other link.
|
||||
if old.invoice_id is not null
|
||||
and new.invoice_id is distinct from old.invoice_id
|
||||
then
|
||||
raise exception 'webshop_orders row % is linked to an invoice; the link is immutable', old.id
|
||||
using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
if old.journal_entry_id is not null
|
||||
and new.journal_entry_id is distinct from old.journal_entry_id
|
||||
then
|
||||
select status into v_entry_status
|
||||
from public.journal_entries
|
||||
where id = old.journal_entry_id;
|
||||
if v_entry_status is null or v_entry_status = 'posted' then
|
||||
raise exception 'webshop_orders row % is booked; the journal link is immutable (use storno)', old.id
|
||||
using errcode = 'P0001';
|
||||
end if;
|
||||
end if;
|
||||
|
||||
if old.journal_entry_id is not null
|
||||
or old.invoice_id is not null
|
||||
or old.manually_booked_at is not null
|
||||
then
|
||||
if new.total is distinct from old.total
|
||||
or new.total_tax is distinct from old.total_tax
|
||||
or new.total_sek is distinct from old.total_sek
|
||||
or new.exchange_rate is distinct from old.exchange_rate
|
||||
or new.currency is distinct from old.currency
|
||||
or new.vat_breakdown is distinct from old.vat_breakdown
|
||||
or new.line_items is distinct from old.line_items
|
||||
or new.order_date is distinct from old.order_date
|
||||
or new.paid_date is distinct from old.paid_date
|
||||
or new.is_paid is distinct from old.is_paid
|
||||
or new.payment_method is distinct from old.payment_method
|
||||
or new.external_id is distinct from old.external_id
|
||||
or new.platform_order_id is distinct from old.platform_order_id
|
||||
then
|
||||
raise exception 'webshop_orders row % is booked/invoiced/marked as booked; financial fields are frozen (unmark or use storno)', old.id
|
||||
using errcode = 'P0001';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
Reference in New Issue
Block a user