feat(peppol): gate Peppol per company: request access, operator enables with a sending cap (#1794)

* feat(peppol): gate Peppol per company: request access, operator enables with a sending cap

Peppol is no longer available to every company by default. Each transmission
is billed per document by the access point and each receiving identifier
consumes a contracted tenant slot, so the product now works like this:

- peppol_access (new table, RLS read-only for members, service-role writes):
  status requested | enabled | disabled, max_sends (null = no cap),
  receive_enabled as a separate grant, who asked and who enabled.
- POST /api/settings/peppol/access: the company asks from Settings >
  Fakturering; the row is written and the operators are e-mailed (best effort,
  the row is the source of truth).
- scripts/peppol/access.ts list | enable <company|orgnr> [--max-sends N]
  [--receive] | disable | show: the operator side.
- POST /api/invoices/[id]/peppol/send refuses PEPPOL_ACCESS_REQUIRED /
  PEPPOL_SEND_LIMIT_REACHED before touching the invoice; the invoice page's
  send item says so instead of pretending. Registration for receiving refuses
  PEPPOL_ACCESS_REQUIRED / PEPPOL_RECEIVING_NOT_ENABLED.
- Settings UI: access status row with "Begär åtkomst", sends used of cap,
  receiving switch only once receiving is granted.

Refs #546

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

* test(peppol): pass route params to the settings handlers; baseline-align the access row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

* fix(peppol): revoke default table privileges from authenticated on the access and receiving tables

Supabase grants ALL on new tables to authenticated by default; the earlier
REVOKE covered PUBLIC and anon only, so a member's UPDATE on peppol_access was
an RLS-filtered no-op instead of a permission error (pg-real caught it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-21 17:27:45 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 9ef7de861f
commit 3ac80edc96
20 changed files with 1075 additions and 34 deletions
+16 -1
View File
@@ -2227,7 +2227,20 @@
"toast_registered_description": "The company's Peppol id is published. It can take a moment before it shows in the Peppol directory.",
"toast_deregistered_title": "Deregistered from Peppol",
"toast_deregistered_description": "The company's Peppol id has been removed at the access point.",
"toast_failed_title": "Could not change the Peppol registration"
"toast_failed_title": "Could not change the Peppol registration",
"access_label": "Access",
"access_none": "Not enabled. Peppol is enabled per company by us; request access and we get back to you.",
"access_requested": "Request sent. We will enable it and get back to you.",
"access_enabled": "Enabled for sending e-invoices.",
"access_disabled": "Switched off. Contact support if you want it back.",
"request_button": "Request Peppol access",
"request_sending": "Sending…",
"request_sent_title": "Request sent",
"request_sent_description": "We will enable Peppol for the company and get back to you.",
"request_failed_title": "Could not send the request",
"sends_used": "{used} of {max} sends used",
"sends_unlimited": "{used} sends, no limit",
"receive_not_enabled": "Receiving is enabled by us on request (limited number of slots)."
},
"settings_pdf_print": {
"coming_soon": "Coming soon",
@@ -4072,6 +4085,8 @@
"peppol_status_business_rejected": "Rejected by the recipient",
"peppol_status_no_route": "Recipient has no Peppol registration",
"peppol_status_failed": "Failed",
"peppol_access_required": "Request Peppol access under Settings > Invoicing and we will enable it for the company.",
"peppol_send_limit_reached": "The company has used its Peppol sends. Contact support for more.",
"pdf_rerender_downloaded_title": "Freshly generated PDF downloaded",
"pdf_rerender_preview_title": "Showing a freshly generated PDF",
"pdf_preview_blocked_title": "Could not open the preview",
+16 -1
View File
@@ -2227,7 +2227,20 @@
"toast_registered_description": "Bolagets Peppol-id är publicerat. Det kan ta en stund innan det syns i Peppol-katalogen.",
"toast_deregistered_title": "Avregistrerad från Peppol",
"toast_deregistered_description": "Bolagets Peppol-id är borttaget hos operatören.",
"toast_failed_title": "Kunde inte ändra Peppol-registreringen"
"toast_failed_title": "Kunde inte ändra Peppol-registreringen",
"access_label": "Åtkomst",
"access_none": "Inte aktiverat. Peppol aktiveras per bolag av oss, begär åtkomst så hör vi av oss.",
"access_requested": "Begäran skickad. Vi aktiverar och hör av oss.",
"access_enabled": "Aktiverat för att skicka e-fakturor.",
"access_disabled": "Avstängt. Hör av dig till support om du vill ha det igen.",
"request_button": "Begär åtkomst till Peppol",
"request_sending": "Skickar…",
"request_sent_title": "Begäran skickad",
"request_sent_description": "Vi aktiverar Peppol för bolaget och hör av oss.",
"request_failed_title": "Kunde inte skicka begäran",
"sends_used": "{used} av {max} sändningar använda",
"sends_unlimited": "{used} sändningar, ingen gräns",
"receive_not_enabled": "Mottagning aktiveras av oss på begäran (begränsat antal platser)."
},
"settings_pdf_print": {
"coming_soon": "Kommer snart",
@@ -4072,6 +4085,8 @@
"peppol_status_business_rejected": "Avvisad av mottagaren",
"peppol_status_no_route": "Mottagaren saknar Peppol-registrering",
"peppol_status_failed": "Misslyckades",
"peppol_access_required": "Begär åtkomst till Peppol under Inställningar > Fakturering, så aktiverar vi det för bolaget.",
"peppol_send_limit_reached": "Bolagets Peppol-sändningar är slut. Hör av dig till support för fler.",
"pdf_rerender_downloaded_title": "Nyskapad PDF nedladdad",
"pdf_rerender_preview_title": "Nyskapad PDF visas",
"pdf_preview_blocked_title": "Kunde inte öppna förhandsgranskningen",