Skip to content

fix(listings): reject non-UUID path ids with 400 instead of 500 - #1313

Merged
piotrswierzy merged 2 commits into
mainfrom
1213-listings-uuid-validation-plan
Jul 2, 2026
Merged

fix(listings): reject non-UUID path ids with 400 instead of 500#1313
piotrswierzy merged 2 commits into
mainfrom
1213-listings-uuid-validation-plan

Conversation

@jakubretajczykBD

@jakubretajczykBD jakubretajczykBD commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Three ListingsController routes (GET /listings/offer-mappings/:id, GET /listings/marketplace-offer/:id, GET /listings/.../offer-creation-status/:connectionId/:offerCreationRecordId) forwarded a raw path param straight into a Postgres uuid-column lookup. A malformed id surfaced as an uncaught QueryFailedError and a 500 instead of a proper validation error.
  • Added ParseUUIDPipe at the controller boundary so a malformed id now returns 400 Bad Request.
  • As defense-in-depth, OfferMappingRepository.findById and OfferCreationRecordRepository.findById / updateStatus now catch driver error code 22P02 (invalid input syntax for uuid) and return null / throw OfferCreationRecordNotFoundException instead of leaking the infrastructure error — mirroring the existing ConnectionRepository pattern.

Related issues

Closes #1213

Test plan

  • New listings-invalid-path-id.int-spec.ts — integration tests asserting 400 on all three routes for an invalid UUID
  • New repository unit tests (offer-mapping.repository.spec.ts, offer-creation-record.repository.spec.ts) covering the 22P02 handling
  • pnpm lint && pnpm type-check && pnpm test

