diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 961e706d2..1490d0f65 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -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. --- diff --git a/docs/architecture/adrs/030-infakt-ksef-indirection.md b/docs/architecture/adrs/030-infakt-ksef-indirection.md new file mode 100644 index 000000000..e9d0ffd4f --- /dev/null +++ b/docs/architecture/adrs/030-infakt-ksef-indirection.md @@ -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) diff --git a/docs/architecture/adrs/README.md b/docs/architecture/adrs/README.md index 5fc20d771..aac2938f7 100644 --- a/docs/architecture/adrs/README.md +++ b/docs/architecture/adrs/README.md @@ -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 | diff --git a/docs/capabilities.md b/docs/capabilities.md index fb5e905bb..d432c8049 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -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. diff --git a/docs/plans/implementation-plan-infakt-docs-adr.md b/docs/plans/implementation-plan-infakt-docs-adr.md new file mode 100644 index 000000000..39ed47533 --- /dev/null +++ b/docs/plans/implementation-plan-infakt-docs-adr.md @@ -0,0 +1,283 @@ +# Implementation Plan: inFakt ADR + operator setup guide (#1283) + +**Date**: 2026-07-02 +**Status**: Ready for Review +**Estimated Effort**: 3-4 hours + +--- + +## Retarget addendum (2026-07-06) + +The plan below was written against a stacked-branch layout that no longer holds: +PR #1307 now targets `main` directly rather than `1282-infakt-fe-plugin`, and its +prerequisites (#1281, #1282, #1309, #1310) have all merged. The PR's diff also grew +four screenshots (`06`-`07`, wizard/edit-form payment-method + bank-picker shots) +that this plan's original "no new screenshots" constraint did not anticipate - both +came from documenting features that landed after this plan was written. Constraints +below are historical context, not current scope. + +--- + +## 1. Task Summary + +**Objective**: Document the architectural decision behind inFakt's KSeF-intermediary +model as an ADR, write an operator-facing setup guide, add the package README the +`@openlinker/integrations-infakt` package is missing, and update +`docs/architecture-overview.md` + the ADR index to reference both. + +**Context**: inFakt (epic #1279) is the second provider of the country-agnostic +Invoicing domain (ADR-026). Its KSeF model differs from both direct KSeF +(`@openlinker/integrations-ksef`, OL owns the session) and Subiekt (local bridge +auto-submits) — inFakt auto-submits to KSeF on its own via its own REST API, so OL +only ever *reads* clearance status. That's a non-obvious, easy-to-misdesign-around +decision, which is exactly what ADR-030 exists to pin down for future maintainers. + +**Classification**: Documentation. + +--- + +## 2. Scope & Non-Goals + +### In Scope +- `docs/architecture/adrs/030-infakt-ksef-indirection.md` (Status: Accepted — the + decision already shipped across #1275/#1292/#1293, so this is retrospective). +- ADR index row in `docs/architecture/adrs/README.md`. +- `docs/integrations/infakt/setup-guide.md` — prerequisites, connection creation, + webhook configuration (URL, HMAC secret, verification handshake), verification + walkthrough, troubleshooting. Illustrated with the 20 real screenshots already + captured on `1282-infakt-fe-plugin` (PR #1300) at + `libs/integrations/infakt/docs/assets/`. +- `libs/integrations/infakt/README.md` — adapter key, capabilities, credentials/config + shape, notable implementation details, doc links (mirrors the Erli/DPD/AI READMEs + from PR #1284). +- `docs/architecture-overview.md` § 14 Invoicing — new "Second provider" paragraph for + inFakt, alongside the existing KSeF paragraph. + +### Out of Scope +- Any code change to the inFakt adapter, plugin, or FE — this issue is docs-only. The + backend (#1280/#1281) and FE (#1282) land in their own PRs. +- New screenshots — the 20 images captured for PR #1300's E2E walkthrough already + cover every UI state this guide needs (wizard, connection test, orders, invoice + issuance/clearance, correction flow) plus the inFakt-dashboard side (login, API key + page, webhooks list, webhook subscription form, invoice-confirmed). + +### Constraints +- This branch is stacked on `1282-infakt-fe-plugin` (not `main`) because the setup + guide documents connection-wizard fields and webhook wiring that only exist on that + branch — #1281's registration/webhook work and #1282's FE plugin are still open PRs. + The PR for this issue targets `1282-infakt-fe-plugin` as its base so the diff stays + docs-only; it re-targets `main` automatically once that branch merges. + +--- + +## 3. Architecture Mapping + +**Target Layer**: Documentation (no code layer touched). + +**Capabilities Involved**: `Invoicing`, `RegulatoryStatusReader`, `CorrectionIssuer` — +described, not implemented, in this issue. + +**Existing Services Reused**: None (docs-only). + +**New Components Required**: None (docs-only). + +**Core vs Integration Justification**: N/A — no code changes. + +--- + +## 4. External / Domain Research + +### Internal Patterns +- **ADR template & conventions**: `docs/architecture/adrs/template.md` + + `docs/architecture/adrs/README.md` (numbering, status taxonomy, retrospective-author + convention, <500-word discipline). +- **Closest ADR precedent**: ADR-026 (country-agnostic invoicing domain) for the + capability-decomposition vocabulary (`RegulatoryTransmitter` / + `RegulatoryStatusReader`), and its "Amendment" section as the precedent for how a + sub-capability gets refined after the fact. +- **Setup-guide precedent**: `docs/integrations/ksef/setup-guide.md` (capability table, + prerequisites, troubleshooting table shape) and the KSeF/Subiekt tutorials from PR + #1284 (screenshot-driven walkthrough structure). +- **README precedent**: `libs/integrations/erli/README.md` (adapter table, capabilities + table, credentials/config JSON blocks, "Notable implementation details", doc links) — + from the same in-flight PR #1284. +- **Source of truth for adapter behavior**: read directly from + `InfaktInvoicingAdapter`, `InfaktWebhookTranslator`, + `InfaktInboundWebhookDecoderAdapter`, `InfaktAdapterFactory`, and + `infakt-connection.types.ts` on `1282-infakt-fe-plugin` rather than from the issue + body alone, per the ADR-authoring rule to verify retrospective specifics against the + actual implementation. +- **Webhook secret mechanics**: `WebhookSecretService.rotate` (OL always generates a + random secret; there is no "set to a caller-supplied value" endpoint) and + `CredentialsWebhookSecretAdapter` (secret stored at + `webhook-secret:`) — this shaped the troubleshooting note about + secret-mismatch being a known rough edge rather than a solved flow. + +--- + +## 5. Questions & Assumptions + +### Open Questions +- Whether inFakt's webhook-subscription UI lets an operator paste in a custom secret, + or only displays one it generates itself. Not verifiable without live dashboard + access in this session. + +### Assumptions +- **Safe default**: document the flow as "generate in OL, paste into inFakt" (the more + common HMAC-webhook UX), and add an explicit fallback note for the reverse case + (copy inFakt's own generated secret into OL) plus a call-out that OL has no + set-custom-value endpoint today — matches the project's "document known gaps, don't + hide them" precedent (PR #1284 did the same for a discovered UI bug, #1287). + +### Documentation Gaps +- None discovered beyond the webhook-secret-direction ambiguity above. + +--- + +## 6. Proposed Implementation Plan + +### Phase 1: Research +1. Read issue #1283, #1279 (epic), #1281 (webhook routing — code exists but PR open). +2. Read `InfaktInvoicingAdapter`, `InfaktWebhookTranslator`, + `InfaktInboundWebhookDecoderAdapter`, `infakt-plugin.ts`, + `infakt-adapter.factory.ts`, `infakt-connection.types.ts` on + `1282-infakt-fe-plugin` for exact wire behavior. +3. Read the generic webhook ingress (`WebhookController`, + `WebhookSecretService`, `CredentialsWebhookSecretAdapter`) to confirm the URL + shape and secret-rotation mechanics. +4. Confirm the FE wizard's actual field labels (`infakt-setup-form.tsx`, + `infakt-setup-page.tsx`) so the guide's field table matches the real UI. + +### Phase 2: ADR +1. **File**: `docs/architecture/adrs/030-infakt-ksef-indirection.md` + - **Action**: write per template — Context / Decision / Alternatives / + Consequences / References, retrospective-authored (decision already merged + across #1275/#1292/#1293), Status: Accepted, dated today. + - **Acceptance**: matches template shape; under ~500 words in the prose sections; + alternatives section lists ≥2 seriously-considered rejected options. +2. **File**: `docs/architecture/adrs/README.md` + - **Action**: add the ADR-030 index row. + - **Acceptance**: row present, links resolve. + +### Phase 3: Setup guide + README +1. **File**: `docs/integrations/infakt/setup-guide.md` + - **Action**: Capabilities table, Prerequisites, connection-creation walkthrough, + webhook-configuration section (URL pattern, HMAC secret, verification handshake), + verification walkthrough, troubleshooting table — each illustrated with the + existing screenshots at `libs/integrations/infakt/docs/assets/`. + - **Acceptance**: every referenced image path exists in the repo tree; field names + match the real wizard. +2. **File**: `libs/integrations/infakt/README.md` + - **Action**: adapter table, capabilities table, credentials/config JSON, + implementation-detail bullets, doc links — mirrors `erli/README.md`. + - **Acceptance**: matches the sibling-package README shape from PR #1284. + +### Phase 4: Architecture overview +1. **File**: `docs/architecture-overview.md` + - **Action**: add a "Second provider" paragraph for inFakt to § 14 Invoicing, + immediately after the existing KSeF paragraph, linking ADR-030 and the new setup + guide. + - **Acceptance**: matches the acceptance-criteria wording quoted in issue #1283. + +### Implementation Details + +**New Components**: none — all changes are Markdown. + +**Configuration Changes**: none. + +**Database Migrations**: none. + +**Events**: none. + +**Error Handling**: N/A. + +--- + +## 7. Alternatives Considered + +### Alternative 1: Write the ADR as "Proposed" pending #1281's PR merge +- **Description**: mark ADR-030 `Proposed` until the registration/webhook PR (#1281) + actually lands on `main`. +- **Why Rejected**: the decision itself (RegulatoryStatusReader-not-Transmitter, the + webhook-as-shortcut design) already shipped in merged commits on #1292 and is stable + regardless of when #1281/#1282 land — only the *host wiring* is still in flight, not + the architectural decision this ADR documents. Every existing retrospective ADR in + this repo is dated to when the underlying decision merged, not when every dependent + PR lands. +- **Trade-offs**: none — `Accepted` is more accurate to what the ADR actually + documents. + +### Alternative 2: Target `main` directly and let the PR carry #1281/#1282's commits too +- **Description**: base this docs branch on `main` instead of stacking on + `1282-infakt-fe-plugin`. +- **Why Rejected**: `main` doesn't yet have the inFakt plugin, webhook decoder, or FE + at all — a setup guide describing a UI and webhook endpoint that don't exist on the + target branch would be undiffable noise, and the PR diff would include ~30 unrelated + commits from #1281/#1282. Stacking keeps this PR's diff to exactly the four docs + files. +- **Trade-offs**: this PR can't merge to `main` until `1282-infakt-fe-plugin` does; + GitHub will retarget it to `main` automatically once that happens (standard + stacked-PR behavior). + +--- + +## 8. Validation & Risks + +### Architecture Compliance +- ✅ N/A — no code layer touched. + +### Naming Conventions +- ✅ ADR filename follows `NNN-kebab-case-title.md`; setup guide follows the + `docs/integrations//setup-guide.md` convention already used by KSeF/Erli. + +### Existing Patterns +- ✅ ADR structure, README structure, and setup-guide structure all mirror existing + sibling docs (ADR-026, `erli/README.md`, `ksef/setup-guide.md`). + +### Risks +- **Webhook-secret-direction ambiguity**: documented as a known gap in + Troubleshooting rather than asserted as verified fact — see + [Questions & Assumptions](#5-questions--assumptions). + +### Edge Cases +- N/A (docs-only). + +### Backward Compatibility +- ✅ Purely additive — no existing doc content removed or restructured other than the + two-line ADR-index and architecture-overview insertions. + +--- + +## 9. Testing Strategy & Acceptance Criteria + +### Unit Tests / Integration Tests +- N/A — no code changes. + +### Acceptance Criteria +- [x] ADR follows the template in `docs/architecture/adrs/README.md` +- [x] ADR status: `Accepted` +- [x] Setup guide covers the webhook URL and HMAC secret configuration steps +- [x] `docs/architecture-overview.md` updated with the inFakt provider entry +- [x] ADR index updated +- [x] Package README added + +--- + +## 10. Alignment Checklist + +- [x] Follows hexagonal architecture (N/A — docs only) +- [x] Respects CORE vs Integration boundaries (N/A) +- [x] Uses existing patterns (ADR template, README precedent, setup-guide precedent) +- [x] Naming conventions followed +- [x] File structure matches standards +- [x] Plan is execution-ready +- [x] Plan is saved as markdown file + +--- + +## Related Documentation + +- [Architecture Overview](../architecture-overview.md) +- [Engineering Standards](../engineering-standards.md) +- [ADR-026](../architecture/adrs/026-country-agnostic-invoicing-domain.md) +- [ADR-030](../architecture/adrs/030-infakt-ksef-indirection.md) diff --git a/libs/integrations/infakt/README.md b/libs/integrations/infakt/README.md new file mode 100644 index 000000000..04af6999e --- /dev/null +++ b/libs/integrations/infakt/README.md @@ -0,0 +1,141 @@ +# @openlinker/integrations-infakt + +inFakt accounting SaaS adapter for OpenLinker — invoice issuance, corrections, and +KSeF clearance-status reads through inFakt's own native KSeF integration. + +## Adapter + +| Property | Value | +|---|---| +| **Adapter key** | `infakt.accounting.v1` | +| **Platform type** | `infakt` | +| **Package** | `@openlinker/integrations-infakt` | + +## Capabilities + +| Capability | Key sub-capabilities | +|---|---| +| `Invoicing` | `InvoicingPort` (`issueInvoice`, `getInvoice`, `upsertCustomer`, `getSupportedDocumentTypes`), `RegulatoryStatusReader` (`getClearanceStatus`), `RegulatoryResubmitter` (`resubmitForClearance`), `CorrectionIssuer` (`issueCorrection`), `BankAccountsReader` (`listBankAccounts`), `BankAccountDefaultSetter` (`setDefaultBankAccount`), `RegulatoryDocumentReader` (`getRegulatoryDocument`, kind `rendered`), `PaymentStatusReader` (`getPaymentStatus`), `PaymentMarker` (`markPaid`), `InvoiceEmailSender` (`sendByEmail`) | + +`RegulatoryTransmitter` is deliberately **not** implemented — see +[ADR-030](../../../docs/architecture/adrs/030-infakt-ksef-indirection.md). OL's adapter +does trigger submission (`send_to_ksef.json`, called inline at issuance), but clearance +timing and status ownership stay with inFakt's own KSeF integration, so that trigger +isn't surfaced as an independently-callable `RegulatoryTransmitter` method — only the +read side (`RegulatoryStatusReader`) makes sense as a public capability here. + +See [`docs/capabilities.md`](../../../docs/capabilities.md) for the full sub-capability +catalog. + +## Credentials & config + +Authentication uses a **static API key** (no OAuth). + +**Credentials** (`InfaktCredentials`, resolved via `CredentialsResolverPort`): +```json +{ + "apiKey": "" +} +``` + +**Config** (`InfaktConnectionConfig`, non-secret, persisted on the connection row): +```json +{ + "baseUrl": "https://api.infakt.pl", + "defaultPaymentMethod": "transfer", + "bankAccount": { + "id": "12345", + "accountNumber": "PL00 0000 0000 0000 0000 0000 0000", + "bankName": "mBank" + } +} +``` + +`baseUrl` is optional — omit it to use inFakt's production API +(`INFAKT_DEFAULT_BASE_URL`); override it to point at a sandbox host. +`defaultPaymentMethod` and `bankAccount` are optional (see #1309/#1310 below) - +omit both to fall back to `cash` with no stamped account. + +## Notable implementation details + +- **KSeF-as-intermediary**: `issueInvoice` creates the document via + `POST /invoices.json`, then explicitly calls `POST /invoices/{uuid}/send_to_ksef.json` + inline (a draft does not auto-submit on its own) — after that, inFakt processes + clearance through its own KSeF integration, on its own timing. + `getClearanceStatus` reads `GET /invoices/{uuid}.json` and maps + `ksef_data.status` (`pending | sent | success | error`) to the neutral + `RegulatoryStatus`. See [ADR-030](../../../docs/architecture/adrs/030-infakt-ksef-indirection.md). +- **Gross→net conversion**: OL's neutral commands carry buyer-paid gross unit prices; + the adapter converts to inFakt's expected net price per line using each line's + `taxRate` before submitting. +- **Corrections**: `issueCorrection` fetches the original invoice, then submits to the + **dedicated** `POST /corrective_invoices.json` endpoint — the generic `invoices.json` + path 404s for corrective documents — carrying a before/after row pair per corrected + line (`correction: false` / `correction: true`). The KSeF submit step also switches + resource: `sendToKsef(uuid, 'corrective_invoices')`, since + `corrective_invoices/{uuid}/send_to_ksef.json` is the only path that resolves for a + corrective uuid (`invoices/{uuid}/send_to_ksef.json` 404s). +- **Resend to KSeF** (#1356, `RegulatoryResubmitter`): `resubmitForClearance` re-hits + `send_to_ksef.json` for the SAME `providerInvoiceId` — it never re-POSTs + `invoices.json`, so it cannot create a second draft. Backs the operator "resend" + action on a document whose clearance ended in `rejected`; the HTTP layer gates the + action to that status so an in-flight or already-accepted document is never re-sent. +- **Payment status sync** (#1354/#1362): `PaymentStatusReader.getPaymentStatus` reads + `GET /invoices/{uuid}.json` and maps inFakt's payment fields to the neutral + `PaymentStatus`; the inbound `invoice_marked_as_paid` / `invoice_marked_as_paid_via_async_api` + webhooks are only a TRIGGER for an immediate re-read (see Inbound webhooks below), + never trusted as the source of truth. The outbound counterpart, `PaymentMarker. + markPaid`, pushes an authoritative "paid" state via + `POST /async/invoices/{uuid}/paid.json` for orders settled elsewhere (e.g. a + marketplace order the buyer paid off-platform, so inFakt's own bank-statement + matching has nothing to reconcile against) — verified live to return a 201 async-task + envelope; re-marking an already-paid invoice is safe. +- **Email delivery** (#1353, `InvoiceEmailSender`): `sendByEmail` triggers + `POST /invoices/{uuid}/deliver_via_email.json`; inFakt composes and sends the message + itself using the client's stored email (no recipient override) and flips the invoice + status to `sent` on its side. +- **Per-connection default payment method** (#1309): `config.defaultPaymentMethod` + (`cash | transfer`) is stamped as `payment_method` on every issued document; + unset falls back to `cash`. `transfer` 422s on inFakt unless the seller's account + has a bank account configured. +- **Bank-account picker with live inFakt default sync** (#1310): `BankAccountsReader. + listBankAccounts()` (`GET /bank_accounts.json`) feeds the wizard and edit-form + picker; the picked account is snapshotted into `config.bankAccount` and pushed back + as the inFakt default via `BankAccountDefaultSetter.setDefaultBankAccount()`. + `transfer` invoices carry the snapshot's `bank_account` / `bank_name` fields. +- **Rendered-PDF download** (#1321): `RegulatoryDocumentReader.getRegulatoryDocument + (record, 'rendered')` fetches the invoice PDF as rendered by inFakt - this backs + the **Download PDF** button on the accepted invoice detail page. +- **Inbound webhooks**, three collaborating classes: `InfaktWebhookTranslator` is the + shared crypto/parsing core — `X-Infakt-Signature` verification (HMAC-SHA256 over the + raw body), the subscription-verification handshake + (`{"verification_code": "..."}` echo), and the event-name allowlist. + `InfaktInboundWebhookDecoderAdapter` (`InboundWebhookDecoderPort`, ADR-021) wraps it + to authenticate + decode at OL's generic `POST /webhooks/:provider/:connectionId` + ingress. `InfaktWebhookEventTranslatorAdapter` (`WebhookEventTranslatorPort`, + ADR-015) then maps the decoded envelope to one of TWO domains: `send_to_ksef_success` + / `send_to_ksef_error` route to `invoicing` (triggers a `getClearanceStatus` + re-read); `invoice_marked_as_paid` / `invoice_marked_as_paid_via_async_api` (#1354) + route to `invoice-payment` (triggers a `getPaymentStatus` re-read). Every other event + (e.g. `draft_invoice_created`) is acknowledged and ignored. Webhook subscriptions + themselves are configured manually in the inFakt dashboard; there is no + `WebhookProvisioningPort` for this adapter. +- **Retry classification**: `InfaktRetryClassifierAdapter` marks rate-limit and + transient-network failures retryable, and validation/auth failures terminal, for the + worker's job runner. +- **Shipping line on invoices** (#1517/#1562, core-level — not inFakt-specific): the + order → `IssueInvoiceCommand` mapper appends a neutral gross shipping line whenever + `order.totals.shipping > 0`, so the invoice gross total matches the buyer-paid order + total; the adapter consumes `cmd.lines` verbatim like any other line. The label + defaults to a neutral English string and can be overridden per-connection via + `config.invoicing.shippingLineName` (any non-empty string, e.g. `"Shipping"` / + `"Dostawa"`) — set it alongside `InfaktConnectionConfig` on the same connection row. + +## Documentation + +- [docs/setup-guide.md](./docs/setup-guide.md) — operator + setup guide (connection creation, webhook configuration, troubleshooting) +- [ADR-030](../../../docs/architecture/adrs/030-infakt-ksef-indirection.md) — design + rationale for the KSeF-intermediary model +- [ADR-026](../../../docs/architecture/adrs/026-country-agnostic-invoicing-domain.md) — + the country-agnostic invoicing domain this provider plugs into diff --git a/libs/integrations/infakt/docs/assets/01-infakt-wizard-empty.png b/libs/integrations/infakt/docs/assets/01-infakt-wizard-empty.png index b3655f420..f288c91b2 100644 Binary files a/libs/integrations/infakt/docs/assets/01-infakt-wizard-empty.png and b/libs/integrations/infakt/docs/assets/01-infakt-wizard-empty.png differ diff --git a/libs/integrations/infakt/docs/assets/02-infakt-wizard-filled.png b/libs/integrations/infakt/docs/assets/02-infakt-wizard-filled.png index 59bc79bd5..68ad196e0 100644 Binary files a/libs/integrations/infakt/docs/assets/02-infakt-wizard-filled.png and b/libs/integrations/infakt/docs/assets/02-infakt-wizard-filled.png differ diff --git a/libs/integrations/infakt/docs/assets/06-infakt-edit-payment-section.png b/libs/integrations/infakt/docs/assets/06-infakt-edit-payment-section.png new file mode 100644 index 000000000..841ac59a2 Binary files /dev/null and b/libs/integrations/infakt/docs/assets/06-infakt-edit-payment-section.png differ diff --git a/libs/integrations/infakt/docs/assets/07-infakt-edit-bank-persisted.png b/libs/integrations/infakt/docs/assets/07-infakt-edit-bank-persisted.png new file mode 100644 index 000000000..7a4248750 Binary files /dev/null and b/libs/integrations/infakt/docs/assets/07-infakt-edit-bank-persisted.png differ diff --git a/libs/integrations/infakt/docs/assets/12-invoice-accepted-cleared.png b/libs/integrations/infakt/docs/assets/12-invoice-accepted-cleared.png index e8bc6aeed..a4aab81cd 100644 Binary files a/libs/integrations/infakt/docs/assets/12-invoice-accepted-cleared.png and b/libs/integrations/infakt/docs/assets/12-invoice-accepted-cleared.png differ diff --git a/libs/integrations/infakt/docs/assets/13-invoice-detail-page.png b/libs/integrations/infakt/docs/assets/13-invoice-detail-page.png index 47aaac3a4..c03658e25 100644 Binary files a/libs/integrations/infakt/docs/assets/13-invoice-detail-page.png and b/libs/integrations/infakt/docs/assets/13-invoice-detail-page.png differ diff --git a/libs/integrations/infakt/docs/assets/14-correction-modal-empty.png b/libs/integrations/infakt/docs/assets/14-correction-modal-empty.png index c2738b7e3..a92daefec 100644 Binary files a/libs/integrations/infakt/docs/assets/14-correction-modal-empty.png and b/libs/integrations/infakt/docs/assets/14-correction-modal-empty.png differ diff --git a/libs/integrations/infakt/docs/assets/15-correction-modal-filled.png b/libs/integrations/infakt/docs/assets/15-correction-modal-filled.png index 208fbbf75..f04327f5f 100644 Binary files a/libs/integrations/infakt/docs/assets/15-correction-modal-filled.png and b/libs/integrations/infakt/docs/assets/15-correction-modal-filled.png differ diff --git a/libs/integrations/infakt/docs/assets/16-correction-issued.png b/libs/integrations/infakt/docs/assets/16-correction-issued.png index 7e198183e..d2f6caf12 100644 Binary files a/libs/integrations/infakt/docs/assets/16-correction-issued.png and b/libs/integrations/infakt/docs/assets/16-correction-issued.png differ diff --git a/libs/integrations/infakt/docs/assets/17-invoices-list.png b/libs/integrations/infakt/docs/assets/17-invoices-list.png index 9e7d072c7..b66ad21e2 100644 Binary files a/libs/integrations/infakt/docs/assets/17-invoices-list.png and b/libs/integrations/infakt/docs/assets/17-invoices-list.png differ diff --git a/libs/integrations/infakt/docs/assets/18-invoice-pdf-download.png b/libs/integrations/infakt/docs/assets/18-invoice-pdf-download.png new file mode 100644 index 000000000..547d72c9e Binary files /dev/null and b/libs/integrations/infakt/docs/assets/18-invoice-pdf-download.png differ diff --git a/libs/integrations/infakt/docs/assets/19-infakt-capability-panel.png b/libs/integrations/infakt/docs/assets/19-infakt-capability-panel.png new file mode 100644 index 000000000..01130cd24 Binary files /dev/null and b/libs/integrations/infakt/docs/assets/19-infakt-capability-panel.png differ diff --git a/libs/integrations/infakt/docs/setup-guide.md b/libs/integrations/infakt/docs/setup-guide.md new file mode 100644 index 000000000..60975529c --- /dev/null +++ b/libs/integrations/infakt/docs/setup-guide.md @@ -0,0 +1,273 @@ +# inFakt Integration Setup Guide + +OpenLinker integrates with **inFakt** (a Polish accounting SaaS) as a fiscal-document +provider: it issues invoices and reads back their KSeF clearance status through +inFakt's own, native KSeF submission. + +inFakt is the second provider of the country-agnostic **Invoicing** domain +([ADR-026](../../../../docs/architecture/adrs/026-country-agnostic-invoicing-domain.md)). Unlike +the `@openlinker/integrations-ksef` package, OL never opens a KSeF session or builds +FA(3) XML for inFakt-issued invoices — inFakt does that internally, on its own +timing. See [ADR-030](../../../../docs/architecture/adrs/030-infakt-ksef-indirection.md) for the +full rationale behind that design. + +--- + +## Capabilities + +| Capability | Supported | Notes | +|---|---|---| +| `Invoicing` (`issueInvoice` / `getInvoice` / `upsertCustomer` / `getSupportedDocumentTypes`) | ✅ | Document types: `invoice`, `corrected`, `proforma`, `prepayment`. | +| `RegulatoryStatusReader` (`getClearanceStatus`) | ✅ | Reads `ksef_data.status` off the stored invoice. **Not** `RegulatoryTransmitter` — OL does trigger submission (`send_to_ksef.json`, called inline at issuance), but clearance timing and status ownership stay with inFakt's own KSeF integration, so that trigger isn't exposed as an independently-callable submit method. | +| `RegulatoryResubmitter` (`resubmitForClearance`) | ✅ | Backs the **Resend to KSeF** action on a document whose clearance ended in `rejected` — re-hits `send_to_ksef.json` for the same document, never re-issues. | +| `CorrectionIssuer` (`issueCorrection`) | ✅ | Issues a `corrective` invoice against the dedicated `POST /corrective_invoices.json` endpoint with a before/after line-pair payload. | +| `BankAccountsReader` (`listBankAccounts`) + `BankAccountDefaultSetter` (`setDefaultBankAccount`) | ✅ | Backs the live bank-account picker for `Transfer` invoices - accounts are fetched from `GET /bank_accounts.json`, and the picked account is synced back as the inFakt default. | +| `RegulatoryDocumentReader` (`getRegulatoryDocument`, kind `rendered`) | ✅ | Fetches the inFakt-rendered invoice PDF - powers the **Download PDF** button on the invoice detail page. | +| `PaymentStatusReader` (`getPaymentStatus`) + `PaymentMarker` (`markPaid`) | ✅ | Payment-status round-trip: inbound `invoice_marked_as_paid` webhooks trigger a re-read (never trusted directly); the outbound **Mark as paid** action pushes an authoritative paid state to inFakt for orders settled elsewhere (e.g. a marketplace order). | +| `InvoiceEmailSender` (`sendByEmail`) | ✅ | Backs the **Send to buyer** action — inFakt renders and emails the already-issued invoice using the client's stored email address. | + +The connection detail page shows the enabled capability roles for the connection: + +![inFakt capability panel](./assets/19-infakt-capability-panel.png) + +- **adapterKey:** `infakt.accounting.v1` +- **platformType:** `infakt` +- **displayName:** `Infakt Accounting API v3` + +--- + +## Prerequisites + +1. An active [inFakt](https://www.infakt.pl/) account (or an inFakt **sandbox** + account for testing — the same API shape, a different `baseUrl`). +2. **KSeF integration enabled** in your inFakt account settings. OL's adapter explicitly + triggers KSeF submission right after creating each invoice — an inFakt draft does + **not** submit to KSeF on its own — but that trigger only succeeds if KSeF + integration is turned on for the account; inFakt still owns the actual clearance + session and timing once submission starts. See + [ADR-030](../../../../docs/architecture/adrs/030-infakt-ksef-indirection.md) for the full + rationale. +3. An **API key**, generated from your inFakt account settings. + +![inFakt dashboard login](./assets/if1-infakt-dashboard-login.png) + +![inFakt API key page](./assets/if2-infakt-api-key-page.png) + +--- + +## 1. Creating the connection in OL + +From **Connections → New connection**, pick **inFakt** from the platform picker. + +![Platform picker](./assets/00-platform-picker.png) + +The guided wizard (`/connections/new/infakt`) collects: + +| Field | Required | Description | +|---|---|---| +| Connection name | ✅ | A label to identify this inFakt account in OpenLinker. | +| API key | ✅ | The credential from [Prerequisites](#prerequisites). Stored encrypted server-side; never echoed back to the browser after creation. | +| Base URL (optional) | ❌ | Advanced override for sandbox testing. Must use HTTPS. Leave blank to use inFakt's production API. | +| Default payment method | ❌ | `Cash` or `Transfer` - the `payment_method` stamped on every invoice issued through this connection. Leave it untouched to fall back to `Cash`. `Transfer` is rejected (422) by inFakt unless a bank account is configured on the seller's inFakt account. | + +![inFakt wizard, empty](./assets/01-infakt-wizard-empty.png) + +![inFakt wizard, filled in](./assets/02-infakt-wizard-filled.png) + +When **Transfer** is selected, the connection also carries a **bank account for +Transfer invoices**. The picker for a specific account unlocks once the connection +exists (the API key is needed to query inFakt): OL fetches the account list live from +inFakt and defaults to whichever account is marked default there, falling back to +`Cash` if the inFakt account has no bank accounts at all. The issued document really +carries this configuration - invoices land in inFakt with `payment_method: transfer` +and the picked account's number and bank name. + +After submitting, the connection is created and a **Test connection** affordance +appears — use it to confirm the API key is valid before relying on the connection. + +![Connection created](./assets/03-infakt-connection-created.png) + +![Connection test passed](./assets/04-infakt-connection-test-ok.png) + +![Connections list with inFakt](./assets/05-connections-list-with-infakt.png) + +Both payment fields stay editable after creation. On the connection **Edit** form, +the **Payment method for invoice** disclosure exposes the same **Default payment +method** select plus a **Bank account for Transfer invoices** picker populated live +from inFakt. Picking a different account persists it eagerly and syncs it back as the +default account in inFakt, so the connection config and the inFakt account never +drift apart. + +![Edit form, payment method disclosure](./assets/06-infakt-edit-payment-section.png) + +![Edit form, changed bank account persisted](./assets/07-infakt-edit-bank-persisted.png) + +--- + +## 2. Webhook configuration + +Webhooks are the low-latency path for learning that inFakt has finished submitting an +invoice to KSeF (the "webhook = trigger, poll = reconciliation backstop" pattern — +`getClearanceStatus()` remains the source of truth; the webhook only triggers an +immediate re-read). + +**inFakt webhook subscriptions are configured entirely in the inFakt dashboard** — there +is no programmatic `WebhookProvisioningPort` for this adapter (unlike PrestaShop's +auto-provisioning). Set it up manually: + +1. In your inFakt account, go to **Settings → Webhooks** (or the equivalent + integrations/API section) and create a new subscription. +2. **URL**: `POST https:///webhooks/infakt/{connectionId}` — substitute + the connection ID shown on the connection-detail page in OL. +3. **Events**: subscribe at minimum to `send_to_ksef_success` and `send_to_ksef_error` + for clearance-status updates. Also subscribe to `invoice_marked_as_paid` (and/or + `invoice_marked_as_paid_via_async_api`) if you want OL's payment status to reflect + payments recorded directly in inFakt (#1354) — every other event inFakt can send is + accepted and silently ignored by OL. +4. inFakt sends a **verification ping** — a POST with `{"verification_code": "..."}` — + to confirm the endpoint is live. OL's webhook decoder echoes the same code back + automatically; the subscription activates once inFakt sees the matching echo. +5. **Secret**: as the subscription-creation form below shows, inFakt's "Nowy webhook" + form has **no secret field** — only the account fields (URL, description, payload + content, event checkboxes) plus your inFakt **account password**, required to + confirm the action. inFakt instead **auto-generates a secret per subscription** + after creation, visible in that subscription's details view in the inFakt + dashboard. Open the newly-created webhook's details, copy the generated secret, and + set it as OL's webhook secret for this connection so `X-Infakt-Signature` + verification matches. + +![inFakt webhooks list](./assets/if3-infakt-webhooks-list.png) + +![inFakt webhook subscription form](./assets/if4-infakt-webhook-form.png) + +> **Known gap**: there is currently no OL admin-UI affordance for webhook secrets on +> the inFakt connection page (unlike PrestaShop/Erli, which push an OL-generated +> secret out to the platform). OL's rotate endpoint only **generates a new random +> secret server-side** — there is no endpoint to set it to an arbitrary caller-supplied +> value, such as the one inFakt just generated: +> +> ```bash +> curl -X POST "https:///v1/connections/{connectionId}/webhooks/secret/rotate" \ +> -H "Authorization: Bearer " +> # => { "secret": "...", "revealedOnce": true, "warning": "..." } (shown once, copy immediately) +> ``` +> +> The returned `secret` will **not** match the one inFakt generated for the +> subscription, so signature verification fails until either an FE affordance or a +> "set secret" endpoint ships. See [Troubleshooting](#troubleshooting) for the +> resulting symptom. + +--- + +## 3. Verifying the integration + +1. Trigger an invoice issuance from OL for a test order (**Order detail → Issue + invoice**, connection set to your inFakt connection). + + ![Orders list](./assets/08-orders-list.png) + + ![Order detail, not yet issued](./assets/10-order-detail-not-issued.png) + +2. Immediately after issuance, the invoice section shows `submitted` — inFakt has + accepted the invoice and queued it for KSeF submission. + + ![Invoice issued, submitted](./assets/11-invoice-issued-submitted.png) + +3. Within roughly a minute (sandbox: ~90s observed), inFakt's own KSeF submission + completes. The webhook fires, OL re-reads the status, and the invoice section + updates to `accepted` with a clearance reference chip. + + ![Invoice accepted / cleared](./assets/12-invoice-accepted-cleared.png) + +4. Confirm the same invoice shows as KSeF-confirmed from inFakt's own side. + + ![inFakt invoice confirmed](./assets/if5-infakt-invoice-confirmed.png) + +5. The full invoice detail page renders the regulatory region alongside the rest of + the invoice. + + ![Invoice detail page](./assets/13-invoice-detail-page.png) + +6. Download the invoice PDF. On the accepted invoice detail page, the **Download + PDF** button in the KSeF clearance panel fetches the invoice as rendered by + inFakt. + + ![Download PDF on the accepted invoice](./assets/18-invoice-pdf-download.png) + +### Correcting an invoice + +Use **Issue correction** on an already-issued invoice to file a `corrective` document. +Pick the line(s) to correct and the new quantity/price. Correction deltas diff +against the invoice lines as issued (the issuance-time snapshot persisted with the +invoice record, #1297), not against the order's current state - editing the order +after issuance does not shift the correction baseline. + +![Correction dialog, empty](./assets/14-correction-modal-empty.png) + +![Correction dialog, filled in](./assets/15-correction-modal-filled.png) + +![Correction issued](./assets/16-correction-issued.png) + +![Invoices list, with correction linked to the original](./assets/17-invoices-list.png) + +### Resending a rejected invoice to KSeF + +If inFakt's own KSeF submission ends in `rejected` (e.g. the seller fixed a data +problem after the fact), use **Resend to KSeF** on the invoice detail page. This +re-triggers `send_to_ksef.json` for the same document — it never creates a second +draft — and is only available while the document's regulatory status is `rejected`. + +*(screenshot pending)* + +### Emailing an invoice to the buyer + +**Send to buyer** on an issued invoice triggers inFakt to render and email the +document to the client's stored email address (no recipient override on OL's side). +The invoice flips to `sent` on inFakt's own side once delivered. + +*(screenshot pending)* + +### Marking an invoice as paid + +For orders settled somewhere inFakt cannot see on its own — most commonly a +marketplace order the buyer paid off-platform — use **Mark as paid** on the invoice +detail page to push an authoritative paid state to inFakt (`POST +/async/invoices/{uuid}/paid.json`). This is an async operation on inFakt's side; +OL re-reads the payment status afterward via `PaymentStatusReader` to reflect it. +Re-marking an already-paid invoice is safe (no error). + +*(screenshot pending)* + +### Bulk-issuing invoices from the list + +The invoices list supports selecting multiple not-yet-issued orders and issuing them +in one action; each selected order is issued independently through the same +`issueInvoice` path used for a single order, so a failure on one order does not block +the others. + +*(screenshot pending)* + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| Connection test fails immediately | Wrong or revoked API key, or `baseUrl` pointed at the wrong environment | Re-check the API key in inFakt account settings; confirm `baseUrl` is blank (production) or the correct sandbox host. | +| Webhook deliveries return 401 | `X-Infakt-Signature` doesn't match — inFakt auto-generates the subscription's secret and OL has no endpoint to set its own secret to that same value (known gap, see [Webhook configuration](#2-webhook-configuration)) | Re-check the secret copied from inFakt's webhook-details view against whatever value was last set on the OL side; until a "set secret" endpoint or FE affordance ships, this requires a manual DB/API workaround per connection. | +| Webhook subscription never activates | The verification-ping echo didn't reach inFakt (host unreachable, TLS issue, or the connection ID in the URL is wrong) | Confirm the URL path matches `POST /webhooks/infakt/{connectionId}` exactly and the host is publicly reachable from inFakt's servers. | +| Invoice stays `submitted` forever, never reaches `accepted` | KSeF itself rejected the document after inFakt submitted it, or (less likely, since OL's `sendToKsef` call would normally fail outright at issuance time if this were disabled) KSeF integration is off in inFakt's account settings | Check inFakt's own invoice/KSeF status in its dashboard first — `ksef_data.status: error` there means inFakt attempted submission and KSeF rejected it (fix the underlying document data and re-issue). If `getClearanceStatus()` keeps returning `not-applicable` with no `ksef_data` at all, confirm KSeF integration is enabled in inFakt's account settings (the [Prerequisites](#prerequisites) requirement). | +| Rate limiting / `429` from inFakt | Sandbox and low-tier plans enforce API rate limits | Space out bulk issuance; inFakt's retry classifier (`InfaktRetryClassifierAdapter`) already treats `429` as retryable in the worker's job runner. | + +--- + +## Related documentation + +- [ADR-030](../../../../docs/architecture/adrs/030-infakt-ksef-indirection.md) — why this adapter + implements `RegulatoryStatusReader`, not `RegulatoryTransmitter` +- [ADR-026](../../../../docs/architecture/adrs/026-country-agnostic-invoicing-domain.md) — the + country-agnostic invoicing domain this provider plugs into +- [ADR-021](../../../../docs/architecture/adrs/021-third-party-native-inbound-webhook-ingestion.md) — + the inbound-webhook-decoder pattern `InfaktInboundWebhookDecoderAdapter` implements +- [`libs/integrations/infakt/README.md`](../README.md) — + package-level adapter reference (capabilities, credentials/config shape)