feat(zettle): sync paid purchases into webshop_orders (#2445)

Community PR #2416 by @olofpinzke, adopted and finished by maintainers (rebased so every commit is signed).

Why the problem occurred: no Zettle integration; POS sales only reached the books as bank descriptors while Woo/Shopify already had order underlag via webshop_orders. The contributor's version also failed at the database (platform CHECKs listed only woocommerce/shopify), which the mocked unit tests never saw.
What was simplified: reused the Orders/book/invoice path instead of a new inbox; Finance API payouts/fees deferred. Sales the one-account, revenue-per-rate model cannot book (split tender, gift cards, tips) import unbookable with a "bokför manuellt" title instead of guessing accounts. Reset parity uses the rename-and-wrap pattern instead of re-issuing the reset body.
Why this solution: per-purchase rows give the radunderlag BFL verifikat need and the bulk-book path exists; daily kassarapport aggregation and Finance API fees/payouts are the follow-up (DECISIONS.md). Skeptic-refuted paths fixed before merge: concurrent refresh-token rotation (sync claim), cron offset paging (candidate snapshot), platform CHECKs, writer-role gate, migration-reset parity, white-label return origin re-validated at callback, VAT net from product rows.

Not live until ZETTLE_CLIENT_ID / ZETTLE_CLIENT_SECRET / ZETTLE_CREDENTIALS_ENCRYPTION_KEY are set on Vercel and a Zettle developer app is registered with the callback redirect URI.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtYqzKPoTSRHskYYdf7MwB
This commit is contained in:
Mattsson
2026-09-09 11:19:39 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent 500806f001
commit 6ea92f3152
58 changed files with 4069 additions and 18 deletions
+48
View File
@@ -536,6 +536,50 @@
"transaction_sync_disabled_toast": "Order sync disabled.",
"transaction_sync_toggle_failed": "Could not save the setting. Please try again."
},
"zettle": {
"title": "Zettle",
"description": "Connect your Zettle account to fetch paid purchases and refunds to the Orders page every night, with amounts, payment method, per-rate VAT and line underlag. You book them from there.",
"not_configured": "The Zettle integration is not configured on this installation. Contact an administrator.",
"load_failed": "Could not read Zettle status. Check your connection and try again.",
"action_timeout": "The action took too long. Reload the page to see if it went through.",
"action_network": "No contact with the server. Check your connection and try again.",
"connect": "Connect Zettle",
"connecting": "Connecting…",
"connect_hint": "You will be sent to Zettle to approve read access to purchases. You can disconnect here at any time.",
"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": "Zettle account",
"connected_toast_title": "Zettle connected",
"connected_toast_description": "Paid purchases and refunds are now fetched every night.",
"disconnected_toast_title": "Zettle disconnected",
"disconnected_toast_description": "The Zettle connection has been removed.",
"connect_failed_title": "Connection failed",
"disconnect_failed_title": "Disconnect failed",
"sync_now": "Sync now",
"syncing": "Syncing…",
"sync_done_title": "Sync complete",
"sync_done_feed": "{fetched} purchase(s) fetched: {imported} new on the Orders page.{needsReview, plural, =0 {} other { # row(s) need manual booking (split payment, gift card or tip).}}",
"sync_done_empty": "Zettle returned no purchases for the period. Check that the right account is connected if you expected purchases.",
"sync_done_feed_errors": "{fetched} purchase(s) fetched: {imported} new on the Orders page.{needsReview, plural, =0 {} other { # row(s) need manual booking.}} {errors} row(s) could not be imported: sync again.",
"sync_partial_title": "Sync paused",
"sync_partial": "{fetched} purchase(s) fetched so far: {imported} new on the Orders page.{needsReview, plural, =0 {} other { # row(s) need manual booking.}}{errors, plural, =0 {} other { # row(s) could not be imported.}} Not all purchases were fetched: sync again to continue where it stopped.",
"sync_failed_title": "Sync failed",
"sync_revoked": "Zettle rejected the connection, so no purchases could be fetched. Connect the account again.",
"transaction_sync_title": "Purchases from Zettle",
"transaction_sync_description": "Fetch paid purchases and refunds to the Orders page every night. You book them from there.",
"transaction_sync_backfill_note": "The first sync fetches up to 90 days of history.",
"transaction_sync_last_synced": "Last synced {date}",
"transaction_sync_never_synced": "Not synced yet",
"transaction_sync_enabled_toast": "Purchase sync enabled. History is fetched on the next sync.",
"transaction_sync_disabled_toast": "Purchase sync disabled.",
"transaction_sync_toggle_failed": "Could not save the setting. Try again."
},
"settings_modal": {
"title": "Settings",
"description": "Manage your company and account"
@@ -7806,6 +7850,10 @@
"shopify_description": "Connect your Shopify store to fetch paid orders and refunds to the Orders page.",
"shopify_not_enabled_title": "The Shopify extension is not enabled",
"shopify_not_enabled_description": "Enable the Shopify extension to connect your store and fetch orders automatically.",
"zettle_title": "Zettle",
"zettle_description": "Connect your Zettle account to fetch paid purchases and refunds to the Orders page.",
"zettle_not_enabled_title": "The Zettle extension is not enabled",
"zettle_not_enabled_description": "Enable the Zettle extension to connect your account and fetch purchases automatically.",
"migration_title": "Import from another system",
"migration_description": "Nothing changes in your existing system.",
"bankfile_title": "Bank file",
+48
View File
@@ -536,6 +536,50 @@
"transaction_sync_disabled_toast": "Ordersynk avaktiverad.",
"transaction_sync_toggle_failed": "Kunde inte spara inställningen. Försök igen."
},
"zettle": {
"title": "Zettle",
"description": "Koppla ditt Zettle-konto så hämtas betalda köp och återbetalningar till Ordersidan varje natt, med belopp, betalsätt, moms per sats och radunderlag. Du bokför köpen därifrån.",
"not_configured": "Zettle-integrationen är inte konfigurerad på den här installationen. Kontakta administratören.",
"load_failed": "Kunde inte läsa Zettle-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.",
"connect": "Anslut Zettle",
"connecting": "Ansluter…",
"connect_hint": "Du skickas till Zettle för att godkänna läsbehörighet till köp. Anslutningen kan när som helst kopplas från här.",
"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": "Zettle-konto",
"connected_toast_title": "Zettle anslutet",
"connected_toast_description": "Betalda köp och återbetalningar hämtas nu varje natt.",
"disconnected_toast_title": "Zettle frånkopplat",
"disconnected_toast_description": "Anslutningen till Zettle är borttagen.",
"connect_failed_title": "Anslutningen misslyckades",
"disconnect_failed_title": "Frånkopplingen misslyckades",
"sync_now": "Synka nu",
"syncing": "Synkar…",
"sync_done_title": "Synkronisering klar",
"sync_done_feed": "{fetched} köp hämtade: {imported} nya på Ordersidan.{needsReview, plural, =0 {} other { # rad(er) kräver manuell bokföring (delad betalning, presentkort eller dricks).}}",
"sync_done_empty": "Zettle returnerade inga köp för perioden. Kontrollera att rätt konto är anslutet om du väntade dig köp.",
"sync_done_feed_errors": "{fetched} köp hämtade: {imported} nya på Ordersidan.{needsReview, plural, =0 {} other { # rad(er) kräver manuell bokföring.}} {errors} rad(er) kunde inte importeras: synka igen.",
"sync_partial_title": "Synkroniseringen pausades",
"sync_partial": "{fetched} köp hämtade hittills: {imported} nya på Ordersidan.{needsReview, plural, =0 {} other { # rad(er) kräver manuell bokföring.}}{errors, plural, =0 {} other { # rad(er) kunde inte importeras.}} Alla köp hann inte hämtas: synka igen för att fortsätta där det stannade.",
"sync_failed_title": "Synkroniseringen misslyckades",
"sync_revoked": "Zettle avvisade anslutningen, så inga köp kunde hämtas. Anslut kontot igen.",
"transaction_sync_title": "Köp från Zettle",
"transaction_sync_description": "Hämta betalda köp och återbetalningar till Ordersidan varje natt. Du bokför köpen därifrån.",
"transaction_sync_backfill_note": "Vid första synkningen hämtas upp till 90 dagars historik.",
"transaction_sync_last_synced": "Senast synkad {date}",
"transaction_sync_never_synced": "Inte synkad ännu",
"transaction_sync_enabled_toast": "Köpsynk aktiverad. Historiken hämtas vid nästa synkning.",
"transaction_sync_disabled_toast": "Köpsynk 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"
@@ -7806,6 +7850,10 @@
"shopify_description": "Koppla din Shopify-butik så hämtas betalda ordrar och återbetalningar till Ordersidan.",
"shopify_not_enabled_title": "Shopify-tillägget är inte aktiverat",
"shopify_not_enabled_description": "Aktivera tillägget Shopify för att koppla din butik och hämta ordrar automatiskt.",
"zettle_title": "Zettle",
"zettle_description": "Koppla ditt Zettle-konto så hämtas betalda köp och återbetalningar till Ordersidan.",
"zettle_not_enabled_title": "Zettle-tillägget är inte aktiverat",
"zettle_not_enabled_description": "Aktivera tillägget Zettle för att koppla ditt konto och hämta köp automatiskt.",
"migration_title": "Hämta från annat system",
"migration_description": "Inget ändras i ditt befintliga system.",
"bankfile_title": "Bankfil",