Quality gate

  • pnpm lint passes (zero errors)
  • pnpm type-check passes (zero errors)
  • pnpm test passes (all unit tests green)
  • Optional, Docker required: pnpm test:integration passes — needed
    only if you touched apps/api/test/integration/** or any plugin's
    infrastructure/adapters/.

By submitting this pull request, I confirm that my contributions
are made under the terms of the Apache License 2.0, and I
certify the Developer Certificate of Origin
by signing off my commits.

Three listings routes forwarded a raw path param straight into a
Postgres uuid-column lookup, so a malformed id surfaced as an
uncaught QueryFailedError -> 500. Add ParseUUIDPipe at the controller
boundary and guard OfferMappingRepository/OfferCreationRecordRepository
against driver code 22P02 as defense-in-depth, mirroring the existing
ConnectionRepository pattern.

Closes #1213

Signed-off-by: jakubret <jakub.retajczyk@blockydevs.com>

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tech Lead Review — Approve (polish only)

Clean, tightly-scoped fix for the 500-on-malformed-uuid class. Two layers, both correct:

  • Interface: ParseUUIDPipe on the three UUID path params — verified against the route decorators (@Get(':id'), @Get(':id/offer'), and offerCreationRecordId on the two-param creation-status route). Correctly leaves connectionId unguarded on the two-param route, since it's a JS string comparison, never a DB query.
  • Infrastructure (defense-in-depth): repository guards mirror the ConnectionRepository precedent verbatim — same error instanceof QueryFailedError && 'code' in error && error.code === '22P02' check, findByIdnull, updateStatusOfferCreationRecordNotFoundException (domain error thrown from the infra layer, no QueryFailedError escapes the port — matches engineering-standards § Error Handling).

Checks I ran:

  • Precedent exists and matches (connection.repository.ts:48-60).
  • Over-catch is bounded — both guarded where clauses filter one uuid column (id); entityType on the offer-mapping query is a hardcoded constant, not user input, so 22P02 can only come from id.
  • Scope is complete: the other :connectionId listings routes resolve through ConnectionRepository, which already carries the 22P02 guard, so they were never in the 500 class.
  • No any. Tests cover both the 22P02 → null/throw path and a re-throw-on-different-code regression guard, plus an int-spec exercising the real Nest pipe pipeline (400 on all three routes + 404 regressions).

Suggestions (non-blocking)

  1. Add @ApiResponse({ status: 400, ... }) to the three fixed routes — they can now return 400 but Swagger doesn't document it (the plan's §8 already flagged this as worth including).
  2. GET /listings/:id/offer int-spec asserts only the 400 case; getOfferMapping gets a matching 404 regression but getMarketplaceOffer doesn't. A one-line 404-on-valid-absent-UUID assertion would symmetrically lock the unchanged happy path.
  3. Nit (negligible): option-less ParseUUIDPipe rejects UUIDs outside v1-5 (e.g. the nil UUID) that Postgres would accept, shifting such nonsense input 404 → 400. Theoretical only — real ids are all v4.

Verdict: approve once out of draft and the quality gate (pnpm lint && pnpm type-check && pnpm test + the int-spec) is green in CI. Suggestions are optional polish.

@jakubretajczykBD
jakubretajczykBD marked this pull request as ready for review July 2, 2026 11:53

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review (head 10fc1c8) — Approve

Re-reviewed after the branch went non-draft. The head moved fed8cdaf → 10fc1c8c only via a merge of maingit diff fed8cdaf..HEAD over the listings files is empty, so the controller and both repositories are byte-identical to the version I approved. No regression from the merge.

Verified

  • ParseUUIDPipe placement — correct on getOfferMapping.id, getMarketplaceOffer.id, and getOfferCreationStatus.offerCreationRecordId. Leaving connectionId unguarded is right: it's only used for an in-memory equality check, never a DB lookup, so it can't raise 22P02. The int-spec documents this with an explicit 404-fall-through case.
  • Repository 22P02 guards — no over-catch. The narrow error instanceof QueryFailedError && error.code === '22P02' check, plus the paired "re-throw a QueryFailedError with a different code" tests in both specs, positively prove non-target driver errors still propagate. Null-vs-throw split (read → null, mutate → OfferCreationRecordNotFoundException) matches the ConnectionRepository precedent.
  • Tests — int-spec now also asserts the symmetric 404-on-valid-absent-UUID case for GET /listings/:id; unit specs cover both findById and updateStatus paths.

Non-blocking polish (carried over, optional)

  1. The three UUID-guarded GET routes still lack @ApiResponse({ status: 400 }) (the two existing 400s are on updateOfferFields/createOffer). Doc-only.
  2. GET /listings/:id/offer int-spec asserts only 400 — the symmetric 404 path is now covered by route 1.
  3. ParseUUIDPipe accepts the nil UUID; harmless given the 400-not-500 goal.

Verdict: Approve. Fix is correct and unchanged; coverage is solid. None of the above blocks merge.

@piotrswierzy
piotrswierzy merged commit f745d38 into main Jul 2, 2026
8 checks passed
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 2, 2026
…1328)

Semantic merge conflict between #1316 (URI versioning, global /v1 default)
and #1313 (new listings-invalid-path-id.int-spec.ts written pre-versioning):
the spec's unprefixed /listings/... requests stopped matching any route, so
every request returned the router-level 404 ("Cannot GET ...") - the three
expect(400) ParseUUIDPipe assertions failed and the expect(404) ones passed
vacuously. Main has been red on Integration Tests since the two merged.

Prefix the five request paths with /v1, matching every other listings
int-spec. Verified against the Testcontainers harness: 5/5 pass.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 3, 2026
* docs(plans): implementation plan for KSeF FA(3) Platnosc (#1311)

Adds the full implementation plan for emitting payment method, bank
account, payment term, and skonto in KSeF FA(3) invoices as a
per-connection config value (no live bank-accounts API on the KSeF
side, unlike inFakt's #1303/#1308). Includes the schema-audited XSD
child order for Platnosc (TerminPlatnosci -> FormaPlatnosci ->
RachunekBankowy -> Skonto) and the design mockup referenced by the
issue.

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

* feat(ksef): emit FA(3) Platnosc from connection payment config (#1311)

Adds a per-connection, manually-entered payment configuration (default
payment method, bank account, payment term, early-payment discount)
and emits it into the FA(3) Platnosc element whenever configured.
Unlike inFakt (#1303/#1308), KSeF has no live bank-accounts API, so
this is a plain config value the operator types in once, mapped
straight through the existing seller/defaultTaxRate resolution chain
(factory -> adapter -> mapper -> pure builder).

- KsefPaymentConfig / KsefBankAccountConfig / KsefFormaPlatnosciValues
  on KsefConnectionConfig (domain types)
- Fa3PaymentInput / Fa3BankAccount / Fa3FormaPlatnosciValues (FA(3)
  builder-internal types)
- platnoscNode() in fa3-xml.builder.ts, emitted as a sibling of
  FaWiersz in the XSD-mandated child order: TerminPlatnosci ->
  FormaPlatnosci -> RachunekBankowy -> Skonto (confirmed against the
  vendored FA(3) v1-0E XSD - not payment-method-first)
- KsefAdapterFactory.resolvePayment + shape-validator checks for
  formaPlatnosci / bankAccount.nrRb / paymentTermDays
- FE: ksef-payment-config.ts assembly module (mirrors
  ksef-seller-config.ts) wired into edit-connection.schema.ts, new
  fields in ksef-structured-section.tsx
- FA3_IMPLEMENTATION_NOTES.md updated with the Platnosc mapping table

Backend: 305/305 ksef unit tests pass, including full XSD structural
validation of the new Platnosc block for both configured and
unconfigured connections. Frontend: 1887/1887 web unit tests pass.
check:invariants clean.

Closes #1311

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

* fix(ksef): stop dropping payment sub-fields based on fill order (#1311)

Live Playwright smoke test against a real dev stack + KSeF sandbox
connection (Phase 6 of the implementation plan) surfaced a real bug:
applyKsefPaymentToConfig deleted the whole bankAccount/skonto
sub-object whenever a sibling field (nrRb, or the other of
conditions/amount) wasn't present in the SAME merge call. Since each
field syncs to configText independently per keystroke, this silently
discarded whatever the operator typed first - skonto could never
actually be saved, and a bankAccount sub-field typed before nrRb was
lost.

Fix: only drop a sub-object when it is completely empty (no keys at
all), never based on a missing sibling. The "nrRb required if
bankAccount is set" and "skonto needs both conditions+amount" rules
now live where they belong - the backend shape validator (save time,
new skonto check added) and the factory's resolvePayment (issuance
time) - so a violation surfaces as a clear error instead of silent
data loss.

Adds the e2e smoke-test script (apps/web/e2e/ksef-payment-config.mjs)
and its screenshots (docs/assets/ksef-1311-smoke/) used to find this
and verify the fix + full field set against the design mockup.

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

* docs(plans): mark #1311 acceptance criteria + Phase 6 complete

All acceptance criteria satisfied and Phase 6 (live smoke test +
verification artifact) done - link to the published artifact and
note the persistence bug found/fixed during the smoke test.

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

* fix(ksef): address tech-review suggestions on PR #1317

- Shape validator + FE Zod schema now enforce the FA(3) TNrRB length
  bound (10-34 chars) on payment.bankAccount.nrRb instead of only
  non-emptiness, so a truncated account number is rejected at
  connection-save time rather than surfacing as an opaque KSeF XSD
  error at issuance.
- resolvePayment now defensively drops an unknown formaPlatnosci code
  or a negative/non-integer paymentTermDays (mirroring the existing
  bankAccount/skonto guards) for connections whose config predates the
  ksef.publicapi.v2 shape validator.

Addresses both SUGGESTION findings from the tech-lead review on #1317.

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

* fix(ksef): correct RachunekBankowy child element order (NrRB, SWIFT, NazwaBanku)

Confirmed live against the KSeF sandbox: the FA(3) XSD mandates NrRB
before SWIFT before NazwaBanku inside RachunekBankowy. Emitting
NazwaBanku before SWIFT caused KSeF to reject the invoice (status 450,
"invalid child element SWIFT... expected OpisRachunku").

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

* fix(ksef): session IV reuse + FA(3) Podmiot2 required JST/GV fields

Two E2E-verified prerequisites for live KSeF acceptance, confirmed
against the KSeF test environment while doing live sandbox
verification of #1311 (the Platnosc feature was silently blocked
from ever actually being accepted end-to-end without these):

1. AES session IV: encryptDocument was generating a fresh random IV
   per document, but KSeF's SendInvoiceRequest wire shape has no
   per-document IV field — it decrypts every document in a session
   using the session IV declared once in
   OpenOnlineSessionRequest.encryption.initializationVector. A
   per-document IV caused a SHA-256 hash mismatch (status 430)
   because KSeF's decryption produced garbage. Fixed: encryptDocument
   now reuses context.symmetricKey.iv (the session IV) for every
   document.

2. JST and GV are REQUIRED by the FA(3) XSD on Podmiot2 (no
   minOccurs="0") but were never emitted, causing a semantic
   validation rejection. Fixed: buyerNode now always emits JST=2,
   GV=2 ("nie dotyczy" — not a JST subsidiary unit / not a VAT group
   member).

Cherry-picked from 634c27a5 (previously only on the unmerged branch
1228-ksef-fa3-full-visualization-new, which was never pushed to
origin) — without this fix no live KSeF invoice submission from this
codebase can be genuinely accepted, regardless of Platnosc content.

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

* fix(ksef): address PR #1317 review - Platnosc structural order checks + ctor options bag

- validateFa3Xml now enforces the XSD-mandated child order of Fa/Platnosc
  (TerminPlatnosci -> FormaPlatnosci -> RachunekBankowy -> Skonto) and of each
  RachunekBankowy (NrRB required, SWIFT before NazwaBanku), so an ordering
  regression like the pre-review RachunekBankowy bug is caught by the
  'passes the structural validator' tests instead of co-signed by them
- KsefInvoicingAdapter trailing optional params (payment, now) move into a
  KsefInvoicingAdapterOptions bag so future additions never shift positional
  call sites
- resolvePayment doc comment now names the intentional three-layer
  (FE assembly / shape validator / factory) defense-in-depth explicitly

The blocking SWIFT/NazwaBanku emit-order finding was already fixed in
c315e4d; this adds the validator coverage that would have caught it.

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

* test(web): align stale skonto persistence test with per-keystroke sync fix

The merge helper intentionally persists an incomplete skonto (conditions
without amount) so independent per-field config sync never drops the
first-typed field; completeness is enforced at save/issuance time (#1311
smoke-test finding). The test still asserted the old drop-on-incomplete
behaviour.

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

* fix(api): version listings invalid-path-id int-spec paths under /v1 (#1328)

Semantic merge conflict between #1316 (URI versioning, global /v1 default)
and #1313 (new listings-invalid-path-id.int-spec.ts written pre-versioning):
the spec's unprefixed /listings/... requests stopped matching any route, so
every request returned the router-level 404 ("Cannot GET ...") - the three
expect(400) ParseUUIDPipe assertions failed and the expect(404) ones passed
vacuously. Main has been red on Integration Tests since the two merged.

Prefix the five request paths with /v1, matching every other listings
int-spec. Verified against the Testcontainers harness: 5/5 pass.

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

* fix(ksef): address second tech-review round on PR #1317

- NrRB input is whitespace-stripped at FE assembly time (new normalizeNrRb,
  mirroring the normalizeNip precedent) so a conventionally-spaced NRB paste
  never reaches config.payment.bankAccount.nrRb or the FA(3) wire with inner
  spaces; the FE zod length check now counts the stripped value, converging
  with the BE shape validator's 10-34 bound
- shape validator rejects a wrong-typed payment / bankAccount / skonto with
  an explicit 'must be an object' issue instead of silently falling through
  to issuance-time drop
- the tripled '1'..'7' TFormaPlatnosci list now carries a cross-reference
  comment at all three declaration sites (FE schema, plugin connection-config
  types, FA3 schema types) so a future 8th code is added everywhere
- ksef-payment-config.mjs smoke script reads login credentials from
  WEB_USER / WEB_PASSWORD env vars (dev-stack defaults preserved), matching
  the WEB_BASE / KSEF_CONN_ID pattern

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

* fix(ksef): address third tech-review round on PR #1317

- shape validator now rejects an nrRb containing inner whitespace (the FE
  strips via normalizeNrRb, but a direct API write bypassed that and a
  spaced NRB would fail KSeF's TNrRB pattern at clearance); the factory's
  resolvePayment additionally strips whitespace defensively for
  pre-validator config rows, and drops a whitespace-only value instead of
  emitting an empty NrRB
- the hardcoded Podmiot2 JST/GV = 2 limitation is now recorded in
  FA3_IMPLEMENTATION_NOTES.md under Known limitations, with a pointer
  comment at the buyerNode declaration site
- new drift spec makes the tripled TFormaPlatnosci '1'..'7' list
  self-enforcing: the two in-package arrays are compared by import and the
  FE array is extracted from its source file, so a one-sided edit fails
  the suite
- follow-up issue #1330 filed for moving plugin-specific connection-config
  assembly behind a plugin slot

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

* fix(ksef): address fourth tech-review round on PR #1317

- IMPORTANT (skonto error locality): editConnectionSchema gains a
  both-or-neither superRefine on the skonto pair, anchoring the error on
  the missing field at submit time instead of surfacing the BE shape
  validator's form-level 400; per-keystroke persistence of a partial pair
  stays untouched. Field descriptions now say the pair is required
  together.
- SUGGESTION (drift-guard monorepo coupling): the cross-package FE half
  of the TFormaPlatnosci drift guard moved out of the plugin jest suite
  (7-level relative path into apps/web) into a repo-level invariant,
  scripts/check-ksef-forma-platnosci-drift.mjs, wired into
  check:invariants; the in-package spec keeps the import-level comparison.
- SUGGESTION (paymentTermDays unbounded): sanity cap at 999 days on both
  sides (FE zod refine + BE shape validator 0-999), with tests at and
  above the bound.
- SUGGESTION (misleading mockup filename): docs/plans/mockups/
  infakt-ksef-bank-account-payment-terms.html renamed to
  ksef-payment-platnosc.html; all references updated.
- SUGGESTION (PR scope / merge-commit visibility): handled in the PR body
  (dedicated line for the session-IV crypto fix so it survives into the
  squash-merge message).

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

* docs(ksef): cite spec sections confirming single-IV-per-session AES model

Review nit on PR #1317 (approve round): the session-IV reuse in
encryptDocument was justified only via the CIRFMF C# reference client.
The doc comments in ksef-session-crypto.service.ts and
ksef-crypto.types.ts now also cite the KSeF 2.0 OpenAPI
(OpenOnlineSessionRequest.encryption.initializationVector is the only IV
on the wire; SendInvoiceRequest has no per-document IV field) and the
official CIRFMF session guide (sesja-interaktywna.md: one 256-bit key +
128-bit IV generated at session open, used for the session's documents).

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

* refactor(web): move plugin-specific connection-config assembly behind a plugin slot (#1334)

* refactor(web): move plugin-specific connection-config assembly behind a plugin slot

Introduce PlatformContribution.connectionConfig (#1330) - the non-render
half of a platform's structured connection-config editing: a Zod schema
fragment composed into the edit-connection resolver at render time, a
read-side readConfigToForm hydrator, and a write-side applyToConfig
partial-patch assembler. Field names are declaration-merged into the new
PluginEditConnectionFields interface (mirrors the PluginApiNamespaces
precedent) so plugin sections stay statically typed.

Migrate KSeF as the first platform: every ksef-named module leaves the
shared features/connections feature (seller/payment assembly, NIP/NRB
normalizers, setup wizard schema+form+page, value sets) and lands in the
plugins/ksef slice; the KSeF Zod fields, superRefine checks, merge
clauses, and hydration readers move verbatim into
plugins/ksef/ksef-connection-config.ts, preserving the #1311
per-keystroke partial-patch semantics (relocated tests pin them).
Other platforms' inline fields migrate as follow-ups now that the seam
exists.

Closes #1330

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

* refactor(web): tighten the connection-config plugin-slot contract per tech review

Address the tech-review round on PR #1334 (1 IMPORTANT + 2 SUGGESTIONS):

- IMPORTANT: constrain ConnectionConfigContribution.schemaShape to a
  keyof-PluginEditConnectionFields mapped type
  (PluginConnectionConfigSchemaShape) so a schema key with no matching
  declaration-merged form field is a compile error. The synthetic
  acmeToken test fixture now declaration-merges its field (same seam
  real plugins use), which also drops the patch-type casts it needed.
- SUGGESTION: extend assertUniquePluginInvariants with invariant 4 -
  cross-plugin connectionConfig.schemaShape field-name collisions throw
  at module load (TS silently accepts same-type declaration merges), with
  a registry test pinning the failure path.
- SUGGESTION: dev-mode console.warn in syncStructuredToJson when a synced
  field has neither a host merge clause nor a connectionConfig
  contribution (the silent-drop misconfiguration #1330 exists to
  prevent); StructuredField is now derived from a runtime as-const array.
  Also drop the redundant defensive spread before applyToConfig.

Doc row for connectionConfig updated with the compiler constraint, the
boot-time collision guard, and the platform-prefix naming convention.

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

* refactor(web): annotate ksefSchemaShape + widen the schemaShape dev-warn per second-pass review

Second-pass tech-review SUGGESTION: the excess-property check from the
PluginConnectionConfigSchemaShape mapped type only fires on a fresh or
annotated literal, so the separate un-annotated ksefSchemaShape const
silently accepted unmerged keys. Annotating it with
ConnectionConfigContribution['schemaShape'] restores the check at the
declaration (verified: a bogus key now fails with TS2353).

Also closes the related note: syncStructuredToJson's dev warn now fires
not only when a platform ships no connectionConfig contribution, but
also when the contribution exists and the synced field is absent from
its schemaShape - the other runtime-detectable silent-drop shape.

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

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(ksef): address fifth tech-review round on PR #1317

- Record the AES-256-CBC session-IV determinism trade-off under Known
  limitations in FA3_IMPLEMENTATION_NOTES.md, next to the JST/GV entry,
  so security reviewers find it where they look first.
- Harden the TFormaPlatnosci drift script's extraction: tolerate an
  optional type annotation, Prettier multi-line wraps, and inline
  comments inside the array body.
- Promote the untyped-bag string readers (readConfigString /
  readOptionalConfigString) to shared/plugins; ksef-connection-config
  and EditConnectionForm now share one definition instead of per-file
  copies.

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

* fix(ci): reclaim disk from orphaned Testcontainers volumes/images/build-cache

Ryuk is disabled on the persistent self-hosted runner, so the manual
orphan-sweep step is the only cleanup for Testcontainers resources.
docker rm -f (no -v) left anonymous volumes behind forever, and the
sweep never touched dangling images or build cache — both accumulate
indefinitely and were the actual cause of "no space left on device"
integration-test failures unrelated to any test's own code.

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

* fix(ci): widen disk cleanup after first attempt reclaimed 0B (#1321)

The prior sweep (docker rm -fv + filtered volume/image/builder prune)
still failed with the exact same "no space left on device" error, and
logged 0B reclaimed across all three prune calls. Two concrete bugs:

- The volume-prune label filter never matches: anonymous volumes don't
  inherit the labels of the container that created them, so
  `--filter "label=org.testcontainers=true"` matches nothing. Dropped
  the filter — an unfiltered `docker volume prune` is still safe,
  since Docker refuses to remove a volume still attached to any
  container regardless of label.
- Plain `docker image prune -f` only removes dangling (untagged)
  images; 0B reclaimed means this runner has none, so the real disk
  pressure is old-but-tagged image versions across many past
  integration-test pulls. Widened to `-af` (all unused images) — still
  safe under concurrency since Docker never removes an image a live
  container references, and a concurrent job needing a pruned image
  just re-pulls it rather than failing.

Also logs `docker system df -v` before/after cleanup so the next
failure (if any) can be diagnosed from CI output alone — there is no
shell access to this runner.

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

* revert(ci): back out disk-cleanup experiment (#1321)

docker system df on the failing run revealed this self-hosted runner
is shared with unrelated third-party workloads (linux-runner-1..4,
hedera-payment-db, hawser-dockhand-agent — none of it OpenLinker's).
The runner's disk pressure is caused by that other tenancy's live
containers, not by anything OpenLinker's CI leaves behind, so a wider
prune (image -af, unfiltered volume prune, builder prune) only ever
reclaims OpenLinker's own garbage and can't fix a permanently
oversubscribed shared host — while carrying real risk of touching
image/volume state a co-tenant relies on. Reverting to the original,
narrowly-scoped cleanup: remove only OpenLinker's own orphaned,
Testcontainers-labeled containers whose owning CI run has finished.
Disk capacity itself needs to be resolved with whoever operates this
runner, not from this repo's workflow.

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

* diag(test): log docker disk usage around each PS+MySQL container boot (#1321)

A full apps/api integration run boots 4 suite-scoped PrestaShop+MySQL
Testcontainer pairs sequentially (maxWorkers: 1), and Ryuk is disabled
on the self-hosted CI runner — so a suite that times out before
reaching its own cleanup() leaks disk for the rest of that run with no
way to tell, from CI output alone, which suite did it (a prior
"no space left on device" mid-run failure had exactly this shape).

Logs `docker system df` before each boot and after each cleanup() in
the shared prestashop-container.helper.ts, so the next such failure
can be attributed to a specific suite without runner shell access.

Considered also converting the two PS suites that don't install the OL
module (prestashop-harness-smoke, prestashop-webhook-provisioning) to
mock the port instead of booting a real container, to cut boot count
from 4 to 2. Both are deliberately real-PS regression guards — the
smoke test exists to prove the Testcontainer harness itself works
end-to-end, and the webhook-provisioning spec guards #541, a bug that
was green at the unit-test/mock layer and only caught against the real
WS API. Mocking either would remove the exact coverage they exist for,
so this is diagnostics-only; reducing real PS boot count needs a
different approach (e.g. sharing one boot between the two suites that
already require installOlModule: true).

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

* diag(ci): probe host /tmp for leaked ol-ps-data-* PrestaShop bind-mount dirs (#1321)

The PS test harness bind-mounts a mkdtemp dir onto /var/www/html and PS
writes ~1GB into it as root; the unprivileged rmSync cleanup leaks it on
the persistent self-hosted host, invisible to docker system df and the
container sweep. Temporary read-only probe to confirm before fixing the
cleanup path.

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

* fix(test): stop leaking root-owned PS bind-mount dirs on the CI host (#1321)

PS writes ~1GB into the ol-ps-data-* bind-mount as root, and on the
runner-in-docker host the source path resolves on the HOST daemon's
/tmp - the unprivileged rmSync cleanup could never delete it, so every
PS suite leaked ~1GB onto the host root fs until containerd hit
"no space left on device".

Two-part fix:
- removePsDataDir empties the dir through a root alpine container
  (works in both host topologies), then rmSync's the local dir.
- The CI orphan-sweep step reaps host /tmp/ol-ps-data-* dirs older
  than 90 min (same age gate as the container sweep), covering
  SIGKILLed/cancelled runs that never reach in-process cleanup.

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

* revert(ci): drop temporary disk diagnostics + host-tmp sweep steps (#1321)

Restores ci.yml to main's version. With the host root fs at 100% every
docker-cli invocation in these steps hangs or fails, blocking the job
before tests even start. The in-process removePsDataDir fix in the PS
container helper stays - it adds no CI step.

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

* Revert "diag(test): log docker disk usage around each PS+MySQL container boot (#1321)"

This reverts commit eea68ac. Diagnostics no longer needed - the root
cause (leaked root-owned ol-ps-data-* bind-mount dirs on the host) is
identified and fixed by removePsDataDir.

Co-Authored-By: Claude Fable 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 Fable 5 <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 3, 2026
 review)

Address the latest #1310 review:
- Add a `connectionIdPipe()` (ParseUUIDPipe, 400 on malformed UUID) to the
  two new bank-account endpoints so a non-UUID path id returns 400 instead
  of surfacing as a 500 from the DB uuid cast (#1313 bug class).
- `encodeURIComponent(accountId)` in `setDefaultBankAccount` so an
  unexpected id can't inject slashes/`..` into the provider URL.

The prior review's edit-path eager-persist and surfaced-sync-failure items
were already addressed in an earlier commit on this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
piotrswierzy pushed a commit that referenced this pull request Jul 3, 2026
…follow-up) (#1310)

* feat(infakt): bank-account picker with live inFakt default sync (#1303 follow-up)

Adds a BankAccountsReader/BankAccountDefaultSetter sub-capability pair to
InvoicingPort so a provider that requires a named bank account on
'transfer' invoices (inFakt) can surface a live picker instead of the
operator guessing which account to configure.

Backend:
- New GET /connections/:id/bank-accounts and POST
  /connections/:id/bank-accounts/:accountId/default routes, both 501 when
  the adapter doesn't implement the capability.
- InfaktInvoicingAdapter.listBankAccounts() maps inFakt's bank_accounts.json
  (including inFakt's own `default` flag) to the neutral shape; a new
  IInfaktHttpClient.put() backs setDefaultBankAccount(), which PUTs
  { bank_account: { default: true } } to keep inFakt's own "default
  account" setting in sync whenever the operator picks a non-default one.
- issueInvoice/issueCorrection stamp bank_account/bank_name on 'transfer'
  invoices only when a bankAccount snapshot is configured.

Frontend:
- Wizard: a post-create step (locked until the connection exists) fetches
  live accounts and auto-applies whichever inFakt itself marks default;
  zero accounts forces the payment method back to Cash.
- Edit screen: the same live picker inside the existing InlineDisclosure,
  requiring an explicit operator pick.
- Both surfaces call the new default-setter mutation when the operator
  picks a non-default account, and show a "(default in inFakt)" marker.

Verified end-to-end against the real inFakt sandbox API with 2 live bank
accounts: switching accounts in OpenLinker's UI genuinely flips inFakt's
own default flag.

Closes #1303 (follow-up)

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

* fix(infakt): address #1310 review - eager edit-path persist, surfaced sync failures, string account id, aligned picker surfaces

Review follow-ups on the bank-account picker:

- IMPORTANT: the edit screen now persists config.bankAccount eagerly on
  pick (from the server-side config snapshot) and flips inFakt's own
  default only after that persist succeeds - abandoning the edit form or
  a failed Save can no longer leave inFakt flipped while OL stamps the
  old account. The wizard pick path is gated the same way.
- IMPORTANT: sync mutations are no longer silent - the default-setter
  hook surfaces failures via an error toast, and every eager
  updateConnection call site toasts on rejection. New tests cover the
  rejecting-mutation paths on both surfaces.
- BankAccount id is a string end-to-end now (config snapshot, Zod
  schema, form state); legacy numeric ids are coerced on read.
- The wizard's picker is gated on Transfer like the edit screen, and the
  duplicated "Payment method" label block is gone.
- setDefaultBankAccount documents that inFakt clears the previous
  default server-side (single PUT, no second call).
- Seeded the send_to_ksef fixture in the #1303 spec groups that were
  missing it after the rebase onto main's inline-KSeF-submit flow.

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

* chore(api): fix import-type lint warning in demo-mode.service.spec

Cleanup surfaced by eslint --fix while validating the rebase.

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

* fix(infakt): validate connectionId as UUID + URL-encode accountId (#1310 review)

Address the latest #1310 review:
- Add a `connectionIdPipe()` (ParseUUIDPipe, 400 on malformed UUID) to the
  two new bank-account endpoints so a non-UUID path id returns 400 instead
  of surfacing as a 500 from the DB uuid cast (#1313 bug class).
- `encodeURIComponent(accountId)` in `setDefaultBankAccount` so an
  unexpected id can't inject slashes/`..` into the provider URL.

The prior review's edit-path eager-persist and surfaced-sync-failure items
were already addressed in an earlier commit on this branch.

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

* fix(infakt): address #1310 pr-review - wizard payment-method lock, docs sync, 502 mapping

- Lock the wizard's payment-method select once the connection is created
  (post-create picks were never persisted, so both drift directions were
  reachable); the zero-accounts fallback now also syncs the form control
  to Cash and keeps its explanation visible outside the Transfer gate.
- Sync docs/capabilities.md + architecture-overview.md §14 with the
  InvoicingPort sub-capabilities: add BankAccountsReader and
  BankAccountDefaultSetter rows, backfill RegulatoryDocumentReader
  (#1224), bump the inventory count 31 -> 34.
- Map live provider failures on the two bank-account proxy endpoints to
  502 (AdapterNotFoundException + live-call errors; provider text logged,
  never echoed), matching the issuance path's mapping.
- Document listBankAccounts' accepted first-page-only v1 scope in the
  adapter JSDoc.
- Surface a hint on the edit screen when the saved bank-account snapshot
  no longer exists in the live inFakt list.
- Make BankAccountDefaultSetter extend BankAccountsReader (genuine is-a,
  mirroring RegulatoryTransmitter) with a both-methods runtime guard.

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

* fix(infakt): address #1310 review round 2 - misleading copy, pick race, config validation, coverage

Addresses all 12 findings from the latest #1310 review:

IMPORTANT
- Wizard fetch-error copy no longer promises a Cash fallback it never
  persists; it now states invoices stay on Transfer and may be rejected
  until an account is picked (finding 1).
- Edit-screen zero-accounts copy no longer claims "invoices will use
  Cash" (that surface never auto-persists); it states the saved method is
  still Transfer and points to the fix (finding 2).
- Zero-accounts forced-Cash downgrade now reverts the optimistic UI flip
  and shows a dedicated toast when its persist fails, instead of leaving a
  locked Cash UI contradicting the server (finding 3).
- Both picker Selects disable while a persist/flip is in flight, closing
  the double-pick race that could diverge OL vs inFakt defaults (finding 4).
- The inFakt config-shape validator now validates config.bankAccount
  (object with id string|number + non-empty accountNumber/bankName), so a
  malformed snapshot 400s at save time instead of a 422 at issuance
  (finding 5).
- The edit-path seam is now tested: readInfaktBankAccount hydration
  (incl. legacy numeric-id coercion + shape rejection) and the
  mergeStructuredIntoConfig infaktBankAccount clause (finding 6).

SUGGESTION
- The wizard auto-apply fallback (no account flagged default) now also
  flips inFakt's default instead of only persisting OL's snapshot
  (finding 7).
- Documented the accepted unsaved-form-state pick behavior in the edit
  section header (finding 8).
- Extracted the duplicated persist-then-flip choreography into a shared
  usePickBankAccount hook used by both surfaces (finding 9).
- Added a placeholder option to the wizard Select (finding 10).
- Added the missing edit-screen fetch-error test (finding 11).
- Added an accountId non-empty param pipe on the default-setter endpoint
  (finding 12).

Co-Authored-By: Claude Fable 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 Fable 5 <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 22, 2026
* docs(plans): implementation plan for KSeF FA(3) Platnosc (#1311)

Adds the full implementation plan for emitting payment method, bank
account, payment term, and skonto in KSeF FA(3) invoices as a
per-connection config value (no live bank-accounts API on the KSeF
side, unlike inFakt's #1303/#1308). Includes the schema-audited XSD
child order for Platnosc (TerminPlatnosci -> FormaPlatnosci ->
RachunekBankowy -> Skonto) and the design mockup referenced by the
issue.

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

* feat(ksef): emit FA(3) Platnosc from connection payment config (#1311)

Adds a per-connection, manually-entered payment configuration (default
payment method, bank account, payment term, early-payment discount)
and emits it into the FA(3) Platnosc element whenever configured.
Unlike inFakt (#1303/#1308), KSeF has no live bank-accounts API, so
this is a plain config value the operator types in once, mapped
straight through the existing seller/defaultTaxRate resolution chain
(factory -> adapter -> mapper -> pure builder).

- KsefPaymentConfig / KsefBankAccountConfig / KsefFormaPlatnosciValues
  on KsefConnectionConfig (domain types)
- Fa3PaymentInput / Fa3BankAccount / Fa3FormaPlatnosciValues (FA(3)
  builder-internal types)
- platnoscNode() in fa3-xml.builder.ts, emitted as a sibling of
  FaWiersz in the XSD-mandated child order: TerminPlatnosci ->
  FormaPlatnosci -> RachunekBankowy -> Skonto (confirmed against the
  vendored FA(3) v1-0E XSD - not payment-method-first)
- KsefAdapterFactory.resolvePayment + shape-validator checks for
  formaPlatnosci / bankAccount.nrRb / paymentTermDays
- FE: ksef-payment-config.ts assembly module (mirrors
  ksef-seller-config.ts) wired into edit-connection.schema.ts, new
  fields in ksef-structured-section.tsx
- FA3_IMPLEMENTATION_NOTES.md updated with the Platnosc mapping table

Backend: 305/305 ksef unit tests pass, including full XSD structural
validation of the new Platnosc block for both configured and
unconfigured connections. Frontend: 1887/1887 web unit tests pass.
check:invariants clean.

Closes #1311

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

* fix(ksef): stop dropping payment sub-fields based on fill order (#1311)

Live Playwright smoke test against a real dev stack + KSeF sandbox
connection (Phase 6 of the implementation plan) surfaced a real bug:
applyKsefPaymentToConfig deleted the whole bankAccount/skonto
sub-object whenever a sibling field (nrRb, or the other of
conditions/amount) wasn't present in the SAME merge call. Since each
field syncs to configText independently per keystroke, this silently
discarded whatever the operator typed first - skonto could never
actually be saved, and a bankAccount sub-field typed before nrRb was
lost.

Fix: only drop a sub-object when it is completely empty (no keys at
all), never based on a missing sibling. The "nrRb required if
bankAccount is set" and "skonto needs both conditions+amount" rules
now live where they belong - the backend shape validator (save time,
new skonto check added) and the factory's resolvePayment (issuance
time) - so a violation surfaces as a clear error instead of silent
data loss.

Adds the e2e smoke-test script (apps/web/e2e/ksef-payment-config.mjs)
and its screenshots (docs/assets/ksef-1311-smoke/) used to find this
and verify the fix + full field set against the design mockup.

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

* docs(plans): mark #1311 acceptance criteria + Phase 6 complete

All acceptance criteria satisfied and Phase 6 (live smoke test +
verification artifact) done - link to the published artifact and
note the persistence bug found/fixed during the smoke test.

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

* fix(ksef): address tech-review suggestions on PR #1317

- Shape validator + FE Zod schema now enforce the FA(3) TNrRB length
  bound (10-34 chars) on payment.bankAccount.nrRb instead of only
  non-emptiness, so a truncated account number is rejected at
  connection-save time rather than surfacing as an opaque KSeF XSD
  error at issuance.
- resolvePayment now defensively drops an unknown formaPlatnosci code
  or a negative/non-integer paymentTermDays (mirroring the existing
  bankAccount/skonto guards) for connections whose config predates the
  ksef.publicapi.v2 shape validator.

Addresses both SUGGESTION findings from the tech-lead review on #1317.

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

* fix(ksef): correct RachunekBankowy child element order (NrRB, SWIFT, NazwaBanku)

Confirmed live against the KSeF sandbox: the FA(3) XSD mandates NrRB
before SWIFT before NazwaBanku inside RachunekBankowy. Emitting
NazwaBanku before SWIFT caused KSeF to reject the invoice (status 450,
"invalid child element SWIFT... expected OpisRachunku").

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

* fix(ksef): session IV reuse + FA(3) Podmiot2 required JST/GV fields

Two E2E-verified prerequisites for live KSeF acceptance, confirmed
against the KSeF test environment while doing live sandbox
verification of #1311 (the Platnosc feature was silently blocked
from ever actually being accepted end-to-end without these):

1. AES session IV: encryptDocument was generating a fresh random IV
   per document, but KSeF's SendInvoiceRequest wire shape has no
   per-document IV field — it decrypts every document in a session
   using the session IV declared once in
   OpenOnlineSessionRequest.encryption.initializationVector. A
   per-document IV caused a SHA-256 hash mismatch (status 430)
   because KSeF's decryption produced garbage. Fixed: encryptDocument
   now reuses context.symmetricKey.iv (the session IV) for every
   document.

2. JST and GV are REQUIRED by the FA(3) XSD on Podmiot2 (no
   minOccurs="0") but were never emitted, causing a semantic
   validation rejection. Fixed: buyerNode now always emits JST=2,
   GV=2 ("nie dotyczy" — not a JST subsidiary unit / not a VAT group
   member).

Cherry-picked from 634c27a5 (previously only on the unmerged branch
1228-ksef-fa3-full-visualization-new, which was never pushed to
origin) — without this fix no live KSeF invoice submission from this
codebase can be genuinely accepted, regardless of Platnosc content.

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

* fix(ksef): address PR #1317 review - Platnosc structural order checks + ctor options bag

- validateFa3Xml now enforces the XSD-mandated child order of Fa/Platnosc
  (TerminPlatnosci -> FormaPlatnosci -> RachunekBankowy -> Skonto) and of each
  RachunekBankowy (NrRB required, SWIFT before NazwaBanku), so an ordering
  regression like the pre-review RachunekBankowy bug is caught by the
  'passes the structural validator' tests instead of co-signed by them
- KsefInvoicingAdapter trailing optional params (payment, now) move into a
  KsefInvoicingAdapterOptions bag so future additions never shift positional
  call sites
- resolvePayment doc comment now names the intentional three-layer
  (FE assembly / shape validator / factory) defense-in-depth explicitly

The blocking SWIFT/NazwaBanku emit-order finding was already fixed in
c315e4d; this adds the validator coverage that would have caught it.

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

* test(web): align stale skonto persistence test with per-keystroke sync fix

The merge helper intentionally persists an incomplete skonto (conditions
without amount) so independent per-field config sync never drops the
first-typed field; completeness is enforced at save/issuance time (#1311
smoke-test finding). The test still asserted the old drop-on-incomplete
behaviour.

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

* fix(api): version listings invalid-path-id int-spec paths under /v1 (#1328)

Semantic merge conflict between #1316 (URI versioning, global /v1 default)
and #1313 (new listings-invalid-path-id.int-spec.ts written pre-versioning):
the spec's unprefixed /listings/... requests stopped matching any route, so
every request returned the router-level 404 ("Cannot GET ...") - the three
expect(400) ParseUUIDPipe assertions failed and the expect(404) ones passed
vacuously. Main has been red on Integration Tests since the two merged.

Prefix the five request paths with /v1, matching every other listings
int-spec. Verified against the Testcontainers harness: 5/5 pass.

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

* fix(ksef): address second tech-review round on PR #1317

- NrRB input is whitespace-stripped at FE assembly time (new normalizeNrRb,
  mirroring the normalizeNip precedent) so a conventionally-spaced NRB paste
  never reaches config.payment.bankAccount.nrRb or the FA(3) wire with inner
  spaces; the FE zod length check now counts the stripped value, converging
  with the BE shape validator's 10-34 bound
- shape validator rejects a wrong-typed payment / bankAccount / skonto with
  an explicit 'must be an object' issue instead of silently falling through
  to issuance-time drop
- the tripled '1'..'7' TFormaPlatnosci list now carries a cross-reference
  comment at all three declaration sites (FE schema, plugin connection-config
  types, FA3 schema types) so a future 8th code is added everywhere
- ksef-payment-config.mjs smoke script reads login credentials from
  WEB_USER / WEB_PASSWORD env vars (dev-stack defaults preserved), matching
  the WEB_BASE / KSEF_CONN_ID pattern

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

* fix(ksef): address third tech-review round on PR #1317

- shape validator now rejects an nrRb containing inner whitespace (the FE
  strips via normalizeNrRb, but a direct API write bypassed that and a
  spaced NRB would fail KSeF's TNrRB pattern at clearance); the factory's
  resolvePayment additionally strips whitespace defensively for
  pre-validator config rows, and drops a whitespace-only value instead of
  emitting an empty NrRB
- the hardcoded Podmiot2 JST/GV = 2 limitation is now recorded in
  FA3_IMPLEMENTATION_NOTES.md under Known limitations, with a pointer
  comment at the buyerNode declaration site
- new drift spec makes the tripled TFormaPlatnosci '1'..'7' list
  self-enforcing: the two in-package arrays are compared by import and the
  FE array is extracted from its source file, so a one-sided edit fails
  the suite
- follow-up issue #1330 filed for moving plugin-specific connection-config
  assembly behind a plugin slot

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

* fix(ksef): address fourth tech-review round on PR #1317

- IMPORTANT (skonto error locality): editConnectionSchema gains a
  both-or-neither superRefine on the skonto pair, anchoring the error on
  the missing field at submit time instead of surfacing the BE shape
  validator's form-level 400; per-keystroke persistence of a partial pair
  stays untouched. Field descriptions now say the pair is required
  together.
- SUGGESTION (drift-guard monorepo coupling): the cross-package FE half
  of the TFormaPlatnosci drift guard moved out of the plugin jest suite
  (7-level relative path into apps/web) into a repo-level invariant,
  scripts/check-ksef-forma-platnosci-drift.mjs, wired into
  check:invariants; the in-package spec keeps the import-level comparison.
- SUGGESTION (paymentTermDays unbounded): sanity cap at 999 days on both
  sides (FE zod refine + BE shape validator 0-999), with tests at and
  above the bound.
- SUGGESTION (misleading mockup filename): docs/plans/mockups/
  infakt-ksef-bank-account-payment-terms.html renamed to
  ksef-payment-platnosc.html; all references updated.
- SUGGESTION (PR scope / merge-commit visibility): handled in the PR body
  (dedicated line for the session-IV crypto fix so it survives into the
  squash-merge message).

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

* docs(ksef): cite spec sections confirming single-IV-per-session AES model

Review nit on PR #1317 (approve round): the session-IV reuse in
encryptDocument was justified only via the CIRFMF C# reference client.
The doc comments in ksef-session-crypto.service.ts and
ksef-crypto.types.ts now also cite the KSeF 2.0 OpenAPI
(OpenOnlineSessionRequest.encryption.initializationVector is the only IV
on the wire; SendInvoiceRequest has no per-document IV field) and the
official CIRFMF session guide (sesja-interaktywna.md: one 256-bit key +
128-bit IV generated at session open, used for the session's documents).

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

* refactor(web): move plugin-specific connection-config assembly behind a plugin slot (#1334)

* refactor(web): move plugin-specific connection-config assembly behind a plugin slot

Introduce PlatformContribution.connectionConfig (#1330) - the non-render
half of a platform's structured connection-config editing: a Zod schema
fragment composed into the edit-connection resolver at render time, a
read-side readConfigToForm hydrator, and a write-side applyToConfig
partial-patch assembler. Field names are declaration-merged into the new
PluginEditConnectionFields interface (mirrors the PluginApiNamespaces
precedent) so plugin sections stay statically typed.

Migrate KSeF as the first platform: every ksef-named module leaves the
shared features/connections feature (seller/payment assembly, NIP/NRB
normalizers, setup wizard schema+form+page, value sets) and lands in the
plugins/ksef slice; the KSeF Zod fields, superRefine checks, merge
clauses, and hydration readers move verbatim into
plugins/ksef/ksef-connection-config.ts, preserving the #1311
per-keystroke partial-patch semantics (relocated tests pin them).
Other platforms' inline fields migrate as follow-ups now that the seam
exists.

Closes #1330

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

* refactor(web): tighten the connection-config plugin-slot contract per tech review

Address the tech-review round on PR #1334 (1 IMPORTANT + 2 SUGGESTIONS):

- IMPORTANT: constrain ConnectionConfigContribution.schemaShape to a
  keyof-PluginEditConnectionFields mapped type
  (PluginConnectionConfigSchemaShape) so a schema key with no matching
  declaration-merged form field is a compile error. The synthetic
  acmeToken test fixture now declaration-merges its field (same seam
  real plugins use), which also drops the patch-type casts it needed.
- SUGGESTION: extend assertUniquePluginInvariants with invariant 4 -
  cross-plugin connectionConfig.schemaShape field-name collisions throw
  at module load (TS silently accepts same-type declaration merges), with
  a registry test pinning the failure path.
- SUGGESTION: dev-mode console.warn in syncStructuredToJson when a synced
  field has neither a host merge clause nor a connectionConfig
  contribution (the silent-drop misconfiguration #1330 exists to
  prevent); StructuredField is now derived from a runtime as-const array.
  Also drop the redundant defensive spread before applyToConfig.

Doc row for connectionConfig updated with the compiler constraint, the
boot-time collision guard, and the platform-prefix naming convention.

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

* refactor(web): annotate ksefSchemaShape + widen the schemaShape dev-warn per second-pass review

Second-pass tech-review SUGGESTION: the excess-property check from the
PluginConnectionConfigSchemaShape mapped type only fires on a fresh or
annotated literal, so the separate un-annotated ksefSchemaShape const
silently accepted unmerged keys. Annotating it with
ConnectionConfigContribution['schemaShape'] restores the check at the
declaration (verified: a bogus key now fails with TS2353).

Also closes the related note: syncStructuredToJson's dev warn now fires
not only when a platform ships no connectionConfig contribution, but
also when the contribution exists and the synced field is absent from
its schemaShape - the other runtime-detectable silent-drop shape.

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

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(ksef): address fifth tech-review round on PR #1317

- Record the AES-256-CBC session-IV determinism trade-off under Known
  limitations in FA3_IMPLEMENTATION_NOTES.md, next to the JST/GV entry,
  so security reviewers find it where they look first.
- Harden the TFormaPlatnosci drift script's extraction: tolerate an
  optional type annotation, Prettier multi-line wraps, and inline
  comments inside the array body.
- Promote the untyped-bag string readers (readConfigString /
  readOptionalConfigString) to shared/plugins; ksef-connection-config
  and EditConnectionForm now share one definition instead of per-file
  copies.

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

* fix(ci): reclaim disk from orphaned Testcontainers volumes/images/build-cache

Ryuk is disabled on the persistent self-hosted runner, so the manual
orphan-sweep step is the only cleanup for Testcontainers resources.
docker rm -f (no -v) left anonymous volumes behind forever, and the
sweep never touched dangling images or build cache — both accumulate
indefinitely and were the actual cause of "no space left on device"
integration-test failures unrelated to any test's own code.

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

* fix(ci): widen disk cleanup after first attempt reclaimed 0B (#1321)

The prior sweep (docker rm -fv + filtered volume/image/builder prune)
still failed with the exact same "no space left on device" error, and
logged 0B reclaimed across all three prune calls. Two concrete bugs:

- The volume-prune label filter never matches: anonymous volumes don't
  inherit the labels of the container that created them, so
  `--filter "label=org.testcontainers=true"` matches nothing. Dropped
  the filter — an unfiltered `docker volume prune` is still safe,
  since Docker refuses to remove a volume still attached to any
  container regardless of label.
- Plain `docker image prune -f` only removes dangling (untagged)
  images; 0B reclaimed means this runner has none, so the real disk
  pressure is old-but-tagged image versions across many past
  integration-test pulls. Widened to `-af` (all unused images) — still
  safe under concurrency since Docker never removes an image a live
  container references, and a concurrent job needing a pruned image
  just re-pulls it rather than failing.

Also logs `docker system df -v` before/after cleanup so the next
failure (if any) can be diagnosed from CI output alone — there is no
shell access to this runner.

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

* revert(ci): back out disk-cleanup experiment (#1321)

docker system df on the failing run revealed this self-hosted runner
is shared with unrelated third-party workloads (linux-runner-1..4,
hedera-payment-db, hawser-dockhand-agent — none of it OpenLinker's).
The runner's disk pressure is caused by that other tenancy's live
containers, not by anything OpenLinker's CI leaves behind, so a wider
prune (image -af, unfiltered volume prune, builder prune) only ever
reclaims OpenLinker's own garbage and can't fix a permanently
oversubscribed shared host — while carrying real risk of touching
image/volume state a co-tenant relies on. Reverting to the original,
narrowly-scoped cleanup: remove only OpenLinker's own orphaned,
Testcontainers-labeled containers whose owning CI run has finished.
Disk capacity itself needs to be resolved with whoever operates this
runner, not from this repo's workflow.

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

* diag(test): log docker disk usage around each PS+MySQL container boot (#1321)

A full apps/api integration run boots 4 suite-scoped PrestaShop+MySQL
Testcontainer pairs sequentially (maxWorkers: 1), and Ryuk is disabled
on the self-hosted CI runner — so a suite that times out before
reaching its own cleanup() leaks disk for the rest of that run with no
way to tell, from CI output alone, which suite did it (a prior
"no space left on device" mid-run failure had exactly this shape).

Logs `docker system df` before each boot and after each cleanup() in
the shared prestashop-container.helper.ts, so the next such failure
can be attributed to a specific suite without runner shell access.

Considered also converting the two PS suites that don't install the OL
module (prestashop-harness-smoke, prestashop-webhook-provisioning) to
mock the port instead of booting a real container, to cut boot count
from 4 to 2. Both are deliberately real-PS regression guards — the
smoke test exists to prove the Testcontainer harness itself works
end-to-end, and the webhook-provisioning spec guards #541, a bug that
was green at the unit-test/mock layer and only caught against the real
WS API. Mocking either would remove the exact coverage they exist for,
so this is diagnostics-only; reducing real PS boot count needs a
different approach (e.g. sharing one boot between the two suites that
already require installOlModule: true).

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

* diag(ci): probe host /tmp for leaked ol-ps-data-* PrestaShop bind-mount dirs (#1321)

The PS test harness bind-mounts a mkdtemp dir onto /var/www/html and PS
writes ~1GB into it as root; the unprivileged rmSync cleanup leaks it on
the persistent self-hosted host, invisible to docker system df and the
container sweep. Temporary read-only probe to confirm before fixing the
cleanup path.

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

* fix(test): stop leaking root-owned PS bind-mount dirs on the CI host (#1321)

PS writes ~1GB into the ol-ps-data-* bind-mount as root, and on the
runner-in-docker host the source path resolves on the HOST daemon's
/tmp - the unprivileged rmSync cleanup could never delete it, so every
PS suite leaked ~1GB onto the host root fs until containerd hit
"no space left on device".

Two-part fix:
- removePsDataDir empties the dir through a root alpine container
  (works in both host topologies), then rmSync's the local dir.
- The CI orphan-sweep step reaps host /tmp/ol-ps-data-* dirs older
  than 90 min (same age gate as the container sweep), covering
  SIGKILLed/cancelled runs that never reach in-process cleanup.

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

* revert(ci): drop temporary disk diagnostics + host-tmp sweep steps (#1321)

Restores ci.yml to main's version. With the host root fs at 100% every
docker-cli invocation in these steps hangs or fails, blocking the job
before tests even start. The in-process removePsDataDir fix in the PS
container helper stays - it adds no CI step.

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

* Revert "diag(test): log docker disk usage around each PS+MySQL container boot (#1321)"

This reverts commit eea68ac. Diagnostics no longer needed - the root
cause (leaked root-owned ol-ps-data-* bind-mount dirs on the host) is
identified and fixed by removePsDataDir.

Co-Authored-By: Claude Fable 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 Fable 5 <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 22, 2026
…follow-up) (#1310)

* feat(infakt): bank-account picker with live inFakt default sync (#1303 follow-up)

Adds a BankAccountsReader/BankAccountDefaultSetter sub-capability pair to
InvoicingPort so a provider that requires a named bank account on
'transfer' invoices (inFakt) can surface a live picker instead of the
operator guessing which account to configure.

Backend:
- New GET /connections/:id/bank-accounts and POST
  /connections/:id/bank-accounts/:accountId/default routes, both 501 when
  the adapter doesn't implement the capability.
- InfaktInvoicingAdapter.listBankAccounts() maps inFakt's bank_accounts.json
  (including inFakt's own `default` flag) to the neutral shape; a new
  IInfaktHttpClient.put() backs setDefaultBankAccount(), which PUTs
  { bank_account: { default: true } } to keep inFakt's own "default
  account" setting in sync whenever the operator picks a non-default one.
- issueInvoice/issueCorrection stamp bank_account/bank_name on 'transfer'
  invoices only when a bankAccount snapshot is configured.

Frontend:
- Wizard: a post-create step (locked until the connection exists) fetches
  live accounts and auto-applies whichever inFakt itself marks default;
  zero accounts forces the payment method back to Cash.
- Edit screen: the same live picker inside the existing InlineDisclosure,
  requiring an explicit operator pick.
- Both surfaces call the new default-setter mutation when the operator
  picks a non-default account, and show a "(default in inFakt)" marker.

Verified end-to-end against the real inFakt sandbox API with 2 live bank
accounts: switching accounts in OpenLinker's UI genuinely flips inFakt's
own default flag.

Closes #1303 (follow-up)

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

* fix(infakt): address #1310 review - eager edit-path persist, surfaced sync failures, string account id, aligned picker surfaces

Review follow-ups on the bank-account picker:

- IMPORTANT: the edit screen now persists config.bankAccount eagerly on
  pick (from the server-side config snapshot) and flips inFakt's own
  default only after that persist succeeds - abandoning the edit form or
  a failed Save can no longer leave inFakt flipped while OL stamps the
  old account. The wizard pick path is gated the same way.
- IMPORTANT: sync mutations are no longer silent - the default-setter
  hook surfaces failures via an error toast, and every eager
  updateConnection call site toasts on rejection. New tests cover the
  rejecting-mutation paths on both surfaces.
- BankAccount id is a string end-to-end now (config snapshot, Zod
  schema, form state); legacy numeric ids are coerced on read.
- The wizard's picker is gated on Transfer like the edit screen, and the
  duplicated "Payment method" label block is gone.
- setDefaultBankAccount documents that inFakt clears the previous
  default server-side (single PUT, no second call).
- Seeded the send_to_ksef fixture in the #1303 spec groups that were
  missing it after the rebase onto main's inline-KSeF-submit flow.

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

* chore(api): fix import-type lint warning in demo-mode.service.spec

Cleanup surfaced by eslint --fix while validating the rebase.

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

* fix(infakt): validate connectionId as UUID + URL-encode accountId (#1310 review)

Address the latest #1310 review:
- Add a `connectionIdPipe()` (ParseUUIDPipe, 400 on malformed UUID) to the
  two new bank-account endpoints so a non-UUID path id returns 400 instead
  of surfacing as a 500 from the DB uuid cast (#1313 bug class).
- `encodeURIComponent(accountId)` in `setDefaultBankAccount` so an
  unexpected id can't inject slashes/`..` into the provider URL.

The prior review's edit-path eager-persist and surfaced-sync-failure items
were already addressed in an earlier commit on this branch.

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

* fix(infakt): address #1310 pr-review - wizard payment-method lock, docs sync, 502 mapping

- Lock the wizard's payment-method select once the connection is created
  (post-create picks were never persisted, so both drift directions were
  reachable); the zero-accounts fallback now also syncs the form control
  to Cash and keeps its explanation visible outside the Transfer gate.
- Sync docs/capabilities.md + architecture-overview.md §14 with the
  InvoicingPort sub-capabilities: add BankAccountsReader and
  BankAccountDefaultSetter rows, backfill RegulatoryDocumentReader
  (#1224), bump the inventory count 31 -> 34.
- Map live provider failures on the two bank-account proxy endpoints to
  502 (AdapterNotFoundException + live-call errors; provider text logged,
  never echoed), matching the issuance path's mapping.
- Document listBankAccounts' accepted first-page-only v1 scope in the
  adapter JSDoc.
- Surface a hint on the edit screen when the saved bank-account snapshot
  no longer exists in the live inFakt list.
- Make BankAccountDefaultSetter extend BankAccountsReader (genuine is-a,
  mirroring RegulatoryTransmitter) with a both-methods runtime guard.

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

* fix(infakt): address #1310 review round 2 - misleading copy, pick race, config validation, coverage

Addresses all 12 findings from the latest #1310 review:

IMPORTANT
- Wizard fetch-error copy no longer promises a Cash fallback it never
  persists; it now states invoices stay on Transfer and may be rejected
  until an account is picked (finding 1).
- Edit-screen zero-accounts copy no longer claims "invoices will use
  Cash" (that surface never auto-persists); it states the saved method is
  still Transfer and points to the fix (finding 2).
- Zero-accounts forced-Cash downgrade now reverts the optimistic UI flip
  and shows a dedicated toast when its persist fails, instead of leaving a
  locked Cash UI contradicting the server (finding 3).
- Both picker Selects disable while a persist/flip is in flight, closing
  the double-pick race that could diverge OL vs inFakt defaults (finding 4).
- The inFakt config-shape validator now validates config.bankAccount
  (object with id string|number + non-empty accountNumber/bankName), so a
  malformed snapshot 400s at save time instead of a 422 at issuance
  (finding 5).
- The edit-path seam is now tested: readInfaktBankAccount hydration
  (incl. legacy numeric-id coercion + shape rejection) and the
  mergeStructuredIntoConfig infaktBankAccount clause (finding 6).

SUGGESTION
- The wizard auto-apply fallback (no account flagged default) now also
  flips inFakt's default instead of only persisting OL's snapshot
  (finding 7).
- Documented the accepted unsaved-form-state pick behavior in the edit
  section header (finding 8).
- Extracted the duplicated persist-then-flip choreography into a shared
  usePickBankAccount hook used by both surfaces (finding 9).
- Added a placeholder option to the wizard Select (finding 10).
- Added the missing edit-screen fetch-error test (finding 11).
- Added an accountId non-empty param pipe on the default-setter endpoint
  (finding 12).

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

Listings controller: non-UUID path ids return 500 instead of 400/404 (unguarded uuid-cast)

2 participants