Partner invoice upload flow¶
When an authorised partner (today: optician on Marmot, supplier = LPL) uploads
a PDF invoice for a Saleor fulfillment, the request goes through 3 explicit
phases, each backed by a separate "brick". The orchestrator
(process_partner_invoice.py) wires them
together and decides which persistence path to take based on the check result.
βββββββββββββββββββββββββββββββββββββββββββ
β Controller : POST .../invoice β
β admin_order.py β
ββββββββββββββββββββββ¬βββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββ
β ORCHESTRATOR β
β process_partner_invoice_upload β
ββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β β β
βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββββββ
β BRICK 1 β β BRICK 2 β β BRICK 3 β
β Parse + β β β Automated β β β Persist β
β optician-facing β β checks β β (success / fail) β
β validation β β (silent, ops) β β β
βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββββββ
raises ValueError returns list[str] writes Saleor
on user-fixable on rule failures metadata
problem β drives UI badge
Brick 1 β Parse + opticien-facing validation¶
Module : partner_invoice_upload.py
Purpose : turn the raw PDF into a structured
ParsedPartnerInvoice and reject inputs the
optician can fix themselves (wrong file, wrong commande, etc.).
Country-agnostic : the parser dispatch + the validation rules apply to
every country that uses this flow. The country adapter is used only to
resolve the buyer's name via resolve_user_name_from_user_id.
Dispatch : parser selection is driven by warehouse.slug through
PARSER_REGISTRY. Adding a new supplier means registering a new parser
function in that mapping (and nothing else in this brick).
Failure mode : ValueError β the controller surfaces it as a 400 with
the error message. The optician sees the error in the dashboard and retries
with a fixed file.
Brick 2 β Automated checks (silent, ops-facing)¶
Modules :
- shop aggregator : glasses_invoice_post_upload_checks.py
- country-specific stub : fr/.../glasses_invoice_post_upload_checks.py
Purpose : run rules that should not block the upload from the optician's
perspective but should prevent the system from creating an InsuranceDocument
on suspicious data.
Reached via : the shop aggregator delegates to
shop_adapter.run_country_specific_invoice_automated_checks, which routes to
the appropriate country's check module. The base adapter ships a no-op
default (return []), so a country opts in by overriding the method.
Failure mode : returns a list[str] of human-readable error messages.
Empty list β success branch. Non-empty β failure branch (see Brick 3).
No exception is raised β the optician's UI flow never stops here.
Adding a new check rule : append to the country-specific module's
run_*_invoice_automated_checksfunction. The specific rules are documented in the source, intentionally not duplicated here β they evolve per business need.
Brick 3 β Persistence¶
Module : process_partner_invoice.py
(_persist_successful_upload / _persist_failed_upload)
The orchestrator branches based on the aggregated check errors :
Success path (no errors)¶
- Calls
shop_adapter.create_insurance_document_from_parsed_partner_invoice, which delegates to the country's claim engine to create a realInsuranceDocumentwith care acts. - Writes
invoice-document-id+invoice-file-nameon the Saleor fulfillment metadata. - Posts a success Saleor order note.
Failure path (β₯ 1 error)¶
- Calls the shop-level
store_failed_partner_invoice_pdf, which uploads the rejected PDF as a genericDocument(with the country-specificDocumentTypefromshop_adapter.get_lab_invoice_document_type) β it stays out of the claim engine. - Writes
failed-invoice-document-id+failed-invoice-file-nameon the fulfillment metadata. - Posts a failure Saleor order note listing every error.
- Pings ops on Slack so a human can review the PDF.
The two metadata key sets are mutually exclusive : the presence of
invoice-document-id is the "checks passed" signal, the presence of
failed-invoice-document-id is the "checks failed" signal. No separate flag
needed.
How the frontend uses the metadata¶
The Marmot optician dashboard reads the fulfillment metadata to render the tΓ©lΓ©TT status :
| Metadata key present | Badge | Meaning |
|---|---|---|
invoice-document-id |
π‘ PrΓͺt pour tΓ©lΓ©transmission | Care acts created, ready for the claim engine. |
failed-invoice-document-id |
π En attente de validation de la facture | Rejected PDF stored, ops needs to review. |
| neither | (no badge) | No invoice uploaded yet. |