Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2681db0
docs(infakt): add first 4 manual inFakt-dashboard walkthrough screens…
norbert-kulus-blockydevs Jul 1, 2026
169bdfe
docs(infakt): add real inFakt E2E screenshots (connection, issuance, …
norbert-kulus-blockydevs Jul 1, 2026
d60f2b3
docs(infakt): drop broken invoice-detail-page screenshot (route missi…
norbert-kulus-blockydevs Jul 1, 2026
420216a
docs(infakt): re-capture E2E screenshots with the money-format fix ve…
norbert-kulus-blockydevs Jul 1, 2026
8ca8ec1
docs(infakt): move E2E screenshots to libs/integrations/infakt/docs/a…
norbert-kulus-blockydevs Jul 1, 2026
6720e0c
docs(infakt): add clean not-issued/submitted screenshots + fresh Part…
norbert-kulus-blockydevs Jul 1, 2026
14fd402
docs(infakt): re-capture screenshots with the cleared->accepted fix v…
norbert-kulus-blockydevs Jul 1, 2026
b0d61a9
docs(infakt): ADR-030 KSeF indirection model + operator setup guide
norbert-kulus-blockydevs Jul 1, 2026
161ab4a
docs(infakt): payment method, bank-account picker, PDF download in se…
norbert-kulus-blockydevs Jul 3, 2026
d453559
Merge remote-tracking branch 'origin/main' into 1307-fixes-local
norbert-kulus-blockydevs Jul 6, 2026
999dda5
docs(infakt): address #1307 review - accepted mapping, webhook-secret…
norbert-kulus-blockydevs Jul 6, 2026
e304219
fix(docs): fix inFakt ADR-030's misleading no-submit-primitive framing
norbert-kulus-blockydevs Jul 6, 2026
7511201
merge main into 1283-infakt-adr-setup-guide
norbert-kulus-blockydevs Jul 8, 2026
5c4f62f
docs(infakt): move setup-guide.md to libs/integrations/infakt/docs/ f…
norbert-kulus-blockydevs Jul 8, 2026
ba61812
Merge remote-tracking branch 'origin/main' into 1283-infakt-adr-setup…
norbert-kulus-blockydevs Jul 14, 2026
1f6e1e4
docs(infakt): catch up ADR-030/README/setup-guide/capabilities.md to …
norbert-kulus-blockydevs Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/architecture-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,13 @@ The system is organized into the following core bounded contexts:
- `RegulatoryDocumentReader` (#1224) — retrieve the authority's confirmation document (e.g. the PL UPO) or a rendered view for a cleared document, as a neutral `RegulatoryDocument` blob.
- `BankAccountsReader` (#1303 follow-up) — list the seller's payable bank accounts known to the provider (neutral `InvoicingBankAccount`), backing the live picker for Transfer invoices.
- `BankAccountDefaultSetter` (extends `BankAccountsReader`, #1303 follow-up) — mark an account as the provider's own default so it stays in sync with the account OL stamps on Transfer invoices.
- `RegulatoryResubmitter` (#1356) — re-trigger transmission of an ALREADY-ISSUED document to the tax authority, for the operator "resend to KSeF" action on a document whose clearance ended in `rejected`. Kept flat (does not `extends RegulatoryStatusReader`) — it backs a NATIVELY-transmitting provider (e.g. inFakt) that needs an explicit resubmit kick from OL, distinct from `RegulatoryTransmitter` (OL itself holds the authority session).
- `PaymentStatusReader` (#1354) — authoritative re-read of a document's payment state; a provider payment webhook is only a trigger, core always re-reads via this capability rather than trusting the webhook body.
- `PaymentMarker` (#1362) — push an authoritative "paid" state to the provider for an order settled elsewhere (e.g. a marketplace order the buyer paid off-platform, so the provider's own bank-statement matching has nothing to reconcile against).
- `InvoiceEmailSender` (#1353) — trigger the provider to render and email the already-issued invoice to the buyer.
- `CorrectionIssuer` (#1229) — issue a correction (`IssueCorrectionCommand`) of an already-issued document. `IssueCorrectionCommand.originalDocument` is an OPTIONAL, caller-assembled reconstruction (buyer/currency/lines/clearance linkage) for adapters that cannot correct via a per-line delta and must resubmit a complete document (KSeF's FA(3) KOR, #1288); adapters that only need deltas (Subiekt) ignore the field. **Issuance-time line snapshot (#1297)**: `InvoiceRecord` persists a neutral `issuedLineSnapshot` (`{ buyer, currency, lines }`) whenever a document is issued (from the issue command) or corrected (the correction's own post-correction lines) — captured in the core `InvoiceService`, so no adapter change. The HTTP controller assembles `originalDocument` from that snapshot on the document being corrected, so the `originalLineNumber`-indexed deltas diff against the lines *as issued* (and, for a correction-of-correction, against the prior correction's own lines) even if the order changed since. Only records issued before the snapshot column existed fall back to rebuilding from the order's *current* state (the pre-#1297 path, with its `buyerTaxId: null` + line-fidelity caveats).
- **First provider**: **KSeF** (Polish national e-invoicing), `@openlinker/integrations-ksef`, adapterKey `ksef.publicapi.v2`, registry capability `Invoicing`. `KsefInvoicingAdapter` implements `InvoicingPort`, `RegulatoryTransmitter`, and `CorrectionIssuer` (#1288): FA(3) `VAT` + `KOR` documents are issued and cleared through the async submit→poll→UPO model (`issueInvoice`), and `issueCorrection` is a pure delegation into the same KOR path (KSeF has no delta-only correction primitive — every correction resubmits a complete FA(3) document). See the [KSeF setup guide](../libs/integrations/ksef/docs/setup-guide.md) for the full flow, current limitations, and compliance caveats.
- **Second provider**: **inFakt** (Polish accounting SaaS), `@openlinker/integrations-infakt`, adapterKey `infakt.accounting.v1`, registry capability `Invoicing`. Acts as a **KSeF intermediary** ([ADR-030](./architecture/adrs/030-infakt-ksef-indirection.md)): `issueInvoice` creates the document via `POST /invoices.json`, then explicitly triggers submission (`POST /invoices/{uuid}/send_to_ksef.json`, inline second step — verified live that an inFakt draft does NOT auto-submit on its own). From that point on inFakt processes clearance through its own KSeF integration, on its own timing — OL never opens a KSeF session itself. `InfaktInvoicingAdapter` implements `InvoicingPort`, `RegulatoryStatusReader` (not `RegulatoryTransmitter` — OL triggers submission but does not own the session/timing), `RegulatoryResubmitter` (#1356, re-send a rejected document without re-issuing), `RegulatoryDocumentReader` (#1321, PDF download), `CorrectionIssuer`, `BankAccountsReader` + `BankAccountDefaultSetter` (#1310, live bank-account picker), `PaymentStatusReader` + `PaymentMarker` (#1354/#1362, inbound/outbound payment-status sync), and `InvoiceEmailSender` (#1353, one-click email to buyer). Clearance status is read via `GET /invoices/{uuid}.json` (`ksef_data.status`); inFakt's `send_to_ksef_success` / `send_to_ksef_error` webhooks (decoded via the ADR-021 inbound-webhook-decoder pattern) are a push-notification shortcut that triggers an immediate status re-read rather than being trusted as the system of record, and `invoice_marked_as_paid` webhooks drive the `PaymentStatusReader` re-read. See the [inFakt setup guide](../libs/integrations/infakt/docs/setup-guide.md) for connection setup, webhook configuration, and troubleshooting.

---

Expand Down
140 changes: 140 additions & 0 deletions docs/architecture/adrs/030-infakt-ksef-indirection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# ADR-030: Infakt as a KSeF intermediary — `RegulatoryStatusReader`, not `RegulatoryTransmitter`

- **Status**: Accepted
- **Date**: 2026-07-02
- **Authors**: OpenLinker maintainers (retrospective documentation of decisions made across PRs #1275, #1292, #1293)

## Context

inFakt is a Polish accounting SaaS and the second provider of the country-agnostic
**Invoicing** domain ([ADR-026](./026-country-agnostic-invoicing-domain.md)),
`@openlinker/integrations-infakt` (adapterKey `infakt.accounting.v1`). Like KSeF
(`@openlinker/integrations-ksef`) and Subiekt, it ultimately clears invoices through
Poland's national e-invoicing system, KSeF — but the three providers sit at three
different points on the "who owns the KSeF session" axis:

- **KSeF direct** (`@openlinker/integrations-ksef`): OL itself holds the NIP-authenticated
KSeF session, builds the FA(3) XML, opens/submits/closes the session, and polls for the
UPO. OL is the transmitter.
- **Subiekt**: a local desktop bridge auto-submits to KSeF on Subiekt's own schedule; OL
only reads back the resulting status.
- **inFakt**: OL calls inFakt's REST API to *create* an invoice (`POST /invoices.json`),
then explicitly triggers submission (`POST /invoices/{uuid}/send_to_ksef.json`) as an
inline second step. From that point on, inFakt processes clearance using its own KSeF
integration, on its own timing — OL never touches KSeF (the session, the FA(3) XML,
or the UPO) directly for inFakt-issued documents.

The `RegulatoryTransmitter` sub-capability ([ADR-026 amendment](./026-country-agnostic-invoicing-domain.md#amendments))
models "submit for clearance + read status" as one capability, precisely because a
provider that *actively transmits* necessarily also needs to read back the authority
reference it just submitted for. inFakt breaks that assumption differently than it
first appears: OL's adapter **does** call an explicit submit primitive
(`POST /invoices/{uuid}/send_to_ksef.json`, inline inside `issueInvoice`/
`issueCorrection`) — verified live (2026-07-01) that an inFakt draft does **not**
auto-submit to KSeF on its own, so this call is what actually starts clearance. What
OL does not have is *ownership* of the submission: inFakt — not OL — still holds the
KSeF session, builds the FA(3) XML, and is the authority on clearance status. OL's
call only hands an already-created document to inFakt's own KSeF integration; it
carries no independent retry/timing semantics of its own the way a true
`submitForClearance()` would.

## Decision

`InfaktInvoicingAdapter` implements `InvoicingPort` + `RegulatoryStatusReader` +
`CorrectionIssuer` — **not** `RegulatoryTransmitter`.

- **No public `submitForClearance()`.** OL retains an out-of-port `sendToKsef` trigger
(called inline by `issueInvoice`/`issueCorrection`, not exposed as a standalone port
method) and does not surface it as a `RegulatoryTransmitter` method because clearance
timing and status ownership stay with inFakt, not OL — a failed `sendToKsef` call
after a successful create leaves an orphaned un-submitted document rather than a
resumable submit step, and the actual clearance work (KSeF session, FA(3) XML, UPO)
happens entirely inside inFakt's infrastructure. Clearance itself still completes on
inFakt's own timing (confirmed live on sandbox, #1274: ~90s from the inline trigger).
- **Status polling via `getClearanceStatus()`.** The adapter reads
`GET /invoices/{uuid}.json` and maps `invoice.ksef_data.status`
(`pending | sent | success | error`) onto the neutral `RegulatoryStatus`
(`submitted | submitted | accepted | rejected`; absent `ksef_data` → `not-applicable`).
`success` maps to the terminal `accepted` state, not `cleared` — `cleared` is reserved
for split-clearance regimes no current provider emits, and a `cleared` mapping here
previously left the FE status badge stuck at "CLEARING" (#1293 review, live E2E
finding).
- **Webhooks are a push-notification shortcut, not a transport.** inFakt fires
`send_to_ksef_success` / `send_to_ksef_error` webhooks the moment its own KSeF
submission resolves. `InfaktInboundWebhookDecoderAdapter` (ADR-021) decodes these
and routes them to trigger an immediate `getClearanceStatus()` re-read rather than
trusting the webhook payload as the system of record — the same "webhook = trigger,
poll = reconciliation backstop" posture the PrestaShop order pipeline already uses.
`invoice_marked_as_paid` / `invoice_marked_as_paid_via_async_api` (#1354) route the
same way to the `invoice-payment` domain, triggering a `getPaymentStatus()` re-read
via `PaymentStatusReader` instead — a sibling capability to `RegulatoryStatusReader`,
added after this ADR was first written, for the same "webhook = trigger" reason.
Every other inFakt event (e.g. `draft_invoice_created`) is acknowledged and ignored
(`toOlDomain()` returns `null`).

## Alternatives considered

- **Build a direct KSeF session from the OL side, bypassing inFakt's own submission**
(i.e. treat inFakt as a plain accounting API and drive KSeF the way
`@openlinker/integrations-ksef` does). Rejected: requires OL to hold a second
NIP-authenticated KSeF token + certificate per operator and to build FA(3) XML a
second time for the same tenant — duplicate infrastructure inFakt already runs, for
no operator benefit, and inFakt would still submit the invoice on its own schedule
regardless, risking a double-submission race.
- **Disable KSeF integration in inFakt's account settings and drive KSeF directly from
OL.** Viable as a fallback but rejected as the default: it throws away the one thing
inFakt uniquely offers over direct KSeF — zero crypto/session management on OL's
side — and would make the inFakt adapter a strictly worse KSeF adapter. Left as an
operator escape hatch, not modeled in code.
- **Model `submitForClearance()` as a no-op that just calls `getClearanceStatus()`
immediately**, to satisfy `RegulatoryTransmitter`'s interface shape. Rejected: it
would lie about what the method does (nothing is "submitted" by calling it) and
would let call sites believe OL controls submission timing, which it does not.

## Consequences

**Pros:**
- The capability the adapter declares (`RegulatoryStatusReader`) accurately reflects
what OL can and cannot do — no misleading "submit" affordance that silently no-ops.
- Core call sites that narrow via `isRegulatoryTransmitter` correctly skip inFakt and
never attempt a submit that has no meaning for this provider.
- The webhook-as-shortcut design means status usually reflects reality within seconds
of inFakt's own submission, without OL polling on a tight interval.

**Cons / trade-offs:**
- OL cannot retry a failed `sendToKsef` call independently of re-issuing the document,
and cannot control the timing of the clearance work itself once inFakt has accepted
the submission — that part lives entirely inside inFakt's own KSeF integration.
- If KSeF integration is disabled in the operator's inFakt account settings, the
explicit `sendToKsef` call OL makes fails outright rather than silently degrading;
`regulatoryStatus` on every inFakt invoice then stays `not-applicable`. There is no
code path to notice and warn about this misconfiguration ahead of time — it's an
operational, bookkeeping-side setting, not one OL surfaces or should re-implement
(raised, not fixed, in the
[setup guide](../../../libs/integrations/infakt/docs/setup-guide.md#troubleshooting)).

## Amendments

- **2026-07-06 — `RegulatoryResubmitter` (#1356).** This ADR's "Cons" section noted OL
"cannot retry a failed `sendToKsef` call independently of re-issuing the document."
`RegulatoryResubmitter.resubmitForClearance` closes that specific gap for the
operator-facing case: it re-hits `send_to_ksef.json` for the SAME
`providerInvoiceId` (never re-POSTs `invoices.json`, so it cannot double-issue) and
backs a "resend to KSeF" action gated to documents whose clearance ended in
`rejected`. It is a NEW, independent sub-capability, not a retrofit of
`RegulatoryTransmitter` — the decision above (no public `submitForClearance()`)
stands unchanged; a resubmitter is deliberately modeled `flat` (does not `extends
RegulatoryStatusReader`) precisely because it does not imply OL owns the session the
way a transmitter would. See `docs/capabilities.md` for the sub-capability's full
contract.

## References

- Related issues: #1279 (epic), #1274/#1275 (feasibility POC), #1280/#1292 (adapter
hardening), #1281/#1293 (registration + webhook routing), #1283 (this ADR)
- Related ADRs: [ADR-026](./026-country-agnostic-invoicing-domain.md) (country-agnostic
invoicing domain + the `RegulatoryTransmitter`/`RegulatoryStatusReader` split),
[ADR-021](./021-third-party-native-inbound-webhook-ingestion.md) (per-provider inbound
webhook decoder — how the `send_to_ksef_*` events reach OL)
- Primary doc section: [docs/architecture-overview.md § 14 Invoicing](../../architecture-overview.md#14-invoicing)
- Operator guide: [libs/integrations/infakt/docs/setup-guide.md](../../../libs/integrations/infakt/docs/setup-guide.md)
1 change: 1 addition & 0 deletions docs/architecture/adrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ One pointer per section, identical format every time.
| [ADR-027](./027-order-status-writeback-capability-and-relay.md) | Order status writeback capability (event-as-data) & lifecycle relay | Accepted | 2026-06-22 |
| [ADR-028](./028-order-cancellation-stock-restore.md) | Order-cancellation-observe hook → marketplace stock-restore | Proposed | 2026-06-22 |
| [ADR-029](./029-versioning-and-release-strategy.md) | Versioning and release strategy (four axes) | Accepted | 2026-06-30 |
| [ADR-030](./030-infakt-ksef-indirection.md) | Infakt as a KSeF intermediary — `RegulatoryStatusReader`, not `RegulatoryTransmitter` | Accepted | 2026-07-02 |
| [ADR-031](./031-erli-allegro-category-catalog-via-client-credentials.md) | Erli category/parameter browsing via an Erli-owned Allegro client-credentials token | Proposed | 2026-07-07 |
| [ADR-032](./032-demo-only-vendor-neutral-analytics-config-seam.md) | Demo-only, vendor-neutral analytics/integration config seam on `/system/config` | Proposed | 2026-07-08 |
| [ADR-033](./033-openlinker-as-mcp-server.md) | Expose OpenLinker as an MCP server | Proposed | 2026-07-11 |
Expand Down
6 changes: 5 additions & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,20 @@ The canonical OL-owned order-lifecycle state machine (authoritative
| `PickupPointFinder` | Search the carrier's pickup points / lockers (e.g. Paczkomat). | `findPickupPoints` | `isPickupPointFinder` |
| `ShipmentCanceller` | Cancel / void a registered shipment. | `cancelShipment` | `isShipmentCanceller` |

### `InvoicingPort` (invoicing) — 6
### `InvoicingPort` (invoicing) — 10

| Sub-capability | What it does | Method(s) | Guard |
|---|---|---|---|
| `RegulatoryStatusReader` | Read the clearance status of a previously-submitted document. | `getClearanceStatus` | `isRegulatoryStatusReader` |
| `RegulatoryTransmitter` *(extends `RegulatoryStatusReader`)* | Submit a document to the tax authority for clearance (+ read its status). | `submitForClearance` | `isRegulatoryTransmitter` |
| `RegulatoryResubmitter` | Re-trigger transmission of an ALREADY-ISSUED document (e.g. the operator "resend to KSeF" action on a rejected document) — flat, not `extends RegulatoryStatusReader`. | `resubmitForClearance` | `isRegulatoryResubmitter` |
| `RegulatoryDocumentReader` | Retrieve the authority's confirmation document (e.g. the PL UPO) or a rendered view for a cleared document. | `getRegulatoryDocument` | `isRegulatoryDocumentReader` |
| `CorrectionIssuer` | Issue a correcting document (e.g. KSeF `KOR`) against an original. | `issueCorrection` | `isCorrectionIssuer` |
| `BankAccountsReader` | List the seller's payable bank accounts known to the provider (live picker for Transfer invoices). | `listBankAccounts` | `isBankAccountsReader` |
| `BankAccountDefaultSetter` *(extends `BankAccountsReader`)* | Mark an account as the provider's own default, keeping it in sync with the account OL stamps on Transfer invoices. | `setDefaultBankAccount` | `isBankAccountDefaultSetter` |
| `PaymentStatusReader` | Authoritative re-read of a document's payment state (a provider payment webhook is only a trigger, never trusted as the system of record). | `getPaymentStatus` | `isPaymentStatusReader` |
| `PaymentMarker` | Push an authoritative "paid" state to the provider for an order settled elsewhere (e.g. a marketplace order the seller's bank statement can't auto-match). | `markPaid` | `isPaymentMarker` |
| `InvoiceEmailSender` | Trigger the provider to render and email the already-issued invoice to the buyer. | `sendByEmail` | `isInvoiceEmailSender` |

See [ADR-026](./architecture/adrs/026-country-agnostic-invoicing-domain.md) for
the country-agnostic invoicing design.
Expand Down
Loading
Loading