feat(integrations): production-harden Infakt plugin - validators, retry classifier, NestJS module, tests#1292
Conversation
Tech review: PR #1292 (feat(integrations): production-harden Infakt plugin)SummarySolid, well-tested hardening pass that follows the KSeF/Subiekt plugin precedent closely (validators, retry classifier, Issues[IMPORTANT] -
[IMPORTANT] -
[IMPORTANT] -
[SUGGESTION] -
[SUGGESTION] -
[SUGGESTION] -
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. |
- 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>
|
Addressed all IMPORTANT and SUGGESTION items from the tech review (80e98c1): IMPORTANT
SUGGESTION
All 7 suites / 80 tests still pass; |
piotrswierzy
left a comment
There was a problem hiding this comment.
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 IssueCorrectionCommand — invoicing.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) whileissueInvoiceusescurrency ?? 'PLN'— inconsistent; read currency from the command or add a comment. src/scripts/poc-sandbox-test.tssits undersrc/**, so it compiles intodistand ships to consumers, and itspoc:sandboxnpm script depends onts-node/tsconfig-pathsnot in the package deps. No hardcoded secrets (env-driven). Consider moving it out ofsrc/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). InfaktWebhookTranslatoris exported from the barrel but doesn't implement the canonicalWebhookEventTranslatorport and is unwired (deferred to #1281) — effectively dead onmainuntil then; confirm #1281 reshapes it to theCanonicalInboundEventcontract.- Factory throws a bare
Erroron missingcredentialsRefrather 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.
- 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>
80e98c1 to
a3b6823
Compare
|
Addressed all findings from the latest review (commit a3b6823): BLOCKING
Suggestions
Branch is already even with |
- 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
left a comment
There was a problem hiding this comment.
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
IssueCorrectionCommandcarries nocurrencyfield (unlikeIssueInvoiceCommand), so'PLN'+ explanatory comment is the correct resolution. poc-sandbox-test.tsundersrc/— ✅ moved toscripts/, outsidetsconfigrootDir/include; won't ship todist.- HTTP client logging raw error body (PII) — ✅ now logs only
method path → status; full body still reaches the caller viaInfaktApiError. - Factory bare
Error— ✅ now throwsInfaktConfigException(message, connectionId). InfaktWebhookTranslatorunwired — ⏸ 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.
…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>
de23a27 to
3d374fe
Compare
|
Addressed the non-blocking follow-up from the latest re-review (commit Fixed
Fix:
Also done
Ready for merge. |
- 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 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>
…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>
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>
…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>
#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>
…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>
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>
…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>
… 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>
…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>
…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>
… 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>
…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>
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).
InfaktConnectionConfigShapeValidatorAdapter/InfaktConnectionCredentialsShapeValidatorAdapter(ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort) so creating a connection with a malformedbaseUrlor missingapiKeyfails fast with a 400 instead of surfacing as a runtime error on first invoice issuanceInfaktRetryClassifierAdapterfor fiscal-safety-correct retry semantics:4xx(except429) →rejected(terminal, safe to reissue),429→ transient (rate-limited),5xx/network →in-doubt(the document may already exist server-side - must not blind-retry)InfaktIntegrationModuleviacreateNestAdapterModule(createInfaktPlugin())(no plugin-specific Nest providers needed) and exports it from the package barrel so a host app can register the plugininfakt-plugin.tsregister()to register all three side-registrations above (previously a no-op)mainclass-transformer/class-validatordeps fromlibs/integrations/infakt/package.jsonissueCorrection(fiscal-correctness bug), moved POC sandbox script out ofsrc/, stopped logging raw HTTP error bodies (PII), domainInfaktConfigExceptioninstead of a bareErrorin the factoryCloses #1280
Test plan
pnpm --filter @openlinker/integrations-infakt test- 7 suites, 81 tests passingpnpm lintpnpm type-checkerrors[]