Repository navigation
[Invoices]: Inbound e-invoice reception — receive purchase documents from e-invoicing providers #658
Description
Parent issue: #126 — its fetchAcknowledgements/webhooks cover status of outbound documents only. This issue ports the v1.80 inbound pipeline: application/modules/integrations/controllers/Integrations.php:157-206 (receive($merchantClientId)), application/modules/integrations/controllers/Incoming.php (listing + sync), Merchant_responses_model::create_inbound with MerchantResponseDirection::In / MerchantResponseType::IncomingInvoice, and the Peppol document-type registry application/modules/integrations/libraries/PeppolDocumentType.php (BillingInvoice, BillingCreditNote, Order, OrderResponse, Catalogue, MessageLevelResponse — full urn:oasis:... IDs).
A Peppol participant receives documents (supplier invoices, credit notes, MLRs) as well as sending them. v1.80 pulls inbound documents from the configured provider, stores payloads under uploads/integrations/incoming/, logs each in ip_merchant_responses (direction in, status received), and lists them on an Incoming screen. v2 has nothing inbound.
Specific
ProviderInterface([Invoices]: Peppol e-invoicing — pluggable multi-provider architecture #126) gainsfetchInboundDocuments(?string $since): array(LetsPeppol exposes this via its Document/Transmission endpoints — see v1providers/LetsPeppol/Endpoints/LetsPeppolDocumentEndpoint.php).- Scheduled job
FetchInboundPeppolDocumentsJobper enabled integration: downloads new documents, stores the raw XML via the existing Upload/storage layer (company-scoped, private disk), records a row in apeppol_inbound_documentstable (integration id, external id, document type urn →PeppolDocumentTypeenum, sender participant id, received_at, statusreceived|processed|error, payload path). - Filament "Incoming documents" page (Company panel, Invoices or Expenses module) listing inbound documents with download-XML action and a manual "Sync now" button (v1
Incoming::sync). - Idempotency: re-syncing must not duplicate documents (unique on integration + external id).
- Out of scope (follow-up): parsing inbound UBL into draft Expenses/purchase invoices.
Measurable
- With a faked provider returning 2 documents, sync creates exactly 2 inbound rows and 2 stored XML files; a second sync creates 0.
- The Incoming page lists them with sender, type label (from the urn enum), and date.
- Provider errors mark the sync attempt logged with status
errorwithout losing prior rows.
Achievable
Builds directly on #126's provider architecture, peppol_integrations config, and LogsPeppolActivity; storage via the existing Core Upload model; UI is a standard Filament resource/page.
Relevant
Belgium mandates B2B e-invoicing via Peppol from 2026-01 — receiving is as mandatory as sending. v1.80 users switching to v2 would lose inbound documents entirely.
Time-Bound
Sprint 6+ (after #126 Sprint 5 outbound scope), before any "v1.80 integrations parity" release claim.
Arrange
- Enabled Peppol integration for the company;
Http::fake()provider returning two inbound UBL invoices with distinct external ids.
Act
- Run
FetchInboundPeppolDocumentsJob(and once more for idempotency); open the Incoming page.
Assert
- Two
peppol_inbound_documentsrows exist with direction-in semantics, correct external ids, andPeppolDocumentType::BillingInvoiceresolved from the urn. - Raw XML stored on the company-scoped private disk and downloadable from the page.
- Second run creates zero new rows (unique integration+external id).
- Manual "Sync now" action triggers the same fetch and notifies success with the count.
- Tenant isolation: a user of another company sees no documents.
Activity