Skip to content

feat(infakt): register Infakt plugin in API/worker + wire webhook ingestion#1293

Merged
norbert-kulus-blockydevs merged 12 commits into
mainfrom
1281-infakt-plugin-registration-webhook
Jul 2, 2026
Merged

feat(infakt): register Infakt plugin in API/worker + wire webhook ingestion#1293
norbert-kulus-blockydevs merged 12 commits into
mainfrom
1281-infakt-plugin-registration-webhook

Conversation

@norbert-kulus-blockydevs

@norbert-kulus-blockydevs norbert-kulus-blockydevs commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #1292 (#1280, not yet merged) - this branch is based on 1280-infakt-plugin-hardening-tests so it carries that PR's diff until it merges. Opened as draft for that reason.

Part of the Infakt epic (#1279). Closes #1281.

  • Registers InfaktIntegrationModule in apps/api/src/plugins.ts and apps/worker/src/plugins.ts (mirrors the KSeF/Subiekt dual-registration pattern) so the host can resolve the Invoicing capability for Infakt connections
  • Adds @openlinker/integrations-infakt to apps/api/apps/worker package.json, and the jest-integration moduleNameMapper entries
  • Wires Infakt's third-party-native webhook (KSeF clearance relay) into OL's ingestion pipeline per ADR-021/ADR-015:
    • InfaktInboundWebhookDecoderAdapter (provider-keyed infakt): HMAC-SHA256 signature verify (no timestamp header, so no replay-window check for this provider), subscription-verification handshake detection, envelope extraction — wraps the already-tested InfaktWebhookTranslator from the feat(infakt): production-harden Infakt plugin – validators, retry classifier, NestJS module, unit tests #1280 POC rather than duplicating its crypto/parsing
    • InfaktWebhookEventTranslatorAdapter (adapterKey-keyed infakt.accounting.v1): maps send_to_ksef_success / send_to_ksef_error to the new invoicing inbound domain; other Infakt events dead-letter (null)
  • Extends InboundWebhookDecoderPort with an optional detectHandshake step (runs before verify) so a provider's subscription-verification ping can echo a body back — WebhookService/WebhookController now thread a return value through instead of always resolving void. This is additive; DefaultWebhookDecoder and existing decoders (InPost) are unaffected.
  • Adds 'invoicing' to the closed InboundEventDomainValues union and a new InboundRoutingPolicyService case: an invoicing webhook event enqueues the existing invoicing.regulatoryStatus.reconcile job (gated on the Invoicing capability). No new by-id job — the webhook is a trigger, not the source of truth (mirrors the InPost/webhook philosophy already documented in architecture-overview.md); the scheduled reconcile still drains the full non-terminal frontier regardless.

No WebhookProvisioningPort (Infakt webhook setup is UI-only, per issue scope).

Scope note: beyond the plugin registration/webhook wiring described above, this PR also carries several fiscal fixes surfaced by live sandbox E2E testing during review — most notably an integer-groszy wire-format correction (invoices were being issued at ~1/100th of the real total) and a KSeF success -> accepted regulatory-status mapping fix (previously mapped to cleared, which the FE never renders as terminal). Both are covered by tests and confirmed against the real sandbox.

Test plan

  • pnpm --filter @openlinker/integrations-infakt test - 9 suites, 93 tests passing (2 new spec files)
  • pnpm --filter @openlinker/core test - 138 suites, 1410 tests passing (incl. new invoicing routing cases)
  • pnpm --filter @openlinker/api test - 48 suites, 580 tests passing (incl. new handshake short-circuit cases)
  • pnpm --filter @openlinker/worker test - 17 suites, 188 tests passing
  • pnpm lint - 0 errors (only pre-existing warnings)
  • pnpm type-check - 0 errors across all packages
  • pnpm check:invariants - cross-context imports, service interfaces, jest-integration-mappers all pass
  • Manual: pnpm start:dev:api starts without errors with Infakt module registered
  • Manual: GET /adapters lists infakt.accounting.v1 (route is GET /adapters, not /integrations/adapters)
  • Manual: POST /webhooks/infakt/{connectionId} with valid HMAC signature returns 202
  • Manual: POST /webhooks/infakt/{connectionId} with invalid signature returns 401
  • Manual: verification handshake echoes verification_code back correctly — required a fix: Infakt's own verifier only accepts HTTP 200 for the echo, not 202 (see manual QA comment below for the full writeup, incl. a live E2E confirmation and 7 other bugs found + fixed against the real sandbox)

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech review — PR #1293

Scope reviewed: this PR's actual diff is the delta from 1280-infakt-plugin-hardening-tests (the still-open parent, #1292) to 1281-infakt-plugin-registration-webhook — 23 files, ~583 additions. The larger file list shown on the PR page is inherited from the unmerged stack.

Verified locally: type-check clean on @openlinker/core, @openlinker/api, @openlinker/integrations-infakt; all new/changed specs pass (webhook.service.spec.ts, inbound-routing-policy.service.spec.ts, the two new Infakt adapter specs); check-cross-context-imports and check-jest-integration-mappers invariants both pass.

Summary

Solid, well-scoped increment. Plugin registration, jest-integration mapper entries, and the webhook decoder/translator pair all follow the exact pattern already established by InPost/Erli (inboundWebhookDecoderRegistry keyed by platformType, webhookEventTranslatorRegistry keyed by adapterKey). The detectHandshake port extension is additive and backward-compatible (optional method, existing decoders unaffected, DefaultWebhookDecoder untouched). No blocking issues found.

Issues

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-inbound-webhook-decoder.adapter.ts:24, infakt-webhook-event-translator.adapter.ts:22

Both files import the sibling infakt-webhook-translator via ../../infrastructure/webhooks/infakt-webhook-translator. Since both adapters live under infrastructure/adapters/, the direct path is ../webhooks/infakt-webhook-translator (one level up, not two-up-then-back-down). Functionally correct either way, just unnecessarily roundabout.

[SUGGESTION]infakt-inbound-webhook-decoder.adapter.ts, infakt-webhook-event-translator.adapter.ts

Both adapters construct new InfaktWebhookTranslator({ secret: '' }, logger) purely to call secret-independent methods (getVerificationEcho, toOlDomain). The comment justifies it as reuse-over-duplication, which is a reasonable call, but the dummy-secret construction is a little awkward. Not blocking — worth a follow-up if InfaktWebhookTranslator ever grows a constructor-time side effect.

[SUGGESTION] — PR description, "Manual" test-plan items

The exact HTTP status/body Infakt expects for the verification-handshake echo is explicitly flagged as unconfirmed in the PR description (currently reusing the existing 202 ACCEPTED from @HttpCode(HttpStatus.ACCEPTED) on the controller, which is fixed regardless of handshake vs. normal path). Worth confirming against the real Infakt webhook setup UI before merge, since a wrong status here means the webhook never activates in practice — but this is already tracked as a manual checklist item, not something the diff itself gets wrong.

Architecture / standards compliance

  • Cross-context contract: no violations — inbound-routing-policy.service.ts only adds a case to a closed never-exhaustive switch, and InboundEventDomainValues is the only place that union is pattern-matched exhaustively in the codebase (verified via grep — no other switch needed updating).
  • WebhookEventTranslatorPort / InboundWebhookDecoderPort implementations mirror the InPost/Erli precedent exactly (same registry keying, same header() case-insensitive helper, same total/never-throw contract on extractEnvelope).
  • RegulatoryStatusReconcilePayloadV1 payload and invoicing.regulatoryStatus.reconcile jobType are pre-existing (from [IMPL] feat(invoicing): regulatory (KSeF) status reconciliation job — refresh InvoiceRecord.regulatoryStatus #1121) — reused correctly, not reinvented; the webhook-triggered job is connection-scoped exactly like the scheduler-fanned-out one, so RegulatoryStatusReconcileHandler needs no changes.
  • Test coverage: new logic (handshake short-circuit, invoicing routing case, decoder detectHandshake/verify/extractEnvelope, translator translate) all has dedicated unit tests with meaningful assertions (short-circuit-before-verify/dedup/publish is explicitly asserted, not just the return value).
  • No domain-layer framework leakage, no any, DI/registration pattern matches the plugin-sdk conventions.

Verdict

Approve — no blocking or important issues. The three suggestions above are optional polish; none affect correctness or architecture compliance. Standard caveat: this PR is a draft stacked on unmerged #1292/#1280, so merge order still applies as already noted in the description.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Shorten the wrap-around import path in both webhook adapters
(infrastructure/adapters -> infrastructure/webhooks was going through
infrastructure twice), and replace the dummy-secret
`new InfaktWebhookTranslator({ secret: '' }, logger)` construction with
a named `InfaktWebhookTranslator.forParsing(logger)` factory that makes
the secret-independent-parsing-only intent explicit in the type itself.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech review suggestions addressed (1363f91)

Both [SUGGESTION]s from the tech review are fixed:

  1. Import pathinfakt-inbound-webhook-decoder.adapter.ts and infakt-webhook-event-translator.adapter.ts now import InfaktWebhookTranslator via ../webhooks/infakt-webhook-translator instead of the wrap-around ../../infrastructure/webhooks/infakt-webhook-translator (both adapters already live under infrastructure/adapters/, so the extra infrastructure/ hop was redundant).
  2. Dummy-secret construction — added InfaktWebhookTranslator.forParsing(logger), a static factory that constructs the translator with an empty secret for callers that only need the secret-independent parsing helpers (getVerificationEcho, parse, toOlDomain, toKsefResource). Both adapters now call this instead of new InfaktWebhookTranslator({ secret: '' }, logger), making the "parsing-only, never verifies" intent explicit in the type itself rather than relying on an inline comment. Added a unit test for the new factory.

The third suggestion (unconfirmed handshake HTTP status) is a manual-verification item, not a code change — left as-is per the PR description's existing checklist.

Verified: pnpm --filter @openlinker/integrations-infakt test (9 suites, 94 tests passing, incl. new factory test), type-check and lint clean.

@norbert-kulus-blockydevs

norbert-kulus-blockydevs commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Manual QA — backend-only, before FE work

Ran the full manual checklist from the PR description against this branch's exact HEAD, plus a full live round-trip (issue → KSeF submission → real Infakt webhook → clearance) against the real Infakt sandbox. Found and fixed 8 bugs along the way — all pushed to this branch. Details below, in discovery order.


Bugs found + fixed (chronological)

  1. Webhook decoder never used the ignore outcome. InfaktInboundWebhookDecoderAdapter.extractEnvelope always returned action: 'route' regardless of event name, so every non-actionable Infakt event (draft_invoice_created, invoice_marked_as_paid, …) still paid for a full Postgres dedup-insert + Redis publish before being dead-lettered two hops later in WebhookToJobHandler. Fixed to short-circuit to ignore when the event isn't one OL acts on.
    libs/integrations/infakt/src/infrastructure/adapters/infakt-inbound-webhook-decoder.adapter.ts (extractEnvelope)

  2. Wrong client field names sent to Infakt's real API. upsertCustomer sent name/post_code; Infakt's v3 API wants company_name/postal_code — the old fields were silently rejected/ignored, so first-time client creation always 422'd.
    libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts (upsertCustomer), libs/integrations/infakt/src/domain/types/infakt.types.ts (InfaktClient)

  3. Wrong default payment method. issueInvoice hardcoded payment_method: 'transfer', which Infakt rejects unless the seller has a bank account configured (bank_account/bank_name required) — something OL has no way to know per-connection. Defaulted to 'cash' (the value proven live in the original June 30 feasibility POC).
    infakt-invoicing.adapter.ts (issueInvoice)

  4. Wrong client identifier on invoice creation. issueInvoice sent client_uuid; Infakt's invoices.json only accepts the numeric client_id and silently rejects client_uuid with "client_id required". providerCustomerId now carries the numeric id.
    infakt-invoicing.adapter.ts (upsertCustomer, resolveClientId, issueInvoice)

  5. Corrections never sent a client at all. issueCorrection omitted the client field entirely — every correction attempt 422'd with "client_id required". Now forwards original.client_id from the invoice being corrected (no extra upsertCustomer round-trip needed).
    infakt-invoicing.adapter.ts (issueCorrection)

  6. Every invoice line was rejected — universally. Core always leaves InvoiceLine.taxRate empty by design (the adapter is documented to "resolve the regime rate"), but the adapter never actually did — an empty tax_symbol cascades into services.gross/value.tax_values rejections too, so every single line on every single invoice 422'd regardless of the fixes above. Added a Polish-standard-VAT (23%) fallback when taxRate is empty.
    infakt-invoicing.adapter.ts (toInfaktTaxSymbol, taxRateNumeric)

  7. Nothing ever triggered KSeF submission. InfaktInvoicingAdapter.sendToKsef() existed but wasn't part of InvoicingPort/any sub-capability and wasn't called anywhere in InvoiceService/InvoicingController/worker handlers — only the standalone POC script ever called it directly. Every Infakt invoice issued through OL sat in draft (KSeF-untouched) forever. This is what the dashboard screenshot caught. Fixed by calling sendToKsef inline from issueInvoice/issueCorrection — mirroring how the KSeF adapter submits inline (build→session→submit, one atomic step) and how Subiekt "transmits to KSeF natively at issuance": issuing IS submitting for this provider too.
    infakt-invoicing.adapter.ts (issueInvoice, issueCorrection)

  8. Handshake echo returned the wrong HTTP status. The subscription-verification echo returned 202 (the same status as a normal accepted-webhook), but Infakt's own "Verify" button reports "could not verify webhook" on anything but 200. Only surfaced once testing against the real Infakt UI (a synthetic curl test can't catch this — it doesn't validate the status code the way Infakt's own verifier does). Fixed: the controller now returns 200 specifically for the handshake-echo branch, leaving 202 as the default for every other outcome.
    apps/api/src/webhooks/http/webhook.controller.ts (receiveWebhook)

All 96 @openlinker/integrations-infakt unit tests pass (4 new regression tests added for bugs 2–8), pnpm --filter @openlinker/api type-check and lint are clean.


PR checklist results

  • pnpm start:dev:api starts without errors with Infakt module registered
  • GET /adapters lists infakt.accounting.v1 (note: the actual route is GET /adapters, not /integrations/adapters as written in the PR description — cosmetic doc nit only)
  • POST /webhooks/infakt/{connectionId} with valid HMAC signature returns 202
  • POST /webhooks/infakt/{connectionId} with invalid signature returns 401
  • Verification handshake echoes verification_code back correctly (see bug Design Technical Architecture & Engineering Standards for OpenLinker (NestJS + Hexagonal) #8 — required a fix to actually pass Infakt's own verifier)

Additional webhook coverage

Connection lifecycle — create against real sandbox + validator negative paths
POST /connections  { platformType: "infakt", config: { baseUrl: "https://api.sandbox-infakt.pl/api/v3" }, credentials: { apiKey: "<sandbox key>" } }
→ 201, connection created, enabledCapabilities: ["Invoicing"]

POST /connections  { ..., credentials: { apiKey: "" } }
→ 400 { "message": "Invalid Infakt credentials: must include a non-empty `apiKey` string" }

POST /connections  { ..., config: { baseUrl: "not-a-url" } }
→ 400 { "message": "Invalid Infakt connection config", "errors": [{"path":"baseUrl","message":"must be a valid URL"}] }
Webhook: valid HMAC signature → 202, published
BODY='{"event":{"uuid":"evt-1","name":"send_to_ksef_success","retry_counter":0,"created_at":"..."},"resource":{...}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST /webhooks/infakt/{connectionId} -H "X-Infakt-Signature: $SIG" -d "$BODY"
→ HTTP 202
webhook_deliveries row: status=published, signatureValid=true
Webhook: invalid / missing signature → 401
curl -X POST /webhooks/infakt/{connectionId} -H "X-Infakt-Signature: deadbeef..." -d "$BODY"  → HTTP 401 {"message":"Invalid webhook signature"}
curl -X POST /webhooks/infakt/{connectionId} -d "$BODY"  (no signature header at all)          → HTTP 401 {"message":"Invalid webhook signature"}
Webhook: malformed payload (valid signature over garbage JSON) → 400
MALFORMED='{"not-an-event-object": true}'
SIG=$(printf '%s' "$MALFORMED" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST /webhooks/infakt/{connectionId} -H "X-Infakt-Signature: $SIG" -d "$MALFORMED"
→ HTTP 400 {"message":"malformed Infakt webhook payload"}
Webhook: replay / dedup — resending the identical signed payload
(same curl as the valid-signature test, sent twice with the same event uuid)
→ 1st: HTTP 202, webhook_deliveries INSERT, event published to events.inbound.webhooks
→ 2nd: HTTP 202, "Duplicate webhook event (Postgres gate)" — no second row, no second publish
Webhook: capability-gated (ungated) + unhandled event type
PATCH /connections/{id}  { enabledCapabilities: [] }        # disable Invoicing
(send send_to_ksef_error webhook)
→ HTTP 202, webhook_deliveries.dlqReason = "ungated: invoicing requires Invoicing" — no job enqueued

(send draft_invoice_created webhook, Invoicing re-enabled)
→ HTTP 202, decoder returns action:'ignore' (bug #1 fix) — no Postgres insert, no publish, no job

Full live E2E confirmation (issue → KSeF → real webhook → cleared)

This goes beyond the PR's own checklist — it's what #1293 actually unlocks (first time Infakt is reachable end-to-end via the API), so I verified it fully against the real sandbox rather than stopping at the wiring level.

Step 1 — issue invoice (after all fixes)
POST /invoices  { connectionId, orderId }
→ HTTP 201
{
  "providerInvoiceId": "f54c283a-...",
  "providerInvoiceNumber": "5/07/2026",
  "regulatoryStatus": "submitted",   ← KSeF submission triggered inline (bug #7 fix)
  "status": "issued"
}
Step 2 — issue correction on that invoice
POST /invoices/{id}/correct  { reason, lines: [{ originalLineNumber: 1, newQuantity: 1 }] }
→ HTTP 201
{
  "providerInvoiceId": "0113723e-...",
  "providerInvoiceNumber": "6/07/2026",
  "regulatoryStatus": "submitted",
  "status": "issued"
}
Step 3 — real webhook received (temporary cloudflared tunnel + the real HMAC secret from Infakt's own dashboard — NOT a synthetic payload)
webhook_deliveries row (real delivery from Infakt, not curl):
eventId:        8ec70d3c-30ee-4148-a0d3-fd72310b5bb8
eventType:      send_to_ksef_success
signatureValid: true                 ← real HMAC verified against Infakt-issued secret
status:         published
payload.ksef_number: "8201194127-20260701-6DB040C00000-90"

This confirms bug #8's fix too — Infakt's "Verify" button only went Aktywny after the handshake was changed to return 200 (see screenshot below).

Step 4 — webhook-triggered reconcile job runs, clearanceReference populated
sync_jobs: invoicing.regulatoryStatus.reconcile → status=succeeded, outcome=ok

invoice_records after reconcile:
providerInvoiceId=0113723e-...  regulatoryStatus=cleared  clearanceReference=8201194127-20260701-6DB040C00000-90
providerInvoiceId=f54c283a-...  regulatoryStatus=cleared  clearanceReference=8201194127-20260701-6CFD40C00000-1B
image

[SCREENSHOT 1 — Infakt dashboard, both invoices now ZAKSIĘGOWANO (booked), no longer draft]

image

[SCREENSHOT 2 — webhook details + delivery history: send_to_ksef_success / draft_invoice_created events, real signature]

image

[SCREENSHOT 3 — invoice detail showing "Wysłano do KSeF" + the real KSeF number, matching clearanceReference above]

Note on screenshot 2: the webhook shows Status: REVOKED and both delivery attempts show ABANDONED — that's expected cleanup, not a failure. The tunnel was intentionally torn down right after this test (temporary cloudflared quick tunnel, not a permanent endpoint), and Infakt's own delivery-history / abandonment bookkeeping reflects that teardown — our side's webhook_deliveries row is the authoritative record that the delivery was received and processed correctly (signatureValid: true, matching ksef_number).


Environment notes (not code issues — context for reproducing)

  • Tested in an isolated worktree on this branch's HEAD, own PORT=3010 / REDIS_DB=1 — a different, already-running dev API instance (unrelated worktree, same shared Redis) initially grabbed our webhook message via the shared webhook-handler consumer group before I isolated onto a separate Redis logical DB. Not a bug, just a note for anyone testing webhooks against a shared dev Redis with multiple API instances running.
  • One invoicing.regulatoryStatus.reconcile job sat queued for ~2.5h before I forced it to run manually via SQL. Investigated separately: this is the normal exponential retry backoff (30 × 2^(attempt−1), capped at 6h) — not a scheduling bug, and not caused by anything in this PR. Root cause is almost certainly that the other already-running (non-Infakt) worker instance mentioned above grabbed this job at some point during testing and failed repeatedly (it can't resolve the Infakt capability), driving it deep into backoff before my own worker got a turn. No fix needed here.

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the #1293-specific delta (25 files, isolated by diffing head against the #1280/#1292 base). Clean, well-tested, and consistent with the KSeF/Subiekt dual-registration precedent. Approve-in-spirit with one item to confirm before this leaves draft.

Must confirm before merge

  • Handshake HTTP statuswebhook.controller.ts:44 sets @HttpCode(HttpStatus.ACCEPTED) (202), so the subscription-verification echo returns 202. Infakt's verification ping commonly expects HTTP 200 to activate the webhook; if so, activation would silently fail. You already flagged this as unconfirmed in the test plan — please verify against Infakt's docs/sandbox. If 200 is required, return a distinct 200 for the handshake path rather than the shared 202.

Suggestions (non-blocking)

  • getVerificationEcho triggers on any JSON body with a string verification_code and runs before signature verification. A signed event carrying such a field would be mis-short-circuited and never processed. Low likelihood given Infakt's {event, resource} envelope, but consider gating on the absence of the event envelope (or a dedicated handshake header) to make detection unambiguous.
  • The handshake echo is returned pre-verification, reflecting an attacker-controlled verification_code. Inherent to the subscription-verification pattern and gated on an active connection, so risk is minimal — noting for completeness only.

Verified clean

  • #917 mapper guard: both ^@openlinker/integrations-infakt$ and .../(.*)$ entries present in both apps' jest-integration.cjs, matching the plugins.ts registration — check:invariants should pass.
  • Routing target invoicing.regulatoryStatus.reconcile is real end-to-end: job-type union, payload type, worker handler + registration, and a DI int-spec all exist. No orphan enqueue.
  • Capability gating correct (requiredCapability: 'Invoicing', ungated when absent — covered by spec).
  • Port change is additive/optional (detectHandshake?); DefaultWebhookDecoder + InPost unaffected.
  • HostServices exposes both registries; decoder keyed by platformType, translator by adapterKey (ADR-021/ADR-015).
  • Dependency direction intact; no migrations; no WebhookProvisioningPort (UI-only setup, correctly out of scope).

Merge is also blocked on the base PRs #1280/#1292 landing first (draft-stacked).

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Address review feedback on #1293: gate getVerificationEcho on the
absence of the event envelope so a signed webhook that happens to
carry a verification_code field in its resource is never
mis-short-circuited into the handshake path. Also cap the echoed
verification_code length, since the handshake echo is returned
pre-signature-verification and shouldn't reflect an unbounded
attacker-controlled value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Shorten the wrap-around import path in both webhook adapters
(infrastructure/adapters -> infrastructure/webhooks was going through
infrastructure twice), and replace the dummy-secret
`new InfaktWebhookTranslator({ secret: '' }, logger)` construction with
a named `InfaktWebhookTranslator.forParsing(logger)` factory that makes
the secret-independent-parsing-only intent explicit in the type itself.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Address review feedback on #1293: gate getVerificationEcho on the
absence of the event envelope so a signed webhook that happens to
carry a verification_code field in its resource is never
mis-short-circuited into the handshake path. Also cap the echoed
verification_code length, since the handshake echo is returned
pre-signature-verification and shouldn't reflect an unbounded
attacker-controlled value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1281-infakt-plugin-registration-webhook branch from c8d8103 to c446b43 Compare July 1, 2026 17:17
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Addressed all findings from the latest review:

Must confirm — handshake HTTP status: already resolved in a prior commit (fix(infakt): correct field names, KSeF submission, and handshake status found in manual QA) — the controller now returns a distinct 200 for the verification-handshake echo (via res.status(HttpStatus.OK)), confirmed live against Infakt's sandbox "Zweryfikuj" button. The route's default @HttpCode(202) still applies to every other outcome.

Suggestion — ambiguous handshake detection: fixed in InfaktWebhookTranslator.getVerificationEcho. The check is now gated on the absence of the event envelope, since every signed delivery is { event, resource } while the handshake ping is the bare { verification_code } shape — a signed event carrying a stray verification_code field in its resource is no longer mis-short-circuited into the handshake path. Added a length cap (256 chars) on the echoed value as well, since the guard now disambiguates by shape rather than by value alone.

Suggestion — pre-verification reflection: added the length cap noted above as defense-in-depth. No further action taken beyond that — the echo is JSON-only (no HTML/script context) and gated on an active connection, so the residual risk matches the reviewer's own assessment ("minimal, noted for completeness").

Added 3 new unit tests covering the disambiguation and length-cap behavior (libs/integrations/infakt/src/infrastructure/webhooks/__tests__/infakt-webhook-translator.spec.ts). Full verification:

  • pnpm --filter @openlinker/integrations-infakt test — 9 suites, 99 tests passing (was 93)
  • pnpm --filter @openlinker/integrations-infakt type-check / pnpm --filter @openlinker/api type-check — 0 errors
  • pnpm check:invariants — all checks pass

Branch has also been rebased onto latest main (linear history, no more merge commits) and force-pushed.

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — both prior findings resolved ✅

Re-reviewed the new commits (head c446b431). Both items from the prior pass are addressed, with test coverage.

1. IMPORTANT — handshake HTTP status (202 → 200): ADDRESSED.
webhook.controller.ts now returns 200 for the handshake echo (res.status(HttpStatus.OK)) while keeping the route default 202 for the route/ignore paths. The inline comment cites live verification against Infakt's "Zweryfikuj" button (2026-07-01): 202 → "could not verify", 200 → accepted. Swagger documents both. Exactly what I asked for, backed by sandbox evidence.

2. SUGGESTION — disambiguate handshake from signed events: ADDRESSED (and hardened).
getVerificationEcho now gates on the absence of the event envelope (!('event' in parsed)), so a signed { event, resource } delivery carrying a verification_code field is no longer mis-short-circuited. Bonus: verification_code is length-capped at 256 chars — since the echo is returned pre-signature-verify, an oversized value is rejected rather than reflected verbatim. Covered by three new translator specs plus a service spec asserting the handshake short-circuits before verify/dedup/publish.

#917 jest mapper guard: intact. Both ^@openlinker/integrations-infakt$ and .../(.*)$ entries are present in both apps/api/test/jest-integration.cjs and apps/worker/test/jest-integration.cjs.

Wiring sanity: decoder registered under platformType='infakt' (matches the /webhooks/infakt/:connectionId path and the controller's ^[a-z]+$ provider regex); translator under the adapterKey. Registration mirrors the Subiekt/KSeF dual-app precedent.

No new blocking findings. Remaining gate before un-drafting is CI — run the full pnpm test:integration (not just the feature specs), since adding an adapter-manifest capability + webhook routing ripples into the routing int-specs. Also blocked mechanically on the base PRs #1280/#1292 landing first.

@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1281-infakt-plugin-registration-webhook branch from c446b43 to 0bd0a9b Compare July 1, 2026 18:14
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Shorten the wrap-around import path in both webhook adapters
(infrastructure/adapters -> infrastructure/webhooks was going through
infrastructure twice), and replace the dummy-secret
`new InfaktWebhookTranslator({ secret: '' }, logger)` construction with
a named `InfaktWebhookTranslator.forParsing(logger)` factory that makes
the secret-independent-parsing-only intent explicit in the type itself.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Address review feedback on #1293: gate getVerificationEcho on the
absence of the event envelope so a signed webhook that happens to
carry a verification_code field in its resource is never
mis-short-circuited into the handshake path. Also cap the echoed
verification_code length, since the handshake echo is returned
pre-signature-verification and shouldn't reflect an unbounded
attacker-controlled value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Latest review addressed

Re-reviewed the two prior findings (handshake HTTP status, handshake/signed-event disambiguation) — both already confirmed resolved in this review pass, no new blocking or important findings raised.

The only remaining item noted was a CI gate: run the full pnpm test:integration suite (not just the feature-scoped specs) before un-drafting, since the manifest-capability + webhook-routing changes ripple into shared routing int-specs. That will run on CI for this push rather than locally.

Also rebased the branch cleanly onto latest main (linear history, no merge commits, force-pushed) — the PR now targets main directly with no base-PR blockers left.

No code changes were needed for this round; the branch is otherwise unchanged.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
… correction

Implements the frontend half of the inFakt accounting integration
(epic #1279): guided connection setup (name + API key + optional
sandbox baseUrl, mirroring the Erli pattern), credentials rotation,
post-create baseUrl editing, and the invoice-surfacing slots
(regulatory status + KSeF number, KOR correction flow) driven by the
existing generic InvoicingPort/CorrectionIssuer backend contract
(PR #1292/#1293).

Also introduces a shared `.reg-card` severity-stripe treatment for the
invoiceDetailSection provider slot (additive alongside each section's
existing root class, so KSeF/Subiekt keep their current DOM shape and
tests) — inFakt ships with it from day one, KSeF/Subiekt adopt it as a
drive-by improvement.

Closes #1282

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech Review — PR #1293

Summary

Registers the Infakt plugin (InfaktIntegrationModule) in the API and worker hosts, wires Infakt's third-party-native webhook into OL's ingestion pipeline via a new InboundWebhookDecoderPort.detectHandshake extension, and adds an invoicing inbound domain routed to the existing invoicing.regulatoryStatus.reconcile job. Architecture is sound: the handshake extension is additive (optional method, DefaultWebhookDecoder/InPost unaffected), the webhook-is-a-trigger-not-source-of-truth design mirrors the documented InPost philosophy, and naming/layering follow the established ADR-021/ADR-015 patterns. Full quality gate (lint, type-check, unit tests across core/api/worker/integrations-infakt, check:invariants) passes clean.

Issues

[IMPORTANT]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts

issueInvoice/issueCorrection now perform two sequential external calls: create the draft (POST invoices.json) then submit it (POST .../send_to_ksef.json). If the second call fails, the method throws and core treats the whole issuance as failed — but Infakt is left holding an un-submitted draft. Retry-safety depends on Infakt deduping POST invoices.json by the external_id (idempotencyKey) field so a retry reuses the same draft and re-attempts submission rather than creating a second orphaned draft. This assumption isn't verified anywhere (no test covers send_to_ksef failing after a successful invoice creation). Worth either a test documenting the expected retry behavior or a short comment noting the assumption explicitly.

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts

payment_method is hardcoded to 'cash' for every invoice and correction (previously 'transfer'), purely to avoid a 422 from a missing bank-account config — not derived from any order/payment signal, since libs/core/src/invoicing has no neutral paymentMethod field on IssueInvoiceCommand at all. Reasonable stopgap for now (documented inline), but since "cash" carries real fiscal/accounting meaning in Poland, this is worth a tracked follow-up issue rather than a permanent silent default.

Verdict

🔄 Approve with changes — no architecture/boundary violations found; the one IMPORTANT item is a reliability gap worth addressing (or explicitly accepting) before merge.

@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1281-infakt-plugin-registration-webhook branch from 0bd0a9b to 132bbe6 Compare July 1, 2026 20:49
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Shorten the wrap-around import path in both webhook adapters
(infrastructure/adapters -> infrastructure/webhooks was going through
infrastructure twice), and replace the dummy-secret
`new InfaktWebhookTranslator({ secret: '' }, logger)` construction with
a named `InfaktWebhookTranslator.forParsing(logger)` factory that makes
the secret-independent-parsing-only intent explicit in the type itself.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
Address review feedback on #1293: gate getVerificationEcho on the
absence of the event envelope so a signed webhook that happens to
carry a verification_code field in its resource is never
mis-short-circuited into the handshake path. Also cap the echoed
verification_code length, since the handshake echo is returned
pre-signature-verification and shouldn't reflect an unbounded
attacker-controlled value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
- Document the retry-safety assumption behind the two-step issue+submit
  flow at both sendToKsef call sites: a retry re-POSTs invoices.json with
  the same external_id, relying on Infakt returning/reusing the same
  invoice uuid rather than creating a duplicate draft, so the follow-up
  sendToKsef becomes a safe re-attempt on the same document.
- Add error-path tests for issueInvoice and issueCorrection covering
  sendToKsef failing after a successful invoice creation, asserting the
  InfaktApiError/failureMode propagates.
- payment_method being hardcoded to 'cash' is tracked separately as #1303
  (no neutral paymentMethod field exists on IssueInvoiceCommand yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech Review Follow-up — Findings Addressed

Both findings from the review above have been addressed:

[IMPORTANT] Two-step issue+submit reliability — Fixed in libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts:

  • Added explicit comments at both sendToKsef call sites (in issueInvoice and issueCorrection) documenting the retry-safety assumption: a retry re-POSTs invoices.json with the same external_id, relying on Infakt returning/reusing the same invoice uuid rather than creating a duplicate draft, so the follow-up sendToKsef call becomes a safe re-attempt on the same document.
  • Added error-path unit tests for both issueInvoice and issueCorrection covering sendToKsef failing after a successful invoice creation, asserting the InfaktApiError/failureMode propagates correctly.

[SUGGESTION] Hardcoded payment_method: 'cash' — Filed as a tracked follow-up: #1303. The neutral IssueInvoiceCommand contract has no paymentMethod field today, so this remains a stopgap default rather than something fixable within this PR's scope.

Also fixed during rebase-onto-main: this branch was originally stacked on the now-merged #1292, so it needed rebasing to drop the two commits already incorporated into main and resolve the resulting conflicts. One of those conflicts (in infakt-invoicing.adapter.ts's tax-rate handling) needed manual reconciliation to avoid a duplicate/dead helper — verified via the full quality gate afterward.

Quality gate (all green after fixes):

  • pnpm --filter @openlinker/integrations-infakt test — 9 suites, 103 tests passing
  • pnpm --filter @openlinker/integrations-infakt type-check — clean
  • pnpm --filter @openlinker/core test — 138 suites, 1410 tests passing
  • pnpm --filter @openlinker/api test — 48 suites, 587 tests passing
  • pnpm --filter @openlinker/worker test — 17 suites, 188 tests passing
  • pnpm lint — 0 errors, check:invariants all pass
  • pnpm type-check — 0 errors across all packages

Branch has been rebased onto current main and force-pushed — no more merge conflicts.

@norbert-kulus-blockydevs norbert-kulus-blockydevs marked this pull request as ready for review July 1, 2026 21:09
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Finding: no ConnectionTester registered for infakt.accounting.v1

While running a live E2E walkthrough of the inFakt FE plugin (PR #1300 / issue #1282) against the real inFakt sandbox on this branch, clicking Test connection on a freshly-created inFakt connection surfaces:

Unable to test connection
Connection testing is not supported for adapter infakt.accounting.v1

This is because infakt-plugin.ts's register(host) registers the config/credentials shape validators, the retry classifier, and the webhook decoder/translator — but never calls host.connectionTesterRegistry.register(...). Every other adapter with a "Test connection" affordance in the FE (Erli, WooCommerce, …) has one; inFakt is the one gap. Not a regression in this PR, just a pre-existing omission that's now visibly reachable once the FE plugin (PR #1300) ships its guided setup wizard with a Test connection button.

Suggested fix

Mirror ErliConnectionTesterAdapter (libs/integrations/erli/src/infrastructure/adapters/erli-connection-tester.adapter.ts):

  1. New adapter libs/integrations/infakt/src/infrastructure/adapters/infakt-connection-tester.adapter.ts:

    • class InfaktConnectionTesterAdapter implements ConnectionTesterPort
    • test(connection, credentialsResolver): resolve apiKey (+ optional config.baseUrl) the same way InfaktAdapterFactory.createInvoicingAdapter does, construct a bare InfaktHttpClient, and issue one cheap authenticated GET — e.g. GET /clients.json?limit=1 (a real, already-used Infakt v3 endpoint per the feasibility POC, cheap, side-effect-free, and requires a valid API key so a 2xx confirms both reachability and credential validity, same posture as Erli's GET /me).
    • Map InfaktApiError (401/403 → bad key, network failure → transport error) to the neutral ConnectionTestResult, same shape as Erli's toFailure() — never let a raw fetch/undici message reach the operator.
    • No-retry budget (fail fast, matches Erli's NO_RETRY).
  2. Register it in infakt-plugin.ts's register(host), alongside the existing validator/retry-classifier/webhook registrations:

    host.connectionTesterRegistry.register(INFAKT_ADAPTER_KEY, new InfaktConnectionTesterAdapter());
  3. Unit test infakt-connection-tester.adapter.spec.ts mirroring erli-connection-tester.adapter.spec.ts — success path, bad-key path, network-failure path.

Why flag it now

Not blocking for this PR's merge, but wanted to surface it before #1293 lands so it doesn't sit as a silent UX gap once operators start using the guided setup wizard — the FE's "Test connection" button is generic across every plugin, so it degrades to this error message for any adapter lacking a tester. Happy to open a dedicated issue if you'd rather track it separately instead of fixing inline here.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Fixed: ConnectionTesterPort registered for infakt.accounting.v1

Addressed the finding above (#4860108754):

  • Added InfaktConnectionTesterAdapter (libs/integrations/infakt/src/infrastructure/adapters/infakt-connection-tester.adapter.ts), mirroring the SubiektConnectionTesterAdapter pattern rather than Erli's factory-based one — Infakt's InfaktAdapterFactory only exposes createInvoicingAdapter, not a bare HTTP-client construction seam, so this tester resolves credentials and builds the client directly.
  • Probe: GET clients.json?limit=1 — cheap, side-effect-free, already used elsewhere in the adapter (findClientByNip), and requires a valid API key so a 2xx confirms both reachability and credential validity.
  • InfaktApiError maps to the neutral ConnectionTestResult (bounded message, no responseBody leak); any other failure (network, missing credentials) collapses to a fixed "Infakt probe failed" string. Never throws.
  • Registered in infakt-plugin.ts's register(host) alongside the existing validator/retry-classifier/webhook registrations.
  • Added infakt-connection-tester.adapter.spec.ts mirroring erli-connection-tester.adapter.spec.ts (success, 401, transport error, no-response-body-leak, missing-credentials paths).

Verified: pnpm --filter @openlinker/integrations-infakt test (108 tests passing), pnpm --filter @openlinker/integrations-infakt type-check, full pnpm lint + pnpm type-check across the monorepo — all clean. Pushed as b39e2efd.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
…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>
…l strings

Critical fiscal bug found during live E2E against the real inFakt sandbox:
issued invoices landed on inFakt's dashboard (and were submitted to KSeF)
at ~1/100th of the real order total (e.g. a PLN 349.00 order → PLN 3.48 on
the resulting invoice). Confirmed against both the raw sandbox response
(net_price/tax_price/gross_price returned as plain integers, e.g. 283 for
PLN 2.83) and the official Infakt API schema (unit_net_price/net_price/
gross_price are `integer`, documented "w groszach") that inFakt's wire
format is a plain integer count of groszy (1 PLN = 100 groszy) everywhere
- not the "amount currency" decimal-string format (e.g. "283.74 PLN") the
adapter was previously sending, which the earlier #1292 review had
incorrectly confirmed against the schema.

- InfaktInvoice.gross_price/net_price/tax_price and
  InfaktInvoiceService.unit_net_price/net_price/tax_price/gross_price are
  now typed `number` (plain integer groszy), not `string`.
- Added toGroszy/fromGroszy helpers replacing the removed parseInfaktAmount;
  issueInvoice and issueCorrection now round PLN amounts to integer groszy
  when building the request payload, and convert the original invoice's
  groszy amounts back to PLN decimals before doing gross-to-net arithmetic.
- Updated all affected unit tests (fixtures + assertions) to the integer
  groszy format and to assert exact groszy values rather than decimal
  strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1281-infakt-plugin-registration-webhook branch from c7fc44c to 651cac2 Compare July 1, 2026 22:05
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech Review — PR #1293 (delta since the last full review)

Scope reviewed: only the two commits that landed after the previous full /tech-review pass (comment #4859909447) and its immediate follow-up fix:

  • 4017f182fix(infakt): register ConnectionTesterPort for infakt.accounting.v1
  • 651cac28fix(infakt): send invoice amounts as plain-integer groszy, not decimal strings

Summary

Both are correctness fixes found during live E2E against the real inFakt sandbox, not new features, and both are scoped tightly to the bug they fix. Architecture is clean and consistent with prior review passes — no new boundary violations. I traced the groszy-format bug through every reader of the affected fields (InfaktInvoice/InfaktInvoiceService price fields) and confirmed infakt-invoicing.adapter.ts is the only consumer in the package, so the type change from stringnumber has no other blast radius. Nothing blocking.

Findings

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-connection-tester.adapter.ts

InfaktConnectionTesterAdapter follows the documented SubiektConnectionTesterAdapter precedent correctly: implements ConnectionTesterPort, never throws, strips InfaktApiError.responseBody from the operator-facing message, and is registered via host.connectionTesterRegistry in infakt-plugin.ts alongside the other Infakt registrations — no notes beyond this being a clean, well-tested gap-fill (5 unit tests covering success/401/transport-error/no-body-leak/missing-credentials).

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts

The toGroszy/fromGroszy fix is correct and well-verified (live sandbox + official schema via MCP meta_schema). One small note for future hardening rather than a blocker: toGroszy uses Math.round(amountPln * 100), which is the right call for this PR's scope, but the underlying grossToNet/taxRateNumeric pair (unchanged by this diff) still does plain floating-point division before rounding happens once at the boundary — fine for typical PLN amounts, but worth keeping in mind if a future correction ever chains multiple gross↔net conversions before the final toGroszy call, since floating-point drift could compound across steps. Not something this PR needs to address.

Both commits report a clean quality gate (pnpm --filter @openlinker/integrations-infakt test, type-check, full pnpm lint + pnpm type-check) per the existing PR comments, consistent with what the diff review would predict — no dead code, no leftover unused currency/parseInfaktAmount references after the refactor.

Verdict

Approve — both fixes are correct, narrowly scoped, well-tested, and consistent with the architecture established in prior review rounds. No blocking or important issues found in this delta.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech Review — PR #1293 (full PR, all 8 commits)

Scope: entire diff from main to 1281-infakt-plugin-registration-webhook (29 files, ~2450 lines). This supersedes my earlier delta-only comment — full re-review below covers everything, including what was already looked at in prior rounds.

Summary

This PR does three things: (1) registers InfaktIntegrationModule in apps/api/apps/worker, (2) extends InboundWebhookDecoderPort with an optional detectHandshake step and wires Infakt's third-party-native webhook through it (ADR-021/ADR-015), and (3) hardens the Infakt invoicing adapter against a series of real-sandbox findings (field names, payment method, client id vs uuid, tax-rate fallback, inline KSeF submission, groszy vs decimal-string amounts, missing ConnectionTester). The architecture is sound throughout: the handshake extension is additive and doesn't touch DefaultWebhookDecoder/InPost, the invoicing inbound domain follows the existing closed-union + exhaustive-switch pattern, and every new adapter is properly named/ported (*Port implementations, registered via the host registries in infakt-plugin.ts, never injected as concrete classes). The commit history itself is instructive — six of the eight commits are review-driven or live-E2E-driven fixes, and each fix is narrowly scoped to the finding it addresses without collateral changes. Full quality gate reported green on every commit.

Issues

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts (sendToKsef)

Retry-safety of the two-step issue+submit flow rests on an unverified assumption, documented inline at both call sites: a retry re-POSTs invoices.json with the same external_id and relies on Infakt reusing the same draft uuid rather than creating a duplicate. This is honestly flagged as unverified, has an error-path test covering the "sendToKsef fails after draft succeeds" branch, and was already raised + accepted in a prior review round. Restating it here only because it's a real production risk (an orphaned, un-submitted KSeF draft is a silent fiscal-compliance gap) — worth a tracked follow-up to confirm Infakt's actual dedup behavior against external_id, even though it's out of scope for this PR to fix blind.

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts (payment_method: 'cash')

Hardcoded for every invoice/correction, not derived from any order/payment signal — IssueInvoiceCommand has no neutral paymentMethod field. Already filed as #1303; no action needed here, just confirming the tracking exists and the stopgap is contained to this one adapter.

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts (sendToKsef visibility)

Changed from private to public with the rationale "so an operator-facing manual re-submit can reuse it later." No current caller exists outside the class. Minor — not a real YAGNI violation since it's a one-line accessibility change with a documented reason, but worth flagging: if the manual re-submit path doesn't materialize soon, consider reverting to private to keep the adapter's public surface minimal.

[SUGGESTION]apps/api/src/webhooks/http/webhook.controller.ts

The handshake-echo status code (200 vs the route's default 202) is set imperatively via injected @Res({ passthrough: true }) + res.status(HttpStatus.OK), based on a live-verified behavioral quirk of Infakt's own webhook verifier. This is a reasonable, tightly-scoped use of the escape hatch (passthrough mode still lets Nest serialize the return value), but it's the first place in this controller that reaches for the raw Response object — worth a one-line comment pointer for future maintainers scanning for why @Res shows up next to an otherwise-clean @HttpCode pattern (the existing comment already explains why 200 is needed; just flagging the pattern is now slightly heavier than the rest of the file).

[SUGGESTION]libs/integrations/infakt/src/infrastructure/adapters/infakt-inbound-webhook-decoder.adapter.ts

parsed.resource['invoice_uuid'] is read via bracket notation with a redundant extra pair of parens ((parsed.resource['invoice_uuid'])) — harmless, but looks like a merge artifact rather than intentional style. Cosmetic only.

No BLOCKING or IMPORTANT findings. I checked for regressions the type change in the final commit (InfaktInvoice/InfaktInvoiceService price fields string → number) could have caused elsewhere in the package — infakt-invoicing.adapter.ts is the only consumer, so there's no blast radius. I also checked that the taxRateNumeric duplication introduced by the rebase (flagged and self-fixed in commit 5/8) actually resolved cleanly — it did: the dead private method was removed and the empty-string fallback landed on the one function that's actually on the call path (grossToNet → module-level taxRateNumeric).

Architecture & standards compliance

  • Hexagonal boundaries: clean. InfaktInboundWebhookDecoderAdapter / InfaktWebhookEventTranslatorAdapter / InfaktConnectionTesterAdapter all implement core ports (InboundWebhookDecoderPort, WebhookEventTranslatorPort, ConnectionTesterPort) and are registered through the plugin's register(host) hook against the documented registry services — no core→integration leakage, no adapter injected as a concrete class anywhere.
  • as const union pattern: InboundEventDomainValues extended correctly (array + derived type), matching the documented pattern.
  • Exhaustiveness: InboundRoutingPolicyService's switch keeps its const exhaustive: never = event.domain guard after adding the 'invoicing' case — a future missed case will fail type-check, not silently no-op.
  • Naming: all new files/classes follow *.adapter.ts / {System}{Capability}Adapter conventions.
  • Testing: every new adapter ships a .spec.ts with success/failure/edge-case coverage (signature verification, handshake disambiguation, length-capping, malformed payload, missing credentials, sendToKsef failure-after-success). Test names follow the should … when … convention.
  • Security: the verification-code handshake disambiguation fix (commit 4/8) is a genuine hardening — gating on absence of the event envelope plus a 256-char cap prevents a signed payload from being mis-routed into the pre-verification echo path, and prevents reflecting an unbounded attacker-controlled string before signature verification. Good catch, well tested.
  • No secrets/PII leakage: InfaktConnectionTesterAdapter.toFailure explicitly avoids leaking InfaktApiError.responseBody, with a test asserting it.

Verdict

Approve — no blocking or important issues across the full PR. The four SUGGESTIONs above are minor and mostly already tracked or self-acknowledged in the PR's own commit messages; none block merge.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Confirmed fixed: amounts now correct against the real sandbox

Re-ran the same live E2E check on a fresh seeded order after pulling 651cac28.

Order: 1 x product, gross total PLN 189.00 (net PLN 153.66 @ 23% VAT, tax PLN 35.34).

inFakt's own record for the resulting invoice (GET /invoices/{uuid}.json):

"net_price": 15366, "tax_price": 3534, "gross_price": 18900

18900 groszy = PLN 189.00 — exact match. KSeF cleared it for real (ksef_number: "8201194127-20260702-010EC0C00000-0A", status: "success").

Correction, adding a second line at PLN 99.99 gross on top of the original 189.00:

"net_price": 23495, "tax_price": 5404, "gross_price": 28899

28899 groszy = PLN 288.99 = 189.00 + 99.99 — exactly right, confirming the "rebuild the complete corrected document" behavior combines original + corrected lines correctly too.

OL's own UI reflects it end to end — order total PLN 189.00 shown identically on the order detail page, the invoice panel, and the dedicated invoice detail page (which also renders correctly now, for what it's worth — the earlier 404 I hit on that route must have been a stale branch snapshot on my end, not a real gap).

Great fix, thanks for the quick turnaround. No further blockers from my side on the money-format front.

Addresses the three outstanding SUGGESTIONs from the full-PR re-review
(2026-07-01T22:11:12Z), none of which were blocking:

- infakt-inbound-webhook-decoder.adapter.ts: drop the redundant
  parenthesization around parsed.resource['invoice_uuid'] flagged as a
  merge-artifact.
- webhook.controller.ts: add a one-line comment pointing at the
  res.status(HttpStatus.OK) call, since @res is the only place this
  controller reaches for the raw Response object.
- infakt-invoicing.adapter.ts: clarify sendToKsef's public-visibility
  rationale — it already has a real external caller
  (scripts/poc-sandbox-test.ts), not just a hypothetical future one.

Verified: pnpm --filter @openlinker/integrations-infakt test (108
passing), pnpm --filter @openlinker/api test -- webhook (608 passing),
type-check clean on both packages, targeted eslint clean on all three
files.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
…rified

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>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Remaining review suggestions addressed (0bca26e)

Went through both submitted reviews plus the full re-review comment thread and confirmed all IMPORTANT/blocking items were already resolved in prior commits (handshake status 200, handshake/signed-event disambiguation, two-step issue+submit retry-safety documentation, groszy amount fix, ConnectionTester registration). This commit cleans up the three remaining open SUGGESTIONs from the last full re-review (2026-07-01T22:11:12Z), all cosmetic/documentation-only:

  1. infakt-inbound-webhook-decoder.adapter.ts — removed the redundant extra parens around parsed.resource['invoice_uuid'] ((parsed.resource['invoice_uuid'])parsed.resource['invoice_uuid']), flagged as looking like a merge artifact.
  2. webhook.controller.ts — added a one-line comment next to the @Res({ passthrough: true }) parameter pointing at the res.status(HttpStatus.OK) call below, since it's the first (and only) place in this controller that reaches for the raw Response object.
  3. infakt-invoicing.adapter.ts (sendToKsef visibility) — the reviewer noted it's public with only a hypothetical future caller ("consider reverting to private" if that doesn't materialize). Checked: it already has a real caller today, scripts/poc-sandbox-test.ts, so public visibility is justified now, not just anticipated. Updated the inline comment to say so explicitly instead of only citing the future manual-resubmit use case.

No behavior changes — pure polish, no new tests needed.

Verified: pnpm --filter @openlinker/integrations-infakt test (108 tests passing), pnpm --filter @openlinker/api webhook-scoped tests (608 tests passing), type-check clean on both @openlinker/integrations-infakt and @openlinker/api, targeted eslint clean on all three touched files.

Branch is even with main (no rebase needed) and pushed as 0bca26e8.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Finding: KSeF "success" maps to `cleared` instead of `accepted` — terminal state never renders

While re-running the live sandbox E2E on a third fresh order, this time letting the real scheduled `invoicing.regulatoryStatus.reconcile` job pick up the clearance (rather than manually poking the DB like my earlier runs), the invoice ended up with:

"regulatoryStatus": "cleared",
"clearanceReference": "8201194127-20260702-036663400000-EA"

`toRegulatoryStatus` in `infakt-invoicing.adapter.ts`:

case 'success':
  return 'cleared';

But per the core `RegulatoryStatusValues` contract (`apps/web/src/features/invoicing/api/invoicing.types.ts`):

Terminal success is `accepted` (KSeF maps 200 -> accepted); `cleared` is reserved for split-clearance regimes and no current provider emits it, so the FE never renders a `cleared` success label.

KSeF's own `success` status is the terminal accepted state for inFakt too (same semantic as KSeF's own adapter's 200 -> `accepted` mapping) — there's no split-clearance regime here that would justify `cleared`. Because the FE's `InfaktInvoiceDetailSection` (and the shared `reg-card` treatment) only branch on `submitted` / `accepted` / `rejected`, an invoice stuck at `cleared` renders as:

  • Badge shows "KSeF: CLEARING" (a non-terminal, in-progress tone) forever
  • No clearance-reference chip shown at all, even though `clearanceReference` is populated in the DB
  • Operator has no visual indication the invoice actually, genuinely cleared on the government side

This one's easy to miss in testing because my earlier runs happened to reach `accepted` — but that was from me manually UPDATE-ing the DB row while investigating the money-format bug, not from the real reconcile path. This run is the first time I let the actual scheduled job do it, and it exposed the mismatch.

Suggested fix: change the mapping to

case 'success':
  return 'accepted';

Happy to re-verify against the sandbox again once that's in — same seeded connection is still up.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
… 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>
Found during a live E2E walkthrough (letting the real scheduled
invoicing.regulatoryStatus.reconcile job pick up clearance, rather than
manually poking the DB): toRegulatoryStatus mapped Infakt's terminal
ksef_data.status: 'success' to the neutral 'cleared' instead of 'accepted'.

Per the core RegulatoryStatus contract, 'cleared' is reserved for
split-clearance regimes that no current provider emits; the FE's status
card only branches on submitted/accepted/rejected, so an invoice stuck at
'cleared' rendered as a permanently in-progress "CLEARING" badge with no
clearance-reference chip, even though the invoice had genuinely cleared on
the government side. KSeF's own adapter already maps its terminal 200
status to 'accepted' for the identical reason - Infakt now mirrors that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Fixed: success now maps to accepted, not cleared

Confirmed against KSeF's own adapter precedent (ksef-invoicing.adapter.ts / ksef-clearance-status.mapper.ts, which maps its terminal 200 status to accepted for the identical reason). Fixed in libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts:

case 'success':
  return 'accepted';

Updated the docblock on toRegulatoryStatus explaining why cleared is wrong here (reserved for split-clearance regimes no current provider emits) and updated the parametrized test asserting the mapping.

Verified: pnpm --filter @openlinker/integrations-infakt test (108 tests passing), pnpm --filter @openlinker/integrations-infakt type-check, full pnpm lint + pnpm type-check — all clean. Pushed as c003797b.

Would appreciate a re-verify against your seeded sandbox connection when convenient — thanks for catching this with the real scheduled reconcile job rather than a manual DB poke, that's exactly the kind of gap unit tests can't catch.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Confirmed fixed: accepted state renders correctly now

Re-ran on a fresh order (PLN 259.00) after pulling `c003797b`. This time the invoice reached KSeF acceptance and the panel shows exactly what it should:

  • Badge: KSeF: ACCEPTED (was stuck on "CLEARING")
  • Clearance reference chip populated: `8201194127-20260702-04CF63400000-1D`
  • Money still exact: `gross_price: 25900` groszy = PLN 259.00

Correction issued on top also checks out: PLN 259.00 + 99.99 = 358.99 PLN, confirmed against the sandbox (`gross_price: 35899`).

Both the money-format fix and this status-mapping fix are holding up together. No further blockers from my side.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Confirming once more, in reply to #1293 (comment) — this fix worked. Re-verified end to end on a fresh sandbox order (PLN 259.00) with both the money-format fix and this status-mapping fix together: gross amount round-trips exactly, and the invoice correctly shows KSeF: ACCEPTED with a real clearance reference chip instead of getting stuck on CLEARING. Screenshots posted on PR #1300.

Thanks for the fast turnaround on both of these — no outstanding blockers from my side.

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (head c003797, rebased onto main) — APPROVE pending int-suite

Re-reviewed the non-draft, rebased head. Branch is cleanly rebased on current main (merge-base == main head); the #1292 grossToNet/taxRateNumeric refactor is now upstream and the rebase-conflict commit folded this branch's empty-taxRate fallback onto the real call path with the dead duplicate dropped.

Both prior findings remain resolved

  • Handshake echo returns HTTP 200 (webhook.controller.tsres.status(HttpStatus.OK)), verified against Infakt's live verifier.
  • getVerificationEcho is gated on !('event' in parsed) and caps verification_code at 256 chars.

Verification points

  • #917 jest mappers: present in BOTH apps/api/test/jest-integration.cjs and apps/worker/test/jest-integration.cjs.
  • Additive detectHandshake? port + return-threading: method is optional; WebhookService calls it via decoder.detectHandshake?.(...). DefaultWebhookDecoder and the InPost decoder don't declare it, so the optional-chain short-circuits to verify — no regression. New spec covers both the handshake-echo and the "proceeds to verify when null" paths.
  • invoicing routing: invoicing → invoicing.regulatoryStatus.reconcile, gated on Invoicing across BOTH supportedCapabilities and connection.enabledCapabilities (pure reads). Payload satisfies RegulatoryStatusReconcilePayloadV1.
  • Plugin registration: InfaktIntegrationModule added to both apps' plugin lists; decoder registered provider-keyed, translator adapterKey-keyed, plus the new InfaktConnectionTesterAdapter.

Notes (non-blocking)

  • Merge gate: Type Check / Build / Lint / unit Test / PHP / Scaffolded builds are green; Integration Tests is still in-progress. Since this PR adds an Invoicing-capability connection and a new inbound domain, the routing int-specs are load-bearing — please hold merge until that check is green.
  • Scope: the title understates several E2E-surfaced fiscal fixes bundled here — most importantly the integer-groszy wire-format correction (invoices were being issued at ~1/100th of the real total). All are correct and CI-green; a one-line note in the PR body would help future readers. payment_method: 'cash' is correctly deferred to #1303/#1309.
  • The two-step issue→sendToKsef retry-safety assumption is documented at both call sites with error-path tests — no change needed.

Verdict: APPROVE, contingent only on the in-progress Integration Tests check passing. No code changes requested.

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Addressed remaining review note

Piotr's latest (APPROVED) re-review had no blocking findings and no code changes requested — the only actionable item was documentation: the PR title/summary understated the E2E-surfaced fiscal fixes bundled alongside the plugin registration/webhook wiring.

Change made: added a "Scope note" to the PR description calling out the two most significant fixes surfaced during review's live sandbox testing:

  • the integer-groszy wire-format correction (invoices were being sent at ~1/100th of the real total)
  • the KSeF success -> accepted regulatory-status mapping fix (previously mapped to cleared, which the FE never renders as a terminal state)

Both were already implemented, tested, and confirmed against the sandbox in prior commits — this only makes them visible to future readers of the PR description, per the review note.

No source changes were needed for this pass. The only other open item from the review is mechanical (waiting for the Integration Tests CI check to go green before merge).

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 2, 2026
… correction

Implements the frontend half of the inFakt accounting integration
(epic #1279): guided connection setup (name + API key + optional
sandbox baseUrl, mirroring the Erli pattern), credentials rotation,
post-create baseUrl editing, and the invoice-surfacing slots
(regulatory status + KSeF number, KOR correction flow) driven by the
existing generic InvoicingPort/CorrectionIssuer backend contract
(PR #1292/#1293).

Also introduces a shared `.reg-card` severity-stripe treatment for the
invoiceDetailSection provider slot (additive alongside each section's
existing root class, so KSeF/Subiekt keep their current DOM shape and
tests) — inFakt ships with it from day one, KSeF/Subiekt adopt it as a
drive-by improvement.

Closes #1282

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 2, 2026
…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>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 2, 2026
…rified

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>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 2, 2026
… 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>

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tech Lead re-review — head 31f0854 (was c003797)

Re-reviewed the two commits added since my approval. Both are pure Merge branch 'main' (271b0cb, 31f0854); they pulled in #1291 (KSeF default tax-rate) and #1312 (int-spec cleanup) from main. The c003797b..HEAD diff touches only KSeF and integration-test-helper files carried over from main — zero Infakt/webhook/routing files were re-touched, and the merges resolved with no conflict artifacts on this PR's own surface. No new hand-written code.

Prior-findings status (all intact at head):

  • #917 jest-integration mappers for @openlinker/integrations-infakt present in both apps/api and apps/worker jest-integration.cjs.
  • Plugin registered in both hosts' plugins.ts.
  • Handshake echo returns HTTP 200 (res.status(HttpStatus.OK)), route default stays 202.
  • getVerificationEcho gated on !('event' in parsed) + MAX_VERIFICATION_CODE_LENGTH = 256.
  • Additive optional detectHandshake? port method — non-breaking for DefaultWebhookDecoder/InPost.
  • invoicing routing gated on the Invoicing capability → invoicing.regulatoryStatus.reconcile.

No regressions, no new findings.

CI: Lint, Type Check, PHP Unit Tests, Scaffolded Builds are green; Integration Tests is in_progress and Test / Build are queued on the freshly-merged head (mergeable_state: unstable). The main-merges re-triggered the full pipeline — the Infakt surface is byte-identical to the approved state, so this is just a wait.

Verdict: Approve — contingent, as before, on Integration Tests (and Test/Build) completing green. Please don't merge until the pipeline lands green.

@norbert-kulus-blockydevs norbert-kulus-blockydevs merged commit 61d5e5c into main Jul 2, 2026
8 checks passed
piotrswierzy pushed a commit that referenced this pull request Jul 2, 2026
… correction (#1300)

* feat(infakt): register Infakt plugin in API/worker + wire webhook ingestion

Registers InfaktIntegrationModule in apps/api and apps/worker so the host
can resolve the Infakt 'Invoicing' capability, and wires Infakt's KSeF-relay
webhooks into OL's ADR-021/ADR-015 ingestion pipeline:

- InfaktInboundWebhookDecoderAdapter (provider-keyed): HMAC-SHA256 verify,
  subscription-verification handshake echo, envelope extraction
- InfaktWebhookEventTranslatorAdapter (adapterKey-keyed): maps
  send_to_ksef_success/error to the new `invoicing` inbound domain
- InboundWebhookDecoderPort grows an optional detectHandshake step (ADR-021)
  so a provider's subscription-verification ping can echo a body before
  signature verification — WebhookService/WebhookController thread the
  return value through
- InboundRoutingPolicyService routes `invoicing` events to the existing
  invoicing.regulatoryStatus.reconcile job (webhook as trigger, not source
  of truth — the scheduled reconcile still drains the full frontier)

Closes #1281

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): address PR #1293 tech review suggestions

Shorten the wrap-around import path in both webhook adapters
(infrastructure/adapters -> infrastructure/webhooks was going through
infrastructure twice), and replace the dummy-secret
`new InfaktWebhookTranslator({ secret: '' }, logger)` construction with
a named `InfaktWebhookTranslator.forParsing(logger)` factory that makes
the secret-independent-parsing-only intent explicit in the type itself.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): correct field names, KSeF submission, and handshake status found in manual QA

Manual E2E testing against the real Infakt sandbox surfaced 8 issues
blocking real invoice issuance/correction and webhook verification:

- upsertCustomer sent `name`/`post_code`; Infakt's v3 API wants
  `company_name`/`postal_code` (rejected/ignored otherwise)
- issueInvoice hardcoded payment_method: 'transfer', which 422s without
  a configured bank account; defaulted to 'cash' (proven live in the
  June 30 POC)
- issueInvoice/issueCorrection sent client_uuid; Infakt's invoices.json
  only accepts the numeric client_id
- issueCorrection never sent a client at all, so every correction 422'd
- empty InvoiceLine.taxRate (core's documented contract) cascaded into
  a rejection on every single invoice line; added a Polish-standard-VAT
  (23%) fallback
- InfaktInvoicingAdapter.sendToKsef() existed but was never called from
  issueInvoice/issueCorrection (only a standalone POC script called it
  directly) - every Infakt invoice sat in `draft` forever. Now called
  inline, matching how KSeF submits inline and Subiekt transmits
  natively at issuance.
- InfaktInboundWebhookDecoderAdapter.extractEnvelope always routed
  regardless of event name instead of using the `ignore` outcome for
  events OL doesn't act on
- The webhook handshake echo returned 202; Infakt's own verifier only
  accepts 200

Confirmed end-to-end against the real sandbox: issue -> auto KSeF
submission -> real Infakt webhook (real HMAC) -> reconcile job ->
regulatoryStatus=cleared with a real KSeF number, for both the
original invoice and its correction.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): disambiguate verification handshake from signed events

Address review feedback on #1293: gate getVerificationEcho on the
absence of the event envelope so a signed webhook that happens to
carry a verification_code field in its resource is never
mis-short-circuited into the handshake path. Also cap the echoed
verification_code length, since the handshake echo is returned
pre-signature-verification and shouldn't reflect an unbounded
attacker-controlled value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): resolve rebase conflict duplicating taxRateNumeric

Rebasing onto main (which already carries #1292's grossToNet/taxRateNumeric
module-level refactor) alongside this branch's own empty-taxRate fix produced
a duplicate: an unused private taxRateNumeric method plus the real,
still-unpatched module-level function actually called by grossToNet. Fold
the empty-string fallback into the module-level function that's on the real
call path and drop the dead duplicate; extend the two issueCorrection tests
exercising send_to_ksef.json to seed the response now that issueCorrection
always submits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): address PR #1293 tech-review findings on KSeF submission

- Document the retry-safety assumption behind the two-step issue+submit
  flow at both sendToKsef call sites: a retry re-POSTs invoices.json with
  the same external_id, relying on Infakt returning/reusing the same
  invoice uuid rather than creating a duplicate draft, so the follow-up
  sendToKsef becomes a safe re-attempt on the same document.
- Add error-path tests for issueInvoice and issueCorrection covering
  sendToKsef failing after a successful invoice creation, asserting the
  InfaktApiError/failureMode propagates.
- payment_method being hardcoded to 'cash' is tracked separately as #1303
  (no neutral paymentMethod field exists on IssueInvoiceCommand yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): register ConnectionTesterPort for infakt.accounting.v1

Found during a live E2E walkthrough of the FE plugin (PR #1300): "Test
connection" fails with "Connection testing is not supported for adapter
infakt.accounting.v1" because infakt-plugin.ts never registered a
ConnectionTesterPort. Every other adapter with a Test connection affordance
(Erli, WooCommerce, PrestaShop, InPost, Allegro, Subiekt) has one; Infakt was
the one gap.

Add InfaktConnectionTesterAdapter mirroring SubiektConnectionTesterAdapter's
pattern (Infakt's factory only exposes createInvoicingAdapter, not a bare
HTTP-client construction seam, so credentials are resolved and the client
built directly rather than via a factory method): probes GET clients.json
(cheap, side-effect-free, requires a valid key), maps InfaktApiError to the
neutral ConnectionTestResult, and never throws. Register it in
infakt-plugin.ts alongside the existing validator/retry-classifier/webhook
registrations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): send invoice amounts as plain-integer groszy, not decimal strings

Critical fiscal bug found during live E2E against the real inFakt sandbox:
issued invoices landed on inFakt's dashboard (and were submitted to KSeF)
at ~1/100th of the real order total (e.g. a PLN 349.00 order → PLN 3.48 on
the resulting invoice). Confirmed against both the raw sandbox response
(net_price/tax_price/gross_price returned as plain integers, e.g. 283 for
PLN 2.83) and the official Infakt API schema (unit_net_price/net_price/
gross_price are `integer`, documented "w groszach") that inFakt's wire
format is a plain integer count of groszy (1 PLN = 100 groszy) everywhere
- not the "amount currency" decimal-string format (e.g. "283.74 PLN") the
adapter was previously sending, which the earlier #1292 review had
incorrectly confirmed against the schema.

- InfaktInvoice.gross_price/net_price/tax_price and
  InfaktInvoiceService.unit_net_price/net_price/tax_price/gross_price are
  now typed `number` (plain integer groszy), not `string`.
- Added toGroszy/fromGroszy helpers replacing the removed parseInfaktAmount;
  issueInvoice and issueCorrection now round PLN amounts to integer groszy
  when building the request payload, and convert the original invoice's
  groszy amounts back to PLN decimals before doing gross-to-net arithmetic.
- Updated all affected unit tests (fixtures + assertions) to the integer
  groszy format and to assert exact groszy values rather than decimal
  strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): polish remaining review suggestions on PR #1293

Addresses the three outstanding SUGGESTIONs from the full-PR re-review
(2026-07-01T22:11:12Z), none of which were blocking:

- infakt-inbound-webhook-decoder.adapter.ts: drop the redundant
  parenthesization around parsed.resource['invoice_uuid'] flagged as a
  merge-artifact.
- webhook.controller.ts: add a one-line comment pointing at the
  res.status(HttpStatus.OK) call, since @res is the only place this
  controller reaches for the raw Response object.
- infakt-invoicing.adapter.ts: clarify sendToKsef's public-visibility
  rationale — it already has a real external caller
  (scripts/poc-sandbox-test.ts), not just a hypothetical future one.

Verified: pnpm --filter @openlinker/integrations-infakt test (108
passing), pnpm --filter @openlinker/api test -- webhook (608 passing),
type-check clean on both packages, targeted eslint clean on all three
files.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): map KSeF success status to accepted, not cleared

Found during a live E2E walkthrough (letting the real scheduled
invoicing.regulatoryStatus.reconcile job pick up clearance, rather than
manually poking the DB): toRegulatoryStatus mapped Infakt's terminal
ksef_data.status: 'success' to the neutral 'cleared' instead of 'accepted'.

Per the core RegulatoryStatus contract, 'cleared' is reserved for
split-clearance regimes that no current provider emits; the FE's status
card only branches on submitted/accepted/rejected, so an invoice stuck at
'cleared' rendered as a permanently in-progress "CLEARING" badge with no
clearance-reference chip, even though the invoice had genuinely cleared on
the government side. KSeF's own adapter already maps its terminal 200
status to 'accepted' for the identical reason - Infakt now mirrors that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* docs(plans): implementation plan for inFakt FE plugin + invoice-section redesign

Covers issue #1282 (guided setup, credentials rotation, invoice detail
section, KOR correction flow) plus the OrderInvoicePanel regulatory-section
host-chrome redesign validated in the approved mockup. Corrects the issue's
original setup-flow description against verified codebase precedent (Erli's
guided-route pattern, not the generic inline create form).

Closes #1282

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* docs(plans): add E2E Playwright evidence phase to inFakt FE plan

Adds Phase 4: two Playwright walkthrough scripts (connection setup,
invoice issuance through real KSeF clearance + a correction) mirroring
the existing Subiekt/Erli screenshot-evidence convention, plus posting
the resulting docs/assets/infakt/*.png screenshots inline in a PR
comment as end-to-end proof.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* feat(web): inFakt FE plugin — connection setup + invoice detail + KOR correction

Implements the frontend half of the inFakt accounting integration
(epic #1279): guided connection setup (name + API key + optional
sandbox baseUrl, mirroring the Erli pattern), credentials rotation,
post-create baseUrl editing, and the invoice-surfacing slots
(regulatory status + KSeF number, KOR correction flow) driven by the
existing generic InvoicingPort/CorrectionIssuer backend contract
(PR #1292/#1293).

Also introduces a shared `.reg-card` severity-stripe treatment for the
invoiceDetailSection provider slot (additive alongside each section's
existing root class, so KSeF/Subiekt keep their current DOM shape and
tests) — inFakt ships with it from day one, KSeF/Subiekt adopt it as a
drive-by improvement.

Closes #1282

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* test(web): add inFakt E2E screenshot-evidence scripts

Playwright walkthroughs (apps/web/e2e/infakt-connection.mjs,
infakt-invoice.mjs) mirroring the existing Subiekt/Erli proof-capture
convention. Not part of pnpm test / CI — manual-run evidence scripts
against a live stack + the real inFakt sandbox, producing
docs/assets/infakt/*.png for the PR comment and the future operator
setup guide.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* 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>

* fix(web): address inFakt FE plugin tech-review findings

Ports the KSeF correction flow's line-delta validation guard into the
inFakt correction flow so a filled line number without a quantity or
price change is rejected client-side (matching the backend's
HasCorrectionDeltaConstraint) instead of silently 400ing. Extracts the
`.reg-card` tone mapping duplicated across KSeF, Subiekt, and inFakt
detail sections into a single `regCardToneFor` helper on the invoicing
feature barrel. Also applies the review's non-blocking suggestions:
narrows the helper's return type, makes InvoiceCorrectionFlowProps.connection
optional (no implementer reads it), and gives the `.textarea` class and
`.reg-card` note paragraphs real CSS rules instead of relying on bare
element selectors / inline styles.

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>

* feat(web): Infakt default-payment-method picker (wizard + edit disclosure)

Adds a "Default payment method" field to the inFakt connection wizard
(defaults to Transfer, the common case for online-shop invoicing) and
the equivalent field to the edit-connection screen, tucked behind a
new InlineDisclosure primitive (shared/ui) so it reads as an inline
fact ("Payment method for invoice: Cash") rather than a permanently-
open control. Wires apps/web/src/features/connections/components/
infakt-setup-form.tsx, infakt-setup.schema.ts, edit-connection.schema.ts,
EditConnectionForm.tsx and plugins/infakt/components/infakt-structured-
section.tsx to the config.defaultPaymentMethod field introduced by #1303.

Related to #1303.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* docs(infakt): re-verify screenshots against current code post tech-review (order PLN 899.00)

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* docs(infakt): re-capture connection wizard screenshots with payment-method field

Rebuilt the web bundle after pulling the default-payment-method picker
feature (#1303) and re-ran the connection walkthrough — the wizard,
created/test/list states now show the new "Default payment method"
field. Live-verified against the real sandbox (Test connection passes).

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(web): address inFakt FE plugin PR #1300 tech-review findings

Default the inFakt setup wizard's payment method to cash - transfer 422s
on inFakt unless a bank account is configured, contradicting the help
copy and the adapter's own fallback. Also syncs the stale lazy-route
count comment (49 total, 10 plugin routes) and drops two unused
.reg-card CSS classes plus a raw-px rule in favor of the rem convention.

Signed-off-by: Norbert Kulus <norbert.kulus@silksh.pl>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(infakt): implement real PDF download via RegulatoryDocumentReader (#1323)

fix(infakt): implement real PDF download via RegulatoryDocumentReader (#1323)

InfaktInvoicingAdapter read a pdf_url field that does not exist on Infakt's
invoice resource (verified live against the sandbox), so InvoiceRecord.pdfUrl
was always null and the "download PDF" affordance never worked. Infakt does
expose a real PDF at GET /invoices/{uuid}/pdf.json?document_type=original,
so the adapter now implements RegulatoryDocumentReader for kind 'rendered'
against that endpoint, and the FE Infakt invoice detail section gets a
working Download PDF button wired to the existing /document?kind=rendered
route.

Closes #1321

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Signed-off-by: Norbert Kulus <norbert.kulus@silksh.pl>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 3, 2026
…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>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 3, 2026
…rified

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>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 3, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(infakt): register Infakt plugin in API + wire webhook ingestion

2 participants