Skip to content

feat(integrations): production-harden Infakt plugin - validators, retry classifier, NestJS module, tests#1292

Merged
piotrswierzy merged 5 commits into
mainfrom
1280-infakt-plugin-hardening-tests
Jul 1, 2026
Merged

feat(integrations): production-harden Infakt plugin - validators, retry classifier, NestJS module, tests#1292
piotrswierzy merged 5 commits into
mainfrom
1280-infakt-plugin-hardening-tests

Conversation

@norbert-kulus-blockydevs

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

Copy link
Copy Markdown
Collaborator

Summary

Production-hardens the Infakt accounting integration, turning the sandbox-validated POC (#1274/#1275, closed without merging) into a real plugin. Part of the Infakt epic (#1279).

  • Adds InfaktConnectionConfigShapeValidatorAdapter / InfaktConnectionCredentialsShapeValidatorAdapter (ConnectionConfigShapeValidatorPort / ConnectionCredentialsShapeValidatorPort) so creating a connection with a malformed baseUrl or missing apiKey fails fast with a 400 instead of surfacing as a runtime error on first invoice issuance
  • Adds InfaktRetryClassifierAdapter for fiscal-safety-correct retry semantics: 4xx (except 429) → rejected (terminal, safe to reissue), 429 → transient (rate-limited), 5xx/network → in-doubt (the document may already exist server-side - must not blind-retry)
  • Wires InfaktIntegrationModule via createNestAdapterModule(createInfaktPlugin()) (no plugin-specific Nest providers needed) and exports it from the package barrel so a host app can register the plugin
  • Wires infakt-plugin.ts register() to register all three side-registrations above (previously a no-op)
  • Carries over the adapter source from the POC (factory, HTTP client, invoicing adapter, webhook translator, exception/type definitions) since docs(invoicing): Infakt invoicing spike — feasibility artifact + POC plan #1275 was closed unmerged - this PR is where that code first lands on main
  • Removes unused class-transformer / class-validator deps from libs/integrations/infakt/package.json
  • Adds unit tests: fake HTTP client, adapter (happy + error path per method), HTTP client, webhook translator, factory, both shape validators - 7 suites / 81 tests
  • Addresses PR review findings: gross->net conversion in issueCorrection (fiscal-correctness bug), moved POC sandbox script out of src/, stopped logging raw HTTP error bodies (PII), domain InfaktConfigException instead of a bare Error in the factory

Closes #1280

Test plan

  • pnpm --filter @openlinker/integrations-infakt test - 7 suites, 81 tests passing
  • pnpm lint
  • pnpm type-check
  • Manual: create an Infakt connection with an empty API key → expect 400 with errors[]
  • Manual: create an Infakt connection with valid credentials → expect validation to pass

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Tech review: PR #1292 (feat(integrations): production-harden Infakt plugin)

Summary

Solid, well-tested hardening pass that follows the KSeF/Subiekt plugin precedent closely (validators, retry classifier, createNestAdapterModule wiring, capability guards). Test coverage is thorough (7 suites / 80 tests, happy + error paths per method). The main concerns are a couple of correctness gaps in InfaktInvoicingAdapter (hardcoded invoice_type, unrounded floating-point price strings) and a documented-standard deviation (inline types instead of *.types.ts) that the sibling KSeF plugin does not have. Nothing here blocks merge, but the two IMPORTANT items should be fixed or explicitly deferred with a follow-up issue before this ships to production traffic.

Issues

[IMPORTANT] - libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts:199 (getInvoice) and :212 (getClearanceStatus)

invoice_kind_to_type(invoice_uuid_kind(providerInvoiceId)) always evaluates to 'vat' - invoice_uuid_kind ignores its argument and returns a hardcoded string, so the composition is dead complexity around a constant. More importantly, getClearanceStatus calls this.http.get(..., { invoice_type: 'vat' }) unconditionally, so looking up a correction document (kind: 'corrective', produced by issueCorrection) queries Infakt with the wrong invoice_type and will likely 404 or return stale data. Since CorrectionIssuer is declared as a supported capability, getClearanceStatus/getInvoice on a correction record is a real code path, not hypothetical. Either resolve invoice_type from the stored InvoiceRecord.documentType (available in getClearanceStatus's record param) or drop the pretend-dynamic helpers and track the limitation explicitly in a follow-up issue if 'vat'-only is an acceptable v1 scope.

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

unit_net_price: ${l.unitPriceGross / (1 + this.taxRateNumeric(l.taxRate))} ${currency ?? 'PLN'}`` does floating-point division with no rounding, so a value like 123 / 1.23 produces `"100.00000000000001 PLN"` (or a long repeating decimal for other tax rates). This gets sent verbatim to Infakt's API as the price string. Money math should round to 2 decimal places before interpolating into the payload (e.g. `.toFixed(2)`), same as `issueCorrection`'s `corrPrice` branch, which also has this gap (`svc.unit_net_price` / `corrLine.newUnitPriceGross` interpolated raw).

[IMPORTANT] - libs/integrations/infakt/src/application/infakt-adapter.factory.ts:15-19

InfaktCredentials and InfaktConnectionConfig are defined inline in the factory file rather than in a *.types.ts file, which is a documented MUST in docs/engineering-standards.md § Type Definitions in Separate Files. The sibling KsefAdapterFactory (libs/integrations/ksef/src/application/factories/ksef-adapter.factory.ts) follows the rule correctly - KsefConnectionConfig/KsefCredentials live in domain/types/ksef-connection.types.ts. Move both interfaces into libs/integrations/infakt/src/domain/types/infakt.types.ts (or a new infakt-connection.types.ts) for consistency with the established plugin pattern.

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

invoice_uuid_kind and invoice_kind_to_type are snake_case function names, which violates docs/engineering-standards.md § Variables and Functions (functions must be camelCase). If the dead-constant issue above is fixed by removing these helpers, this becomes moot; otherwise rename to inferInvoiceKind / mapInvoiceKindToType.

[SUGGESTION] - libs/integrations/infakt/src/infakt-plugin.ts:557-578 (createCapabilityAdapter)

The try { ... } catch (err) { return Promise.reject(err as Error); } wrapper is a no-op - an async function already returns a rejected promise on a thrown error, so this adds nothing except an unsound err as Error cast (the caught value isn't guaranteed to be an Error). Safe to delete the try/catch entirely.

[SUGGESTION] - libs/integrations/infakt/src/application/infakt-adapter.factory.ts

InfaktAdapterFactory has no implements clause / interface, unlike KsefAdapterFactory implements IKsefAdapterFactory. Not enforced by CI for plugin packages, but worth aligning with the sibling plugin's pattern for consistency, especially since this factory is on the standard "single per-connection construction seam" path other plugins document explicitly.

Verdict

🔄 Approve with changes - no architecture or security violations; the IMPORTANT items are real correctness gaps in the correction/clearance-lookup and price-formatting paths that should be fixed (or explicitly deferred via a tracked follow-up) before this handles live fiscal documents.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
- Resolve invoice_type from InvoiceRecord.documentType in getClearanceStatus,
  and try vat then corrective in getInvoice, instead of a dead-constant
  helper pair that always evaluated to 'vat' (was returning wrong/404 data
  for correction lookups)
- Round unit_net_price/corrPrice to 2 decimals (toFixed) to avoid raw
  floating-point strings like "100.00000000000001" in the Infakt payload
- Move InfaktCredentials/InfaktConnectionConfig into
  domain/types/infakt-connection.types.ts, mirroring the KSeF plugin
- Add IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory
- Drop the no-op try/catch wrapper in createCapabilityAdapter

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

Addressed all IMPORTANT and SUGGESTION items from the tech review (80e98c1):

IMPORTANT

  • getClearanceStatus now resolves invoice_type from InvoiceRecord.documentType (correctedcorrective, proformaproforma, else vat) instead of the dead-constant invoice_kind_to_type(invoice_uuid_kind(...)) pair, so correction-status lookups hit the right Infakt kind. getInvoice has no InvoiceRecord to read a kind from, so it now tries vat then corrective in turn (skipping on 404, propagating any other error).
  • unit_net_price / corrPrice are now rounded with .toFixed(2) before interpolation, so the Infakt payload never carries raw floating-point strings like "100.00000000000001".
  • InfaktCredentials / InfaktConnectionConfig moved out of the factory file into domain/types/infakt-connection.types.ts, mirroring the KSeF plugin's ksef-connection.types.ts layout.

SUGGESTION

  • The dead invoice_uuid_kind / invoice_kind_to_type snake_case helpers are deleted (moot once the dead-constant issue was fixed).
  • Removed the no-op try { ... } catch (err) { return Promise.reject(err as Error) } wrapper in createCapabilityAdapter - an async function already rejects on a thrown error.
  • Added IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory, aligning with the IKsefAdapterFactory sibling pattern.

All 7 suites / 80 tests still pass; type-check and lint are clean for the package.

@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 review: production-harden Infakt plugin

Reviewed at head 80e98c15 — this already includes the fixes from the earlier self-review (invoice_type resolution, price rounding, types moved to infakt-connection.types.ts, dead helpers + no-op try/catch removed). Thorough pass overall: it follows the KSeF/Subiekt precedent closely, ADR-026 neutrality holds (all nip/ksef/tax_symbol vocabulary stays in the package, core stays neutral), HostServices purity is intact, capability manifest lists only Invoicing with sub-capabilities narrowed via guards, no any/console in production code, and the apiKey is never logged. 80 tests cover happy + error paths per method.

One net-new correctness defect remains beyond the earlier review:

🔴 BLOCKING

libs/integrations/infakt/src/infrastructure/adapters/infakt-invoicing.adapter.ts:274-294 — correction writes a gross price into the net field

issueCorrection interpolates corrLine.newUnitPriceGross (a gross price, per IssueCorrectionCommandinvoicing.types.ts:233-234) straight into Infakt's unit_net_price, while issueInvoice:158 correctly converts gross → net first (l.unitPriceGross / (1 + taxRateNumeric(l.taxRate))). The fallback branch (svc.unit_net_price, already net) is correct, so a price-changing correction line overstates the net amount by the tax fraction and emits an incorrect fiscal correction document that Infakt forwards to KSeF. CorrectionIssuer is a declared capability core can invoke, so this is a live path. Please apply the same gross → net conversion (reuse taxRateNumeric with svc.tax_symbol) before .toFixed(2), ideally via a shared grossToNet helper both call sites use.

🟡 SUGGESTION

  • Correction currency is hardcoded PLN (l.276/294) while issueInvoice uses currency ?? 'PLN' — inconsistent; read currency from the command or add a comment.
  • src/scripts/poc-sandbox-test.ts sits under src/**, so it compiles into dist and ships to consumers, and its poc:sandbox npm script depends on ts-node/tsconfig-paths not in the package deps. No hardcoded secrets (env-driven). Consider moving it out of src/ or excluding it from the build.
  • HTTP client logs the raw error response body ({ body: text }, infakt-http-client.ts:78), which can carry buyer PII; consider trimming on non-error bodies (matches KSeF, low priority).
  • InfaktWebhookTranslator is exported from the barrel but doesn't implement the canonical WebhookEventTranslator port and is unwired (deferred to #1281) — effectively dead on main until then; confirm #1281 reshapes it to the CanonicalInboundEvent contract.
  • Factory throws a bare Error on missing credentialsRef rather than a domain exception (minor).

Process: branch is behind main (rebase before merge), and the repo-level pnpm lint / pnpm type-check checklist items are still unticked — get the full gate green first.

Verdict: Request changes — one fiscal-correctness bug in the correction price path. Everything else is solid and mirrors the KSeF precedent; fix the gross→net conversion in issueCorrection and this is an easy approve.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
- Resolve invoice_type from InvoiceRecord.documentType in getClearanceStatus,
  and try vat then corrective in getInvoice, instead of a dead-constant
  helper pair that always evaluated to 'vat' (was returning wrong/404 data
  for correction lookups)
- Round unit_net_price/corrPrice to 2 decimals (toFixed) to avoid raw
  floating-point strings like "100.00000000000001" in the Infakt payload
- Move InfaktCredentials/InfaktConnectionConfig into
  domain/types/infakt-connection.types.ts, mirroring the KSeF plugin
- Add IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory
- Drop the no-op try/catch wrapper in createCapabilityAdapter

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
… correction path

Fixes a fiscal-correctness bug: issueCorrection wrote a gross price straight
into Infakt's net-only unit_net_price field for price-changing correction
lines, overstating the corrected amount by the tax fraction. Extracts the
gross->net conversion issueInvoice already used into a shared grossToNet
helper and reuses it here.

Also addresses the review's suggestions: moves the POC sandbox script out of
src/ so it no longer compiles into dist, stops logging the raw Infakt error
response body (can carry buyer PII), and replaces the factory's bare Error
on a missing credentialsRef with a domain InfaktConfigException (matching
the DPD/KSeF precedent).

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1280-infakt-plugin-hardening-tests branch from 80e98c1 to a3b6823 Compare July 1, 2026 15:21
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Addressed all findings from the latest review (commit a3b6823):

BLOCKING

  • issueCorrection was writing a gross price (newUnitPriceGross) straight into Infakt's net-only unit_net_price field for price-changing correction lines, overstating the corrected amount by the tax fraction. Extracted the gross→net conversion issueInvoice already used into a shared grossToNet helper and reused it in issueCorrection (keyed off the original line's tax_symbol). Added a regression test asserting 61.50 PLN gross converts to 50.00 PLN net at a 23% rate.

Suggestions

  • Correction currency was hardcoded to 'PLN' inline in two places; extracted to a local currency const with a comment explaining why (Infakt's invoice type carries no currency field; PLN is the only value the API has ever returned).
  • Moved poc-sandbox-test.ts out of src/ into a top-level scripts/ folder so it no longer compiles into dist or ships to consumers; moved its ts-node/tsconfig-paths deps into devDependencies.
  • InfaktHttpClient no longer logs the raw error response body (could carry buyer PII) — the full body is still available to callers via InfaktApiError.responseBody.
  • InfaktAdapterFactory now throws a domain InfaktConfigException instead of a bare Error on a missing credentialsRef, matching the DPD/KSeF precedent.
  • InfaktWebhookTranslator's wiring remains deferred to feat(infakt): register Infakt plugin in API + wire webhook ingestion #1281 as noted in the review — no change needed here.

Branch is already even with main (0 ahead/behind), so no rebase was required. Package-scoped lint / type-check / test (81 tests, 7 suites) all pass; the repo-wide CI Lint and Type Check checks are green on this head.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
- Resolve invoice_type from InvoiceRecord.documentType in getClearanceStatus,
  and try vat then corrective in getInvoice, instead of a dead-constant
  helper pair that always evaluated to 'vat' (was returning wrong/404 data
  for correction lookups)
- Round unit_net_price/corrPrice to 2 decimals (toFixed) to avoid raw
  floating-point strings like "100.00000000000001" in the Infakt payload
- Move InfaktCredentials/InfaktConnectionConfig into
  domain/types/infakt-connection.types.ts, mirroring the KSeF plugin
- Add IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory
- Drop the no-op try/catch wrapper in createCapabilityAdapter

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Re-review — de23a27 (was 80e98c1)

Re-verified the prior BLOCKING finding and all suggestions. Verdict: Approve.

BLOCKING — gross→net in issueCorrection: ✅ ADDRESSED

infakt-invoicing.adapter.ts now extracts a shared grossToNet(unitPriceGross, taxRate) helper and applies it in both issueInvoice and issueCorrection. The corrected line uses grossToNet(corrLine.newUnitPriceGross, svc.tax_symbol), so correction documents are no longer overstated. Confirmed the conversion is correct across tax symbols (23 → 0.23; zw/np → 0, no division) and that the untouched-line fallback re-emits the original net price without double-converting. Exactly the fix requested, via a shared helper.

Suggestions

  • Correction currency hardcoded 'PLN' — ✅ addressed with justification. Confirmed IssueCorrectionCommand carries no currency field (unlike IssueInvoiceCommand), so 'PLN' + explanatory comment is the correct resolution.
  • poc-sandbox-test.ts under src/ — ✅ moved to scripts/, outside tsconfig rootDir/include; won't ship to dist.
  • HTTP client logging raw error body (PII) — ✅ now logs only method path → status; full body still reaches the caller via InfaktApiError.
  • Factory bare Error — ✅ now throws InfaktConfigException(message, connectionId).
  • InfaktWebhookTranslator unwired — ⏸ explicitly deferred to #1281 (host registration + webhook routing), documented in the integration-module docstring. Fine to defer.

New (non-blocking, for the sandbox POC to confirm)

InfaktInvoiceService.unit_net_price is typed number and the correction path calls .toFixed(2) on the value read back from Infakt. If Infakt v3 returns monetary fields in grosze (integer minor units) rather than decimal PLN, the echoed "before"/fallback rows would be 100× off. Unverified against the live API and equally applicable to any read path — worth confirming with the sandbox POC; no code change requested now.

Nice, clean resolution — approving. Run the standard lint/type-check/test gate before merge.

norbert-kulus-blockydevs and others added 5 commits July 1, 2026 20:10
…ry classifier, NestJS module, tests

Config/credentials shape validators, fiscal-safety retry classifier
(4xx->rejected, 429->transient, 5xx/network->in-doubt), and the
NestJS module via createNestAdapterModule, wired into register().
Adds unit test coverage for the adapter, HTTP client, webhook
translator, factory, and both validators.

Closes #1280

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
- Resolve invoice_type from InvoiceRecord.documentType in getClearanceStatus,
  and try vat then corrective in getInvoice, instead of a dead-constant
  helper pair that always evaluated to 'vat' (was returning wrong/404 data
  for correction lookups)
- Round unit_net_price/corrPrice to 2 decimals (toFixed) to avoid raw
  floating-point strings like "100.00000000000001" in the Infakt payload
- Move InfaktCredentials/InfaktConnectionConfig into
  domain/types/infakt-connection.types.ts, mirroring the KSeF plugin
- Add IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory
- Drop the no-op try/catch wrapper in createCapabilityAdapter

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

Fixes a fiscal-correctness bug: issueCorrection wrote a gross price straight
into Infakt's net-only unit_net_price field for price-changing correction
lines, overstating the corrected amount by the tax fraction. Extracts the
gross->net conversion issueInvoice already used into a shared grossToNet
helper and reuses it here.

Also addresses the review's suggestions: moves the POC sandbox script out of
src/ so it no longer compiles into dist, stops logging the raw Infakt error
response body (can carry buyer PII), and replaces the factory's bare Error
on a missing credentialsRef with a domain InfaktConfigException (matching
the DPD/KSeF precedent).

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
…g-paths devDeps

Companion to the previous commit's new devDependencies (needed to run the
relocated poc:sandbox script) — CI's frozen-lockfile install was failing
without this.

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

Address the non-blocking note from the PR #1292 re-review: Infakt's v3
schema documents unit_net_price/net_price/tax_price/gross_price as
"amount currency" strings (e.g. "100.00 PLN"), not plain numbers.
issueCorrection called .toFixed(2) on svc.unit_net_price assuming it was
a number, which would throw at runtime against the real API. Corrected
the wire types to string and added a parseInfaktAmount helper to read
the value back before formatting.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs norbert-kulus-blockydevs force-pushed the 1280-infakt-plugin-hardening-tests branch from de23a27 to 3d374fe Compare July 1, 2026 18:11
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Addressed the non-blocking follow-up from the latest re-review (commit de23a27c).

Fixed

InfaktInvoiceService.unit_net_price (and sibling monetary fields) were typed number, but Infakt's v3 API returns them as "amount currency" strings (e.g. "100.00 PLN") — confirmed against the official vat_invoice/corrective_invoice schema, not the grosze/minor-units concern raised in the review. issueCorrection called .toFixed(2) directly on svc.unit_net_price, which would throw a runtime TypeError against the real API since strings don't have .toFixed.

Fix:

  • InfaktInvoice.{gross,net,tax}_price and InfaktInvoiceService.{unit_net,net,tax,gross}_price are now typed string in infakt.types.ts, matching the documented wire shape.
  • Added a parseInfaktAmount(value: string): number helper in infakt-invoicing.adapter.ts and used it in issueCorrection before formatting the "before" row and the untouched-line fallback.
  • Updated test fixtures to use real "NN.NN PLN" string values and added a regression test asserting the untouched-line fallback path parses and re-formats correctly.

Also done

  • Rebased onto main (was behind).
  • pnpm --filter @openlinker/integrations-infakt test — 7 suites / 82 tests passing.
  • pnpm --filter @openlinker/integrations-infakt type-check — clean.
  • check-cross-context-imports / check-service-interfaces invariants — clean.

Ready for merge.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
- Resolve invoice_type from InvoiceRecord.documentType in getClearanceStatus,
  and try vat then corrective in getInvoice, instead of a dead-constant
  helper pair that always evaluated to 'vat' (was returning wrong/404 data
  for correction lookups)
- Round unit_net_price/corrPrice to 2 decimals (toFixed) to avoid raw
  floating-point strings like "100.00000000000001" in the Infakt payload
- Move InfaktCredentials/InfaktConnectionConfig into
  domain/types/infakt-connection.types.ts, mirroring the KSeF plugin
- Add IInfaktAdapterFactory interface, implemented by InfaktAdapterFactory
- Drop the no-op try/catch wrapper in createCapabilityAdapter

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@piotrswierzy piotrswierzy merged commit 4bf58d5 into main Jul 1, 2026
7 checks passed
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 added a commit that referenced this pull request Jul 1, 2026
…sueInvoiceResult

CI on PR #1231 failed lint/test/type-check: the merge-into-main brought in
InvoicingPort.issueInvoice's new IssueInvoiceResult wrapper (added on this
branch for the UPO/document-retrieval flow), but Infakt's adapter — merged
from main via #1292 — still implemented the pre-wrapper signature
(Promise<InvoiceRecord>), which the base `tsc -b` build across libs/**
fails on for every downstream job. Infakt has no seller lookup or adapter-
built source document (it submits to KSeF natively), so the fix wraps the
existing InvoiceRecord in `{ record }` and leaves seller/sourceDocument
unset. Updated the happy-path test to unwrap accordingly.

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

norbert-kulus-blockydevs commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Resolved in PR #1293 — invoice amounts are now sent as plain-integer groszy, not decimal strings. Moving discussion there since #1292 is already merged.

norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
#1224) (#1231)

* feat(invoicing): KSeF UPO endpoint + order-snapshot invoice projection (#1224)

Backend half of epic #1142 C8/C15 — the seam PR #1191 (FE) already consumes.

Neutral contract (ADR-026 — no nip/ksef/vat/jpk/faktura in libs/core):
- New `RegulatoryDocumentReader` sub-capability of `InvoicingPort`
  (`getUpo(record): Promise<RegulatoryDocument>` where `RegulatoryDocument =
  { content: Uint8Array; contentType: string }`) + `isRegulatoryDocumentReader`
  guard. The KSeF adapter implements it: decodes the composite providerInvoiceId
  and reads the session-scoped UPO endpoint as binary via a new
  `IKsefHttpClient.getExpectingBinary`.
- `GET /invoices/:invoiceId/upo` (apps/api/src/invoicing) — admin-gated; resolves
  the connection's Invoicing adapter, narrows to the clearance-document reader,
  streams the blob. 404 unknown id, 409 not-yet-cleared / provider can't confirm.
- Order-detail read merges a neutral `invoice` sub-tree
  ({ invoiceId, regulatoryStatus, clearanceReference, upoReference }) into the
  order snapshot via `findLatestByOrderId` — detail-only (no list N+1).

Tests: capability guard, adapter getUpo (incl. fakes), invoicing controller
(200/404/409 + unsupported-provider), orders invoice-projection enrichment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(invoicing): apply review findings to KSeF UPO seam (#1224)

- cap binary UPO response size; typed OrderInvoiceProjectionDto; neutral getRegulatoryDocument; deterministic findLatestByOrderId

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* feat(invoicing): invoice detail + issued-document content snapshot (#1224 W2)

Add the invoice detail read and the issued-document content snapshot behind the
neutral invoicing surface (ADR-026), backing the FE detail page + "Invoice
contents" card.

- Core: new neutral IssuedDocumentContent (§7.3 — seller/buyer/lines/vatBreakdown/
  totals/currency/dates/payment) on invoicing.types; new IssueInvoiceResult wraps
  the InvoicingPort.issueInvoice projection with an OPTIONAL adapter-resolved
  neutral seller block. InvoiceRecord + ORM entity gain a nullable documentContent
  (jsonb) column; repo toDomain/toOrm map it.
- Core: new InvoiceService (+ IInvoiceService) — getInvoiceById and an issueInvoice
  orchestrator that resolves the per-connection Invoicing adapter, persists the
  projection, and snapshots content (per-line net/vat/gross + VAT breakdown +
  totals computed from the command; seller from the adapter result, null when
  absent). Wired + exported via the core InvoicingModule/token/barrel.
- KSeF adapter: issueInvoice returns { record, seller } — seller mapped from its
  KsefSellerConfig onto the neutral { name, taxId{pl-nip}, address }. The pl-nip
  scheme tag stays behind the adapter; core never sees a NIP wire detail.
- API: GET /invoices/:invoiceId (InvoiceRecordResponseDto, 404) and
  GET /invoices/:invoiceId/content (IssuedDocumentContentDto, 404/409), declared
  before the single-segment route so /upo + /content match first.
- Migration 1813000000000-add-invoice-document-content adds the nullable jsonb
  column.
- Specs: service (incl. VAT math), repo round-trip, controller endpoints, KSeF
  adapter seller block.

Refs #1224

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(invoicing): apply W2 review findings (#1224)

Address the W2 review (invoice detail + issued-document content snapshot):
- Lint: resolve consistent-type-imports errors in the invoice DTOs +
  controller (type-only imports for type-position symbols; keep
  decorator-metadata-referenced types as value imports).
- Merge the split IssuedDocumentContent type import on the ORM entity
  (Prettier-conformant single line).
- KSeF adapter: normalize the neutral seller address line2 to `null` when
  the PL seller config omits it (snapshot fidelity).
- DTO: TotalsDto implements the neutral DocumentTotals; the totals field is
  typed DocumentTotals (single source of truth).

Refs #1224

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* feat(invoicing): persist FA(3) source XML + document fetch by kind (#1224 W3)

Back the KSeF FA(3) source-document download (and the seam for a future
human-readable rendering) on the neutral invoicing surface (ADR-026).

- Core types: neutral RegulatoryDocumentKind (`upo|source|rendered`) +
  StoredDocument (provider MIME + base64 bytes, jsonb-friendly).
  IssueInvoiceResult gains an optional `sourceDocument`; InvoiceRecord (+ORM)
  gains a nullable `sourceDocument` jsonb column; repo toDomain/toOrm + create
  input/patch map it. Migration 1814000000000 (strictly greater than W2's
  1813000000000) adds the column.
- Capability: RegulatoryDocumentReader.getRegulatoryDocument gains an optional
  `kind` (defaults to `upo` for back-compat). New neutral domain error
  UnsupportedRegulatoryDocumentKindError for a kind the provider cannot produce
  (soft 409, not a failure).
- KSeF adapter: issueInvoice returns the built FA(3) XML as a neutral base64
  source document so core persists it (no provider round-trip on read).
  getRegulatoryDocument serves `upo`; `source` is core-served from the snapshot
  and `rendered` is not a KSeF server-side capability — both throw
  UnsupportedRegulatoryDocumentKindError. Success maps to `accepted` (never
  `cleared`) — verified.
- API: GET /invoices/:invoiceId/document?kind=source|rendered beside the
  existing /upo. `source` streams the persisted blob (404/409); `rendered`
  goes through the adapter (409 when unsupported / not cleared); unknown kind
  is 400. /upo refactored onto the shared binary streamer.
- Specs: capability default-kind, adapter source-document + unsupported-kind,
  service source-document persistence, repo round-trip, controller document
  endpoint (source 200, default, 404, 409-no-snapshot, 400-bad-kind,
  409-rendered-unsupported, 409-not-cleared).

Stack note: landed on the #1231 stack head (1224-ksef-upo-endpoint) — the only
branch where RegulatoryDocumentReader + the invoicing controller + the FA(3)
builder coexist. #1231 bases on #1189 (1151-ksef-kor-corrections), so this W3
work sits above both and flows to main with the stack.

Refs #1224 #1151

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(invoicing): de-KSeF-ify core source-document fixtures + guard write-once (#1224 W3 review)

Review findings on W3:
- BLOCKING: two core spec fixtures used base64 decoding to <Faktura> (provider
  vocabulary in libs/core, violating ADR-026 neutral core). Replaced with neutral
  <Document>fake</Document> base64.
- IMPORTANT: updateOutcome relied only on the InvoiceOutcomePatch type to keep
  sourceDocument write-once. Added a runtime guard so a cast/untyped patch cannot
  clobber the persisted snapshot via Object.assign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ShufMjpq73tHU3W9EDsD
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(invoicing): apply rebase conflict resolution — InvoiceRecord ctor, SubiektAdapter return type (#1224)

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

* fix(invoicing): correct @get route — 'invoices/:invoiceId' not ':invoiceId'

With @controller() (no prefix), @get(':invoiceId') registers GET /:invoiceId
(one segment), which never matches GET /invoices/:id (two segments). Fixed by
making the full path explicit: @get('invoices/:invoiceId').

Discovered during full-environment smoke tests where GET /invoices/:id returned
404 despite the handler existing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WHzgkHr1p1DeynN5kMgC4
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(invoicing): add ParseUUIDPipe to GET /invoices/:invoiceId — return 404 on invalid id

Without UUID validation, a non-UUID path segment (e.g. /invoices/nonexistent)
reaches the DB query which throws a TypeORM error, producing 500. ParseUUIDPipe
with errorHttpStatusCode:404 short-circuits before the query and returns a clean
404 — consistent with the handler's own NotFoundException for an unknown but
valid UUID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WHzgkHr1p1DeynN5kMgC4
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(ksef): treat FA(3) XSD validation status 430 as non-terminal

Status code 430 (KSEF_STATUS_FA3_PROCESSING) signals that MF is running
secondary FA(3) XSD schema validation — the session is still in progress.
Adding it to SUBMITTED_CODES prevents the reconcile loop from treating it
as a rejection and prematurely marking the invoice as failed.

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

* fix(ksef): persist FA(3) source XML to invoice_records for kind=source download

The XML was built transiently and discarded after submission. Persist it
to InvoiceRecord.sourceDocument immediately after issue so the document
endpoint can serve the raw FA(3) XML on GET /invoices/:id/document?kind=source.

- Add sourceDocument to InvoiceOutcomePatch type (write-once field)
- Loosen repository guard: allow first-set (null→value), reject overwrite
- Destructure sourceDocument from IssueInvoiceResult and pass in success patch
- Fix spec: makeIssuedFromAdapter returns IssueInvoiceResult; W2/W3 tests
  capture documentContent/sourceDocument from updateOutcome patch, not create

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

* fix(invoicing): ADR-026 vocabulary renames + cross-context boundary fix (#1224)

Apply Piotr's review fixes:

B1 — ADR-026 vocabulary: rename `VatBreakdownEntry`→`TaxBreakdownEntry`,
`vatBreakdown`→`taxBreakdown`, all `.vat` fields→`.tax` in
`IssuedDocumentContent` / `DocumentTotals` / `IssuedDocumentLine`.
Rename `RegulatoryDocumentKindValues` entry `'upo'`→`'confirmation'`
(provider-neutral name for the authority confirmation document).

B2 — Cross-context boundary: `OrdersController` and `InvoicingController`
were injecting `InvoiceRecordRepositoryPort` directly, violating the
cross-context rule. Both now go through `IInvoiceService`
(`INVOICE_SERVICE_TOKEN`). Added `getLatestInvoiceForOrder` to the
service interface and implementation.

Additional improvements per review:
- `@Param('invoiceId', ParseUUIDPipe)` on download endpoints (validation).
- Binary size cap (20 MB) in `streamBinaryDocument`.
- `upoReference: string|null`→`confirmationDocumentAvailable: boolean`
  in `OrderInvoiceProjectionDto` (simpler, non-KSeF-specific shape).
- All spec files updated to match renamed fields.

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

* fix(invoicing): fix spec files broken by rebase — upo→confirmation + IssueInvoiceResult unwrap

- ksef-invoicing.adapter.spec.ts: `getRegulatoryDocument(record(), 'upo')`
  → `'confirmation'` (matches the renamed `RegulatoryDocumentKind` value)
- subiekt-invoicing.adapter.spec.ts: `adapter.issueInvoice()` now returns
  `IssueInvoiceResult` (not bare `InvoiceRecord`); unwrap `.record` at every
  call site; pass `.record` to `getClearanceStatus` in all getClearanceStatus tests

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

* fix(invoicing): add getLatestInvoiceForOrder to IInvoiceService + consolidate interface file (#1224)

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

* fix(invoicing): update UPO integration test filename expectation to match confirmation rename

dd66f73 renamed the 'upo' RegulatoryDocumentKind to 'confirmation' (ADR-026
vocabulary), which changed the download filename from ol-upo-*.xml to
ol-confirmation-*.xml. 302949a updated the other affected specs but missed
this int-spec assertion, breaking CI on PR #1231.

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

* fix(invoicing): address tech-lead review findings on KSeF UPO endpoint (#1224)

Fixes the remaining review findings on PR #1231:
- parseDocumentKind now accepts all three neutral RegulatoryDocumentKind
  values (source|rendered|confirmation) instead of a stale Exclude<...,
  'upo'> type that no longer matched any union member; kind=confirmation
  is now reachable from GET /invoices/:id/document too, not only via the
  dedicated /upo alias.
- downloadUpo now catches UnsupportedRegulatoryDocumentKindError and maps
  it to 409, matching downloadDocument's existing behavior.
- Aligned :invoiceId param validation across every invoicing route to a
  shared ParseUUIDPipe helper (404 on malformed UUID, consistently).
- Fixed stale 'upo' wording in RegulatoryDocumentReader JSDoc and its
  guard spec (the neutral kind is 'confirmation').
- Reworded a stale 'VAT breakdown' comment to 'tax breakdown' and added
  an explicit non-authoritative caveat to buildContent's recomputed
  net/tax/gross figures.
- Replaced the raw Error thrown by the sourceDocument write-once guard
  with a new SourceDocumentImmutableError domain exception.
- KsefHttpClient now reads binary responses via a streaming reader with
  a running byte-count cap instead of buffering the full body before
  checking size, closing the OOM vector left open when Content-Length
  is absent or understates the true body size.

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

* fix(invoicing): address PR #1231 final review nits on updateOutcome + downloadUpo

Two non-blocking items from the last review pass (#1231):
- `updateOutcome` now enforces the write-once `sourceDocument` invariant via a
  single guarded UPDATE (WHERE "sourceDocument" IS NULL) instead of a
  read-then-check-then-write, closing the theoretical concurrent-overwrite race.
- Add the symmetric unit test for `downloadUpo` mapping
  UnsupportedRegulatoryDocumentKindError to 409, mirroring the existing
  downloadDocument coverage.

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

* fix(integrations): fix InfaktInvoicingAdapter.issueInvoice against IssueInvoiceResult

CI on PR #1231 failed lint/test/type-check: the merge-into-main brought in
InvoicingPort.issueInvoice's new IssueInvoiceResult wrapper (added on this
branch for the UPO/document-retrieval flow), but Infakt's adapter — merged
from main via #1292 — still implemented the pre-wrapper signature
(Promise<InvoiceRecord>), which the base `tsc -b` build across libs/**
fails on for every downstream job. Infakt has no seller lookup or adapter-
built source document (it submits to KSeF natively), so the fix wraps the
existing InvoiceRecord in `{ record }` and leaves seller/sourceDocument
unset. Updated the happy-path test to unwrap accordingly.

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

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Signed-off-by: norbert-kulus-blockydevs <42zeroo@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
…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 added a commit that referenced this pull request Jul 1, 2026
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>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 1, 2026
…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 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
…estion (#1293)

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

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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>
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): production-harden Infakt plugin – validators, retry classifier, NestJS module, unit tests

2 participants