feat(infakt): consume invoice_marked_as_paid webhook + payment-status sync (#1354) - #1361
Conversation
… sync (#1354) The inFakt invoice_marked_as_paid (+ _via_async_api) webhook was recognized but dropped, so OL never learned when an invoice became paid. Route it onto the invoicing domain and refresh payment status via an authoritative provider re-read (never trusting the webhook body), mirroring the KSeF-status reconciliation pattern. - CORE (invoicing): neutral PaymentStatus (unknown|unpaid|partially-paid| paid) + PaymentStatusResult; paymentStatus on InvoiceRecord (+ isPaid); PaymentStatusReader sub-capability + guard; PaymentStatusRefreshService (by-id, authoritative re-read, write-on-change, graceful no-op); ORM column + repo mapping + findByProviderInvoiceId; migration. - CORE (sync): CanonicalInboundEvent domain invoice-payment; job type invoicing.paymentStatus.refreshByExternalId; InboundRoutingPolicy routes it to the refresh job, gated on Invoicing. - Worker: PaymentStatusRefreshHandler. - Integration: adapter implements PaymentStatusReader (maps inFakt status/ paid_date via authoritative GET); translator maps both payment events to invoice-payment; decoder derives externalId from the invoice uuid. Outbound InvoicePaymentMarker (mark-paid to inFakt) is deferred to a follow-up. Closes #1354 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
piotrswierzy
left a comment
There was a problem hiding this comment.
/pr-review — systematic review
Textbook-clean feature — the only thing standing between it and merge is a cross-PR migration-timestamp collision.
🔴 BLOCKING — 1818000000004 collides with PR #1341
Both PRs claim the same 13-digit prefix on an invoice_records migration:
- this PR:
1818000000004-add-invoice-payment-status.ts - #1341 (open, closes #1338):
1818000000004-backfill-ksef-provider-invoice-number.ts
That's the #374 collision class (docs/migrations.md § Timestamp uniqueness invariant). The class names differ so there's no duplicate-migrations-row hazard, but the shared prefix violates the uniqueness + strictly-greater invariants. check-migration-timestamps.mjs compares each branch only against origin/main, so both pass lint in isolation and the second-to-merge fails pnpm lint. Latest prefix on main is 1818000000003, so …004 is otherwise the correct next step. Fix: whichever lands second re-prefixes to 1818000000005 + updates its class suffix — recommend this PR pre-empt to 1818000000005, since #1341 is a smaller self-contained bugfix. (I've flagged the same on #1341.)
✅ Everything else verified clean
- Migration safety:
up()/down()present;ADD COLUMN IF NOT EXISTS "paymentStatus" text NOT NULL DEFAULT 'unknown'— idempotent, metadata-only in PG11+, existing rows backfill tounknown(never falselyunpaid); ORM@Columndefault matches the DDL (nomigration:showdrift). - ADR-026:
PaymentStatusValues(unknown|unpaid|partially-paid|paid),PaymentStatusResult,PaymentStatusReader, and the syncinvoice-paymentdomain carry zero inFakt/PL vocabulary — allstatus/paid_datemapping confined totoPaymentStatus()in the adapter. - Authoritative re-read:
refreshByExternalIdre-reads viaadapter.getPaymentStatus(record)and never touches the webhook body (payload carries onlyexternalInvoiceId); write-on-change only; graceful no-ops (unsupportedwarn when!isPaymentStatusReader,not-foundwhen no record) — only a transport failure propagates for retry. Mirrors the webhook=trigger / poll=source-of-truth principle and theRegulatoryStatusReaderprecedent. - Sub-capability + routing:
payment-status-reader.capability.ts+ co-locatedisPaymentStatusReaderguard, barrel-exported;InboundRoutingPolicygatesinvoice-paymenton capabilityInvoicing(not platformType), switch stays exhaustive; runtime-detected (correctly not insupportedCapabilities), so no manifest/routing-int-spec ripple. - Layers/types: no
any; service implementsIPaymentStatusRefreshServicein a separate interface file; Symbol token ininvoicing.tokens.ts(export *'d); worker handler registered in both the module andhandler-registration.service.ts; cross-context imports barrel-only;findByProviderInvoiceIdbacked by the pre-existing partial index, newest-first. - Tests: colocated and branch-complete (refresh-service updated/unchanged/not-found/unsupported; adapter mapping + 503 propagation; decoder; translator both events + dead-letter).
✅ Positives
Clean orthogonal modelling — paymentStatus is separate from issuance status and regulatoryStatus, and invoice-payment is deliberately split from the invoicing domain so a paid document nudges the by-id refresh rather than the regulatory sweep. Fiscal-safe unknown default applied consistently across type doc, entity, ORM, and migration.
Verdict: 🔄 Approve with changes — re-prefix the migration to 1818000000005 to clear the #1341 collision; the code is otherwise merge-ready.
Re-prefix the backfill migration from 1818000000004 to 1818000000005 - PR #1361 also claims 1818000000004 for a different invoice_records migration, and TypeORM 0.3.17 has no tie-breaker for a shared prefix. Per docs/migrations.md's ordering rule, whichever PR lands second re-prefixes; doing it here avoids leaving the collision to be caught reactively by CI after one of the two merges. Addresses Piotr's review on #1341. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Resolves the migration-timestamp collision flagged in review: PR #1341 (closes #1338) already claims the 1818000000004 prefix on an invoice_records migration. Re-prefix this PR's migration to the next free slot (1818000000005) so both branches satisfy the timestamp uniqueness + strictly-greater invariant regardless of merge order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
|
Thanks for the review, @piotrswierzy. All findings addressed - in this case the single 🔴 BLOCKING item (there were no inline comments or open suggestions to fold in): 🔴 Migration-timestamp collision with #1341 - fixed ( Re-prefixed the migration
Verification:
No functional change - |
/tech-review - independent passIndependent tech-lead review of PR #1361, formed from scratch against the diff and the OpenLinker docs. Not an echo of any prior review. SummarySolid, well-scoped work. The inbound-half implementation is faithful to the documented patterns: a distinct Issues[SUGGESTION] -
[SUGGESTION] -
[SUGGESTION] -
[SUGGESTION] -
Notes verified (no action)
Verdict✅ Approve - ready to merge. All open items are SUGGESTIONs; none block. If any is worth acting on, the handler unit spec is the highest-value follow-up. |
… classifier Address tech-review SUGGESTIONs on PR #1361: - Add unit spec for PaymentStatusRefreshHandler pinning the getPayload reject branches (missing/invalid payload, wrong schemaVersion, empty externalInvoiceId), the success delegation, and the OL-shaped error wrapping. Mirrors regulatory-status-reconcile.handler.spec.ts. - Make toPaymentStatus classify against a documented, known inFakt token set (verified against the invoice_statuses + payment_statuses meta dictionaries) instead of a bare === 'paid' check; behaviour unchanged. Extend the adapter spec with partial_payment / draft / printed cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Tech-review follow-up (all 4 SUGGESTIONs addressed)Thanks for the pass. Addressed in Fixed#1 - Missing handler unit spec (
9 tests, all green. #4 -
Behaviour is unchanged ( Deliberately kept (with reasons)#2 - #3 - result types in Checks (scoped to touched packages)
No migrations touched. |
piotrswierzy
left a comment
There was a problem hiding this comment.
/pr-review — delta re-review (9b1cac40 → 4f309826)
The non-migration delta is straight approve-quality. The sole remaining blocker is the migration collision — which moved but wasn't resolved.
🔴 CROSS-PR BLOCKING — the …004 collision became a …005 collision (still unresolved)
The migration was re-prefixed 1818000000004 → 1818000000005 (class + name both → AddInvoicePaymentStatus1818000000005) — but #1341 also independently re-prefixed to 1818000000005. So both now share …005 (and …004 is an unused gap). origin/main contains neither, so it's invisible in each isolated diff, but the second-to-merge still fails pnpm lint. Fix (needs coordination with #1341): exactly one PR owns …005; the other takes a distinct slot — …004 is now free and is the correct next step after main's …003 tail. Flagged the same on #1341.
✅ Non-migration delta — approve-quality
- Adapter (+27): a behavior-preserving refactor, not a correctness change —
status === 'paid'→INFAKT_PAID_TOKENS.includes(status)(tokens['paid'], identical), the partial branch →INFAKT_PARTIAL_TOKENS.some(...)(logically identical to the priorincludes('partial')||includes('partly')),paid_datefallback +unpaiddefault unchanged. Now driven by named constants + a doc comment citing the verified live inFakt dictionaries. Neutrality holds — inFakt tokens stay in the adapter; only the neutralPaymentStatuscrosses to core;getPaymentStatusstill returnsunknownwhenproviderInvoiceIdis absent and never trusts a webhook body. - New
payment-status-refresh.handler.spec.ts(+91): real handler test, colocated, AAA — 7it.eachpayload-validation branches all assertSyncJobExecutionErrorand the service is never called; success asserts delegation torefreshByExternalId('conn-1','inv-1')+{outcome:'ok'}; error asserts OL-shaped wrapping withjobId/jobType/connectionId. Correctly leaves the unsupported/not-found branching to the refresh service's own spec (the handler is a thin delegate). Closes the worker-handler test gap. - Adapter spec (+5): exercises the new token constants (
partial_payment → partially-paid,draft/printed → unpaid).
🟢 Minor (non-blocking, pre-existing)
The doc comment attributes partial_payment to inFakt's payment_statuses dictionary, but toPaymentStatus reads invoice.status (dictionary draft|sent|printed|paid, no partial token), and InfaktInvoice carries only a single status field — so the partial branch may be unreachable against real invoice.status data. It's fiscally conservative (partial safer than a false "paid") and unchanged from the prior head — worth a note, not a gate.
✅ No regressions
Authoritative re-read still never trusts the webhook body; write-on-change owned by the (untouched) service; sub-capability PaymentStatusReader + isPaymentStatusReader guard barrel-exported; fiscal-safe unknown default preserved; no any. Migration body still idempotent (ADD COLUMN IF NOT EXISTS … DEFAULT 'unknown') with a symmetric down().
Verdict: 🔄 Approve with changes — resolve the 1818000000005 collision with #1341 (one PR takes a distinct slot, e.g. …004); everything else is merge-ready.
…ct with #1361 Both #1341 and #1361 landed on 1818000000005, sharing the same TypeORM timestamp prefix on invoice_records (the #374 collision class). Move this PR's backfill to the free 1818000000004 slot — the correct next step after main's 1818000000003 tail — leaving 1818000000005 uniquely to #1361. Updates the class-name suffix and the controller comment that references the filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Migration timestamp collision with #1341 — resolved on the #1341 sideHeads-up on the I've re-prefixed #1341's backfill down to the free |
…rd (#1354) Piotr's re-review flagged that the token doc attributed `partial_payment` to the field `toPaymentStatus` reads, but that function reads `InfaktInvoice.status` (the `invoice_statuses` dictionary: draft|sent|printed|paid — no partial token), so the partial branch is unreachable against real `status` data. Clarify in the comment that the `partial`/`partly` match is an intentional forward-looking guard against future vocabulary drift, not a currently-reachable classification. Comment-only; behavior unchanged (fiscally conservative). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
🟢 Minor doc finding addressed (
|
piotrswierzy
left a comment
There was a problem hiding this comment.
/pr-review — delta re-review (4f309826 → 5aac870c)
The migration collision is resolved — the sole remaining blocker. Approving.
Verified locally: this PR keeps 1818000000005-add-invoice-payment-status.ts and #1341 moved to 1818000000004 — two distinct, sequential prefixes above main's 1818000000003 tail. No more collision.
The only author change since my last review (cutting through the merge-of-main noise from #1284/#1342/#1335 landing on main) is one commit — docs(infakt): clarify partial-payment branch is a forward-looking guard — which addresses my minor note that the partial mapping branch may be unreachable against real invoice.status data by documenting it as an intentional forward-looking guard. Good.
Everything else was already verified clean at the prior head: authoritative re-read (never trusts the webhook body), write-on-change only, PaymentStatusReader sub-capability + barrel-exported guard, routing gated on Invoicing capability, fiscal-safe unknown default, the 91-line handler spec, idempotent ADD COLUMN IF NOT EXISTS … DEFAULT 'unknown' migration with a symmetric down(), no any.
Verdict: ✅ Approve.
…ord (#1341) * fix(ksef): persist FA(3) P_2 document number on the issued InvoiceRecord KsefInvoicingAdapter.issueInvoice stamped the FA(3) P_2 number into the XML (invoiceNumber: cmd.orderId) but returned the InvoiceRecord with providerInvoiceNumber = null, and nothing ever backfilled it. Since the correction precondition landed (#1289), every KSeF KOR was rejected with "missing document number / issue date" even for fully cleared invoices. Persist the same P_2 value on the record. Verified live on the KSeF test environment during the 2026-07-03 E2E run: with this change (plus a backfill for pre-existing records) the full KOR flow issues and clears. Existing rows need a one-off backfill, e.g.: update invoice_records set "providerInvoiceNumber" = "orderId" where "providerType" = 'ksef' and "providerInvoiceNumber" is null; Closes #1338 Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * refactor(ksef): single documentNumber source for FA(3) P_2 stamp and persisted record Review follow-up on #1341: hoist the P_2 value into one const consumed by both the FA(3) builder input and the InvoiceRecord constructor, so the persisted providerInvoiceNumber can never drift from the number stamped in the XML when the #1118 sequential-numbering follow-up replaces the orderId placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * test(ksef): assert providerInvoiceNumber on correction record; clarify 422 for legacy rows Review follow-up on #1341: - Add an explicit providerInvoiceNumber assertion to the issueCorrection happy-path test. The correction path is the primary consumer of the #1289 precondition #1338 unblocks; guarding it directly (not only via the issueInvoice delegation) protects against a future refactor that special-cases correction-record construction. - Extend the correction 422 message so an operator hitting a pre-fix KSeF row (fully issued/cleared, but null providerInvoiceNumber) gets a signal that a one-off backfill is needed, instead of the misleading 'may not be fully issued yet' alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): ship provider invoice number backfill as an idempotent migration Review follow-up on #1341 (piotrswierzy): a manual deployment-note SQL step is easy to forget and, as flagged, easy to get wrong without care around the camelCase column quoting. Ship it as a committed, idempotent TypeORM migration instead, per docs/migrations.md — it runs automatically via migration:run, is reviewable, and the IS NULL guard makes re-running it a no-op. Scoped to status = 'issued' so failed/ pending rows (which legitimately have no stamped P_2) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(ksef): flag correction P_2 collision surfaced by #1338's persistence fix Review follow-up on #1341 (piotrswierzy): issueCorrection delegates to issueInvoice with the same orderId, so a KOR stamps the same P_2 as the original document it corrects - invalid for FA(3), where P_2 must be unique per document. Pre-existing (the orderId-as-P_2 placeholder predates this fix) but now visible since providerInvoiceNumber is actually persisted and read by the correction precondition. Left as an explicit code note pending the real per-seller sequential FA(3) numbering source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): resolve migration-timestamp collision with #1361 Re-prefix the backfill migration from 1818000000004 to 1818000000005 - PR #1361 also claims 1818000000004 for a different invoice_records migration, and TypeORM 0.3.17 has no tie-breaker for a shared prefix. Per docs/migrations.md's ordering rule, whichever PR lands second re-prefixes; doing it here avoids leaving the collision to be caught reactively by CI after one of the two merges. Addresses Piotr's review on #1341. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef,api): address independent tech-review suggestions on #1341 Move the internal issue reference out of the operator-facing UnprocessableEntityException message in the correction precondition check (moved into a code comment instead, matching the existing message-style convention elsewhere in the controller family). File and cross-reference #1364 as the dedicated tracking issue for the KOR-shares-original-document-number FA(3) semantics gap flagged in the issueCorrection NOTE comment, so it's discoverable going forward. No action needed on the migration down() no-op per reviewer. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): re-prefix backfill migration to 1818000000004 to de-conflict with #1361 Both #1341 and #1361 landed on 1818000000005, sharing the same TypeORM timestamp prefix on invoice_records (the #374 collision class). Move this PR's backfill to the free 1818000000004 slot — the correct next step after main's 1818000000003 tail — leaving 1818000000005 uniquely to #1361. Updates the class-name suffix and the controller comment that references the filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> --------- Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
@norbert-kulus-blockydevs resolve conflicts |
…-paid-webhook Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> # Conflicts: # libs/integrations/infakt/src/infrastructure/adapters/__tests__/infakt-invoicing.adapter.spec.ts # libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts
…the 4 sub-capabilities shipped since #1307's last update InfaktInvoicingAdapter now implements 10 Invoicing sub-capabilities; this branch's docs only covered 6 (missing RegulatoryResubmitter #1356, PaymentStatusReader #1354, PaymentMarker #1362, InvoiceEmailSender #1353). Also fixes a since-stale claim that invoice_marked_as_paid webhooks are ignored (they now drive PaymentStatusReader via #1354/#1361), documents the dedicated corrective_invoices.json endpoint (#1342), and adds the per-connection shipping-line label override (#1517/#1562). docs/capabilities.md and architecture-overview.md's InvoicingPort sub-capability list had the same 6-of-10 staleness independent of this branch - fixed alongside since they're the same underlying gap. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
…ord (#1341) * fix(ksef): persist FA(3) P_2 document number on the issued InvoiceRecord KsefInvoicingAdapter.issueInvoice stamped the FA(3) P_2 number into the XML (invoiceNumber: cmd.orderId) but returned the InvoiceRecord with providerInvoiceNumber = null, and nothing ever backfilled it. Since the correction precondition landed (#1289), every KSeF KOR was rejected with "missing document number / issue date" even for fully cleared invoices. Persist the same P_2 value on the record. Verified live on the KSeF test environment during the 2026-07-03 E2E run: with this change (plus a backfill for pre-existing records) the full KOR flow issues and clears. Existing rows need a one-off backfill, e.g.: update invoice_records set "providerInvoiceNumber" = "orderId" where "providerType" = 'ksef' and "providerInvoiceNumber" is null; Closes #1338 Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * refactor(ksef): single documentNumber source for FA(3) P_2 stamp and persisted record Review follow-up on #1341: hoist the P_2 value into one const consumed by both the FA(3) builder input and the InvoiceRecord constructor, so the persisted providerInvoiceNumber can never drift from the number stamped in the XML when the #1118 sequential-numbering follow-up replaces the orderId placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * test(ksef): assert providerInvoiceNumber on correction record; clarify 422 for legacy rows Review follow-up on #1341: - Add an explicit providerInvoiceNumber assertion to the issueCorrection happy-path test. The correction path is the primary consumer of the #1289 precondition #1338 unblocks; guarding it directly (not only via the issueInvoice delegation) protects against a future refactor that special-cases correction-record construction. - Extend the correction 422 message so an operator hitting a pre-fix KSeF row (fully issued/cleared, but null providerInvoiceNumber) gets a signal that a one-off backfill is needed, instead of the misleading 'may not be fully issued yet' alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): ship provider invoice number backfill as an idempotent migration Review follow-up on #1341 (piotrswierzy): a manual deployment-note SQL step is easy to forget and, as flagged, easy to get wrong without care around the camelCase column quoting. Ship it as a committed, idempotent TypeORM migration instead, per docs/migrations.md — it runs automatically via migration:run, is reviewable, and the IS NULL guard makes re-running it a no-op. Scoped to status = 'issued' so failed/ pending rows (which legitimately have no stamped P_2) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(ksef): flag correction P_2 collision surfaced by #1338's persistence fix Review follow-up on #1341 (piotrswierzy): issueCorrection delegates to issueInvoice with the same orderId, so a KOR stamps the same P_2 as the original document it corrects - invalid for FA(3), where P_2 must be unique per document. Pre-existing (the orderId-as-P_2 placeholder predates this fix) but now visible since providerInvoiceNumber is actually persisted and read by the correction precondition. Left as an explicit code note pending the real per-seller sequential FA(3) numbering source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): resolve migration-timestamp collision with #1361 Re-prefix the backfill migration from 1818000000004 to 1818000000005 - PR #1361 also claims 1818000000004 for a different invoice_records migration, and TypeORM 0.3.17 has no tie-breaker for a shared prefix. Per docs/migrations.md's ordering rule, whichever PR lands second re-prefixes; doing it here avoids leaving the collision to be caught reactively by CI after one of the two merges. Addresses Piotr's review on #1341. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef,api): address independent tech-review suggestions on #1341 Move the internal issue reference out of the operator-facing UnprocessableEntityException message in the correction precondition check (moved into a code comment instead, matching the existing message-style convention elsewhere in the controller family). File and cross-reference #1364 as the dedicated tracking issue for the KOR-shares-original-document-number FA(3) semantics gap flagged in the issueCorrection NOTE comment, so it's discoverable going forward. No action needed on the migration down() no-op per reviewer. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(ksef): re-prefix backfill migration to 1818000000004 to de-conflict with #1361 Both #1341 and #1361 landed on 1818000000005, sharing the same TypeORM timestamp prefix on invoice_records (the #374 collision class). Move this PR's backfill to the free 1818000000004 slot — the correct next step after main's 1818000000003 tail — leaving 1818000000005 uniquely to #1361. Updates the class-name suffix and the controller comment that references the filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> --------- Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… sync (#1354) (#1361) * feat(infakt): consume invoice_marked_as_paid webhook + payment-status sync (#1354) The inFakt invoice_marked_as_paid (+ _via_async_api) webhook was recognized but dropped, so OL never learned when an invoice became paid. Route it onto the invoicing domain and refresh payment status via an authoritative provider re-read (never trusting the webhook body), mirroring the KSeF-status reconciliation pattern. - CORE (invoicing): neutral PaymentStatus (unknown|unpaid|partially-paid| paid) + PaymentStatusResult; paymentStatus on InvoiceRecord (+ isPaid); PaymentStatusReader sub-capability + guard; PaymentStatusRefreshService (by-id, authoritative re-read, write-on-change, graceful no-op); ORM column + repo mapping + findByProviderInvoiceId; migration. - CORE (sync): CanonicalInboundEvent domain invoice-payment; job type invoicing.paymentStatus.refreshByExternalId; InboundRoutingPolicy routes it to the refresh job, gated on Invoicing. - Worker: PaymentStatusRefreshHandler. - Integration: adapter implements PaymentStatusReader (maps inFakt status/ paid_date via authoritative GET); translator maps both payment events to invoice-payment; decoder derives externalId from the invoice uuid. Outbound InvoicePaymentMarker (mark-paid to inFakt) is deferred to a follow-up. Closes #1354 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(infakt): re-prefix payment-status migration to 1818000000005 (#1354) Resolves the migration-timestamp collision flagged in review: PR #1341 (closes #1338) already claims the 1818000000004 prefix on an invoice_records migration. Re-prefix this PR's migration to the next free slot (1818000000005) so both branches satisfy the timestamp uniqueness + strictly-greater invariant regardless of merge order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * test(infakt): add payment-refresh handler spec, harden inFakt payment classifier Address tech-review SUGGESTIONs on PR #1361: - Add unit spec for PaymentStatusRefreshHandler pinning the getPayload reject branches (missing/invalid payload, wrong schemaVersion, empty externalInvoiceId), the success delegation, and the OL-shaped error wrapping. Mirrors regulatory-status-reconcile.handler.spec.ts. - Make toPaymentStatus classify against a documented, known inFakt token set (verified against the invoice_statuses + payment_statuses meta dictionaries) instead of a bare === 'paid' check; behaviour unchanged. Extend the adapter spec with partial_payment / draft / printed cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): clarify partial-payment branch is a forward-looking guard (#1354) Piotr's re-review flagged that the token doc attributed `partial_payment` to the field `toPaymentStatus` reads, but that function reads `InfaktInvoice.status` (the `invoice_statuses` dictionary: draft|sent|printed|paid — no partial token), so the partial branch is unreachable against real `status` data. Clarify in the comment that the `partial`/`partly` match is an intentional forward-looking guard against future vocabulary drift, not a currently-reachable classification. Comment-only; behavior unchanged (fiscally conservative). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> --------- Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the 4 sub-capabilities shipped since #1307's last update InfaktInvoicingAdapter now implements 10 Invoicing sub-capabilities; this branch's docs only covered 6 (missing RegulatoryResubmitter #1356, PaymentStatusReader #1354, PaymentMarker #1362, InvoiceEmailSender #1353). Also fixes a since-stale claim that invoice_marked_as_paid webhooks are ignored (they now drive PaymentStatusReader via #1354/#1361), documents the dedicated corrective_invoices.json endpoint (#1342), and adds the per-connection shipping-line label override (#1517/#1562). docs/capabilities.md and architecture-overview.md's InvoicingPort sub-capability list had the same 6-of-10 staleness independent of this branch - fixed alongside since they're the same underlying gap. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
…the 4 sub-capabilities shipped since #1307's last update InfaktInvoicingAdapter now implements 10 Invoicing sub-capabilities; this branch's docs only covered 6 (missing RegulatoryResubmitter #1356, PaymentStatusReader #1354, PaymentMarker #1362, InvoiceEmailSender #1353). Also fixes a since-stale claim that invoice_marked_as_paid webhooks are ignored (they now drive PaymentStatusReader via #1354/#1361), documents the dedicated corrective_invoices.json endpoint (#1342), and adds the per-connection shipping-line label override (#1517/#1562). docs/capabilities.md and architecture-overview.md's InvoicingPort sub-capability list had the same 6-of-10 staleness independent of this branch - fixed alongside since they're the same underlying gap. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
…1307) * docs(infakt): add first 4 manual inFakt-dashboard walkthrough screenshots Part of the E2E evidence trail for PR #1300 / issue #1282: inFakt sandbox dashboard login, API key generation page, and the webhook creation flow (list + new-webhook form). Captured manually against the real inFakt sandbox dashboard, no secrets visible in frame. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): add real inFakt E2E screenshots (connection, issuance, KOR, list) Captured against the real inFakt sandbox on a temporary paired stack (this branch's web build + the 1281-infakt-plugin-registration-webhook backend branch), confirming the full connect -> issue -> KSeF-clearance -> correction flow works end to end through OpenLinker: - 00-05: guided connection setup, Test connection (passing, after the ConnectionTesterPort fix landed on PR #1293), connections list - 12: invoice accepted with real KSeF clearance number (8201194127-20260701-A5797F400000-ED) - 14-16: KOR correction flow, also cleared by the real sandbox - 17: invoices list showing the original + correction, both accepted - if1-if4: manual inFakt-dashboard screenshots (API key, webhooks) Known gaps (tracked, not blocking): - 13-invoice-detail-page was captured via the /invoices/:invoiceId page, but that GET route doesn't exist yet on the 1281 backend branch's base main snapshot — will be trivial to recapture once #1292/#1293 land on current main. - The not-issued / submitted (pending) order-detail states aren't captured cleanly yet — the seeded test order is now terminal (accepted) for this connection; a second seeded order would be needed for a clean before/during capture. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): drop broken invoice-detail-page screenshot (route missing on this backend branch, not blocking) Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): re-capture E2E screenshots with the money-format fix verified Re-ran the full sandbox walkthrough on a fresh order (PLN 189.00) after the groszy/decimal-string fix landed on PR #1293 (651cac2). Confirmed against inFakt's raw API response that amounts now round-trip exactly (gross_price: 18900 groszy = PLN 189.00) and the KOR correction combines original + corrected lines correctly (189.00 + 99.99 = 288.99 PLN). Also adds the invoice-detail-page screenshot (13) that 404'd on a stale branch snapshot last time — the route works fine now. Replaces the earlier PLN 349.00 test order's screenshots, which (correctly, at the time) showed the ~100x-low amounts that led to the money-format bug report on PR #1293. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): move E2E screenshots to libs/integrations/infakt/docs/assets/ Aligns with the convention established in PR #1284 (KSeF/Subiekt tutorials) — screenshot assets live inside the integration package's own docs/assets/, not a root-level docs/assets/<provider>/ directory. Updates the two e2e scripts' output path accordingly. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): add clean not-issued/submitted screenshots + fresh Part 6 confirmation - 08-orders-list.png: clean, filtered (sourceConnectionId + createdFrom) view of just the seeded test orders — the unfiltered list is too cluttered with other sessions' dev-DB fixtures to be tutorial-usable. - 10/11: genuinely clean not-issued -> submitted transition, captured without a page reload wiping the connection-picker selection (fixed infakt-invoice.mjs to shoot the submitted state before any reload). - if5-infakt-invoice-confirmed.png: fresh manual inFakt-dashboard screenshot confirming the money-format fix (9/07/2026 = PLN 189.00, 10/07/2026 correction = PLN 288.99), replacing the pre-fix evidence. - 13/17 updated: also documents the KSeF cleared-vs-accepted mapping bug found this run (flagged on PR #1293) — two rows show "KSeF: CLEARING" (the real reconcile job's output, unpatched) alongside two "KSeF: ACCEPTED" rows from earlier runs where I'd manually corrected the DB while chasing the money bug. Will re-capture once the accepted-mapping fix lands. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): re-capture screenshots with the cleared->accepted fix verified Fourth fresh sandbox order (PLN 259.00), confirming both backend fixes together: gross_price round-trips exactly (25900 groszy = PLN 259.00), and the invoice now shows "KSeF: ACCEPTED" with a real clearance reference chip instead of getting stuck on "KSeF: CLEARING" forever. Correction verified too (259.00 + 99.99 = 358.99 PLN exact, gross_price 35899 groszy). Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): ADR-030 KSeF indirection model + operator setup guide Documents why InfaktInvoicingAdapter implements RegulatoryStatusReader (not RegulatoryTransmitter) — inFakt auto-submits to KSeF on its own, so OL only ever reads clearance status back. Adds the operator setup guide (connection creation, webhook configuration, troubleshooting), the package README the infakt adapter was missing, and the architecture-overview.md provider entry. Part of #1279. Closes #1283. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): payment method, bank-account picker, PDF download in setup guide Live-E2E-verified against the inFakt sandbox (2026-07-03): - section 1: Default payment method wizard field + Transfer bank-account behaviour (live picker on the Edit form, eager persist, inFakt default sync) with fresh wizard + edit-form screenshots - section 3: Download the invoice PDF step (rendered PDF via RegulatoryDocumentReader, #1321) - corrections: issuance-time line snapshot note (#1297) - README: BankAccountsReader / BankAccountDefaultSetter / RegulatoryDocumentReader notes + implementation details - capability-panel screenshot (post-#1320) Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): address #1307 review - accepted mapping, webhook-secret reality check - ADR-030: fix ksef_data.status mapping (success -> accepted, not cleared) - the ADR was re-canonizing exactly the bug the shipped adapter comment warns against - bank-picker docs (BankAccountsReader/BankAccountDefaultSetter, #1310) now describe a MERGED feature - #1310 landed to main 2026-07-03, after Piotr's review; no doc changes needed, kept as-is post branch update - setup-guide step 5: replace the nonexistent "Rotate webhook secret" FE button with the actual API call (curl snippet against POST /v1/connections/:id/webhooks/secret/rotate); invert the secret-exchange framing to lead with what if4's screenshot actually shows (no secret field) and mark the paste-into-inFakt direction explicitly unverified - README: add defaultPaymentMethod + bankAccount to the Config JSON example (InfaktConnectionConfig carries both since #1309/#1310) - plan: retarget addendum noting the branch now targets main with all prereqs merged - merged origin/main (branch was behind #1297/#1329/#1310/#1320/#1331) Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * fix(docs): fix inFakt ADR-030's misleading no-submit-primitive framing The earlier review-fix commit (999dda5) addressed the /pr-review's four numbered findings but missed the tech-lead draft review's BLOCKING finding: ADR-030, the setup guide, and the README all claimed OL "has no submit primitive to call" and that inFakt "auto-triggers" KSeF submission on its own. The shipped adapter's own docstring says otherwise: an inFakt draft does NOT auto-submit on its own, so issueInvoice/issueCorrection call send_to_ksef.json explicitly and inline - verified live 2026-07-01. Reworded throughout to the framing the reviewer suggested: OL retains an out-of-port sendToKsef trigger, not surfaced as RegulatoryTransmitter, because clearance timing and status ownership stay with inFakt - not because there is nothing for OL to call. Also disambiguates the three webhook classes in the README (InfaktWebhookTranslator, InfaktInboundWebhookDecoderAdapter, InfaktWebhookEventTranslatorAdapter), per the same draft review's suggestion, which the prior commit also left unaddressed. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): move setup-guide.md to libs/integrations/infakt/docs/ for convention parity Every other integration (allegro, dpd-polska, erli, inpost, ksef, subiekt, woocommerce) already lives at libs/integrations/<name>/docs/setup-guide.md after the recent doc-location convention change on main. inFakt's guide had already moved its screenshots there but left the markdown file behind at the old top-level docs/integrations/infakt/ location. Move the file alongside its assets and fix every cross-reference (ADR-030, package README, architecture-overview.md). Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): catch up ADR-030/README/setup-guide/capabilities.md to the 4 sub-capabilities shipped since #1307's last update InfaktInvoicingAdapter now implements 10 Invoicing sub-capabilities; this branch's docs only covered 6 (missing RegulatoryResubmitter #1356, PaymentStatusReader #1354, PaymentMarker #1362, InvoiceEmailSender #1353). Also fixes a since-stale claim that invoice_marked_as_paid webhooks are ignored (they now drive PaymentStatusReader via #1354/#1361), documents the dedicated corrective_invoices.json endpoint (#1342), and adds the per-connection shipping-line label override (#1517/#1562). docs/capabilities.md and architecture-overview.md's InvoicingPort sub-capability list had the same 6-of-10 staleness independent of this branch - fixed alongside since they're the same underlying gap. Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(infakt): correct webhook-secret injection to env-var mechanism The webhook-secret exchange direction is now confirmed by the merged ingestion integration test (#1555): inFakt auto-generates the secret per subscription and the operator injects it into OL via OPENLINKER_WEBHOOK_SECRET__INFAKT[__<CONNECTION_ID>]. The secret/rotate endpoint is the wrong tool (generates a random secret inFakt never sees). Removes the unverified "known gap" framing; keeps the accurate residual limitation (no set-arbitrary-secret endpoint / FE affordance yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> * docs(invoicing): document DocumentNumberConsumer capability + note ADR-030 roster - capabilities.md: add the DocumentNumberConsumer (#1575) invoicing sub-capability row and bump the InvoicingPort count 12 -> 13 to match the 13 capability files on disk (the row was missing on main too; this PR owns the count since it re-tallied the section). - ADR-030: add a scope note that the adapter accreted further sub-capabilities post-decision, pointing at the README / architecture -overview for the full code-synced roster instead of restating it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> --------- Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com> Signed-off-by: Peter Swierzy <123735851+piotrswierzy@users.noreply.github.com> Co-authored-by: Peter Swierzy <123735851+piotrswierzy@users.noreply.github.com>
What changed
The inFakt
invoice_marked_as_paid(+_via_async_api) webhook was recognized but dropped, so OL never learned when an invoice became paid. This routes it onto the invoicing domain and refreshes payment status via an authoritative provider re-read (never trusting the webhook body), mirroring the KSeF-status reconciliation pattern.PaymentStatus(unknown|unpaid|partially-paid|paid) +PaymentStatusResult;paymentStatusonInvoiceRecord(+isPaid);PaymentStatusReadersub-capability + guard;PaymentStatusRefreshService(by-id, authoritative re-read, write-on-change, graceful no-op); ORM column + repo mapping +findByProviderInvoiceId; migration1818000000004-add-invoice-payment-status.ts.CanonicalInboundEventdomaininvoice-payment; job typeinvoicing.paymentStatus.refreshByExternalId;InboundRoutingPolicyroutes it to the refresh job, gated onInvoicing.PaymentStatusRefreshHandler.PaymentStatusReader(maps inFaktstatus/paid_datevia authoritative GET); translator maps both payment events toinvoice-payment; decoder derivesexternalIdfrom the invoiceuuid.How to test
pnpm --filter @openlinker/core test(1450 pass),pnpm --filter @openlinker/integrations-infakt test(153 pass) - translator/routing/refresh-service/adapter/decoder specs.pnpm --filter @openlinker/api migration:runon a test DB to apply thepaymentStatuscolumn.Scope
InvoicePaymentMarker->POST /async/invoices/{uuid}/paid.json+ endpoint/UI) is deferred; follow-up filed and linked below.Closes #1354
🤖 Generated with Claude Code