feat(woocommerce): store order/refund feed extension (#1442)

* feat(woocommerce): store order/refund feed extension

Connect a WooCommerce store via the wc-auth key handshake (manual key
fallback) with per-store consumer key/secret AES-256-GCM encrypted at rest,
and import paid orders and refunds into the transactions inbox as a
bank-style feed on the 1680 cash account. Feed-only: nothing auto-books,
gateway fees/payouts are out of scope (core wc/v3 does not expose them).

Sync is cursor-paginated on modified_after (offset pages only inside
same-second date_modified ties), terminates on an empty page, holds the
cursor below failed refund fetches / ingest errors / deadline-skipped work,
checks the time budget between refund fetches, and drops rows dated on or
before bookkeeping_locked_through on every run. Nightly cron gated on the
extension registry + new paid capability woocommerce_sync (backfilled to
existing bank_sync grant holders).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(migrations): move woocommerce migrations past main's 20260806090000

origin/main gained 20260806090000_recurring_schedule_interval_months while
this branch was in flight; identical version timestamps abort the Supabase
apply, so the two new migrations move to 20260806170000/20260806170100.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(woocommerce): resolve CodeRabbit review findings

- callback 503s early when WOOCOMMERCE_CREDENTIALS_ENCRYPTION_KEY is
  unset: encryptCredential would otherwise throw after the probe and
  strand the pending row without error_message
- disconnect and upstream-revoke clear the encrypted consumer key/secret:
  nothing reads them after revoke and keeping decryptable dead
  credentials is unnecessary retention
- manual sync gets a 240s time budget and the panel reports a truncated
  run as 'partial, sync again' instead of a normal completion
- listOrderRefunds terminates on an empty batch (hosts may cap per_page),
  dedupes by id against hosts that ignore page, and caps total pages
- unparseable money strings count as errors and log instead of being
  silently identical to a zero total
- pg test uses per-run unique store URLs so committed rows cannot hit
  the store_url partial unique index across pg-real runs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(woocommerce): resolve CodeRabbit cycle-2 findings

- listOrderRefunds throws when the page cap is exhausted with data still
  flowing, instead of returning a silently partial list the sync cursor
  would advance past; the error routes into the existing held-cursor
  refund-retry path
- partial sync results keep the row-error count, and the partial toast
  string surfaces it (ICU plural, hidden at zero) in both locales

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: retrigger CI after dropped push event

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-06 23:30:00 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent cd344b6dbb
commit 707d597b2e
41 changed files with 4335 additions and 9 deletions
+57
View File
@@ -409,6 +409,59 @@
"transaction_sync_toggle_failed": "Could not save the setting. Please try again.",
"sync_done_transactions": "{imported} transaction(s) imported, {linked} linked to vouchers."
},
"woocommerce": {
"title": "WooCommerce",
"description": "Connect your WooCommerce store to fetch paid orders and refunds into the transaction inbox, as a bank-style feed for the store. You book the rows from the inbox as usual.",
"not_configured": "The WooCommerce integration is not configured on this installation. Contact your administrator.",
"load_failed": "Could not read the WooCommerce status. Check your connection and try again.",
"action_timeout": "The action took too long. Reload the page to see whether it went through.",
"action_network": "No contact with the server. Check your connection and try again.",
"store_url_label": "Store address",
"connect": "Connect store",
"connecting": "Connecting…",
"connect_hint": "You are sent to your store to approve the connection with read access. The keys are stored encrypted and you can revoke them at any time, here or in WooCommerce.",
"manual_toggle": "Enter API keys manually",
"manual_hint": "Create a read-only API key in WooCommerce under Settings, Advanced, REST API and paste the keys here.",
"consumer_key_label": "Consumer key",
"consumer_secret_label": "Consumer secret",
"manual_connect": "Connect with keys",
"disconnect": "Disconnect",
"disconnect_confirm": "Yes, disconnect",
"cancel": "Cancel",
"status_active": "Connected",
"status_pending": "Pending",
"status_revoked": "Disconnected",
"status_error": "Error",
"connected_since": "Connected {date}",
"unnamed_store": "WooCommerce store",
"pending_note": "Waiting for the store to send the keys. Finish the approval in the store if you have not already.",
"connected_toast_title": "WooCommerce connected",
"connected_toast_description": "The store's paid orders and refunds are now fetched every night.",
"disconnected_toast_title": "WooCommerce disconnected",
"disconnected_toast_description": "Also remove the API key in the store's WooCommerce settings.",
"connect_failed_title": "Connection failed",
"disconnect_failed_title": "Disconnect failed",
"error_generic": "Something went wrong. Please try again.",
"error_denied": "The connection was denied in the store.",
"sync_now": "Sync now",
"syncing": "Syncing…",
"sync_done_title": "Sync complete",
"sync_done_feed": "{fetched} order(s) fetched: {imported} new rows in the inbox.",
"sync_done_empty": "The store returned no orders for the period. Check that the right store is connected if you expected orders.",
"sync_done_feed_errors": "{fetched} order(s) fetched: {imported} new rows in the inbox. {errors} row(s) could not be imported: sync again.",
"sync_partial_title": "Sync paused",
"sync_partial": "{fetched} order(s) fetched so far: {imported} new rows in the inbox.{errors, plural, =0 {} other { # row(s) could not be imported.}} Not all orders were fetched in time: sync again to continue where it stopped.",
"sync_failed_title": "Sync failed",
"sync_revoked": "The store rejected the API key, so no orders could be fetched. Connect the store again.",
"transaction_sync_title": "Orders from WooCommerce",
"transaction_sync_description": "Fetch the store's paid orders and refunds into the transaction inbox every night, as a bank-style feed for the store. You book the rows from the inbox as usual.",
"transaction_sync_backfill_note": "The first sync fetches up to 90 days of history, but never before the bookkeeping lock.",
"transaction_sync_last_synced": "Last synced {date}",
"transaction_sync_never_synced": "Not synced yet",
"transaction_sync_enabled_toast": "Order sync enabled. History is fetched on the next sync.",
"transaction_sync_disabled_toast": "Order sync disabled.",
"transaction_sync_toggle_failed": "Could not save the setting. Please try again."
},
"settings_modal": {
"title": "Settings",
"description": "Manage your company and account"
@@ -6356,6 +6409,10 @@
"stripe_description": "Connect your company's Stripe account to fetch payments, fees, and payouts continuously and book them against the Stripe balance.",
"stripe_not_enabled_title": "The Stripe extension is not enabled",
"stripe_not_enabled_description": "Enable the Stripe extension to connect your Stripe account and sync transactions automatically.",
"woocommerce_title": "WooCommerce",
"woocommerce_description": "Connect your WooCommerce store to fetch paid orders and refunds into the transaction inbox.",
"woocommerce_not_enabled_title": "The WooCommerce extension is not enabled",
"woocommerce_not_enabled_description": "Enable the WooCommerce extension to connect your store and fetch orders automatically.",
"migration_title": "Import from another system",
"migration_description": "Nothing changes in your existing system.",
"bankfile_title": "Bank file",
+57
View File
@@ -409,6 +409,59 @@
"transaction_sync_toggle_failed": "Kunde inte spara inställningen. Försök igen.",
"sync_done_transactions": "{imported} transaktion(er) importerade, {linked} länkade till verifikat."
},
"woocommerce": {
"title": "WooCommerce",
"description": "Koppla din WooCommerce-butik så hämtas betalda ordrar och återbetalningar till transaktionsinkorgen, som ett bankflöde för butiken. Du bokför raderna som vanligt från inkorgen.",
"not_configured": "WooCommerce-integrationen är inte konfigurerad på den här installationen. Kontakta administratören.",
"load_failed": "Kunde inte läsa WooCommerce-statusen. Kontrollera din uppkoppling och försök igen.",
"action_timeout": "Åtgärden tog för lång tid. Ladda om sidan för att se om den gick igenom.",
"action_network": "Ingen kontakt med servern. Kontrollera din uppkoppling och försök igen.",
"store_url_label": "Butikens adress",
"connect": "Anslut butik",
"connecting": "Ansluter…",
"connect_hint": "Du skickas till din butik för att godkänna kopplingen med läsbehörighet. Nycklarna lagras krypterade och du kan när som helst återkalla dem här eller i WooCommerce.",
"manual_toggle": "Ange API-nycklar manuellt",
"manual_hint": "Skapa en API-nyckel med läsbehörighet i WooCommerce under Inställningar, Avancerat, REST-API och klistra in nycklarna här.",
"consumer_key_label": "Konsumentnyckel (consumer key)",
"consumer_secret_label": "Konsumenthemlighet (consumer secret)",
"manual_connect": "Anslut med nycklar",
"disconnect": "Koppla från",
"disconnect_confirm": "Ja, koppla från",
"cancel": "Avbryt",
"status_active": "Ansluten",
"status_pending": "Väntar",
"status_revoked": "Frånkopplad",
"status_error": "Fel",
"connected_since": "Ansluten {date}",
"unnamed_store": "WooCommerce-butik",
"pending_note": "Väntar på att butiken ska skicka nycklarna. Slutför godkännandet i butiken om du inte redan gjort det.",
"connected_toast_title": "WooCommerce anslutet",
"connected_toast_description": "Butikens betalda ordrar och återbetalningar hämtas nu varje natt.",
"disconnected_toast_title": "WooCommerce frånkopplat",
"disconnected_toast_description": "Ta även bort API-nyckeln i butikens WooCommerce-inställningar.",
"connect_failed_title": "Anslutningen misslyckades",
"disconnect_failed_title": "Frånkopplingen misslyckades",
"error_generic": "Något gick fel. Försök igen.",
"error_denied": "Anslutningen nekades i butiken.",
"sync_now": "Synka nu",
"syncing": "Synkar…",
"sync_done_title": "Synkronisering klar",
"sync_done_feed": "{fetched} order/ordrar hämtade: {imported} nya rader i inkorgen.",
"sync_done_empty": "Butiken returnerade inga ordrar för perioden. Kontrollera att rätt butik är ansluten om du väntade dig ordrar.",
"sync_done_feed_errors": "{fetched} order/ordrar hämtade: {imported} nya rader i inkorgen. {errors} rad(er) kunde inte importeras: synka igen.",
"sync_partial_title": "Synkroniseringen pausades",
"sync_partial": "{fetched} order/ordrar hämtade hittills: {imported} nya rader i inkorgen.{errors, plural, =0 {} other { # rad(er) kunde inte importeras.}} Alla ordrar hann inte hämtas: synka igen för att fortsätta där det stannade.",
"sync_failed_title": "Synkroniseringen misslyckades",
"sync_revoked": "Butiken avvisade API-nyckeln, så inga ordrar kunde hämtas. Anslut butiken igen.",
"transaction_sync_title": "Ordrar från WooCommerce",
"transaction_sync_description": "Hämta butikens betalda ordrar och återbetalningar till transaktionsinkorgen varje natt, som ett bankflöde för butiken. Du bokför raderna som vanligt från inkorgen.",
"transaction_sync_backfill_note": "Vid första synkningen hämtas upp till 90 dagars historik, dock inte före bokföringslåset.",
"transaction_sync_last_synced": "Senast synkad {date}",
"transaction_sync_never_synced": "Inte synkad ännu",
"transaction_sync_enabled_toast": "Ordersynk aktiverad. Historiken hämtas vid nästa synkning.",
"transaction_sync_disabled_toast": "Ordersynk avaktiverad.",
"transaction_sync_toggle_failed": "Kunde inte spara inställningen. Försök igen."
},
"settings_modal": {
"title": "Inställningar",
"description": "Hantera ditt företag och konto"
@@ -6356,6 +6409,10 @@
"stripe_description": "Koppla företagets Stripe-konto så hämtas betalningar, avgifter och utbetalningar löpande och bokförs mot Stripe-saldot.",
"stripe_not_enabled_title": "Stripe-tillägget är inte aktiverat",
"stripe_not_enabled_description": "Aktivera tillägget Stripe för att koppla ditt Stripe-konto och synka transaktioner automatiskt.",
"woocommerce_title": "WooCommerce",
"woocommerce_description": "Koppla din WooCommerce-butik så hämtas betalda ordrar och återbetalningar till transaktionsinkorgen.",
"woocommerce_not_enabled_title": "WooCommerce-tillägget är inte aktiverat",
"woocommerce_not_enabled_description": "Aktivera tillägget WooCommerce för att koppla din butik och hämta ordrar automatiskt.",
"migration_title": "Hämta från annat system",
"migration_description": "Inget ändras i ditt befintliga system.",
"bankfile_title": "Bankfil",