The settings group showed the receiving switch (disabled) and a status row
to every company, which read as "anyone can receive". Now the switch and its
status exist only once the operators granted receiving (or a registration
already exists that the company must be able to see and withdraw), and the
access request carries a "we also want to receive" checkbox that lands in the
request note and the support mail (with --receive in the enable command).
The access line says whether receiving is included.
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>
* 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>
* feat(peppol): receive e-invoices via Qvalia: registration, inbound archive, inbox delivery
Second Peppol slice (#546). Qvalia confirmed that sending needs no
per-company account, so receiving keeps the consolidated partner account:
each company publishes its 0007:orgnr on our account and inbound documents
are routed by the AccountingCustomerParty endpoint.
- PeppolTransport grows optional receiving methods (registerRecipient,
unregisterRecipient, listInboundDocuments, fetchInboundDocumentXml); the
Qvalia adapter implements them (PUT/DELETE /peppol/{id}, readinvoices /
readcreditnotes, exact XML fetch).
- lib/invoices/peppol-inbound-ubl.ts reads the provider's UBL-JSON
(xml2js-style prefixed keys, verified against Qvalia's real inbound test
invoice, kept as a fixture) into a neutral document: parties, payment
means with SE:BANKGIRO/SE:PLUSGIRO/IBAN, totals, VAT subtotals, lines,
embedded attachments, credit notes.
- Migration 20260821170000: peppol_registrations (one live row per company
and participant), peppol_inbound_documents (exact XML immutable and
undeletable, routed once), invoice_inbox_items.source gains 'peppol' with a
per-channel dedupe index; pg-real test covers RLS, uniqueness, immutability
and routing.
- POST/DELETE/GET /api/settings/peppol + "E-faktura via Peppol" switch in
Settings > Fakturering; personnummer-based companies are refused until 0088
GLN exists; sandbox refused.
- GET /api/peppol/inbound/cron every 10 minutes: archive, route, deliver.
lib/invoices/peppol-inbox-delivery.ts archives the XML as a WORM document
(upload_source e_invoice, extractionOwner none), an embedded PDF when
present, and creates the inbox row with the extraction filled from the UBL
(confidence 1, no model pass), matching the supplier by org number. The
existing inbox review/convert flow takes over.
- document-service accepts application/xml for the archive; inbox list shows
a Peppol icon.
Refs #546
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ
* test(peppol): archive contract, pg fixture and phantom-column ceiling for the receiving tables
The two new tables are räkenskapsinformation and join MASTER_DATA_DUMP_TABLES;
the pg fixture for a deregistered row now carries deregistered_at as the
status-shape constraint requires; the archive insert is an inline literal and
the one generic processing-state updater is accounted for in the ceiling.
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>