feat: financial + full KSeF feature set integration - #29
Conversation
Add @open-mercato/financial-pl — Polish KSeF 2.0 e-invoicing official module: FA(3) submission, status polling, UPO retrieval; per-org encrypted credentials (ksef_pl provider); per-invoice idempotency with KSeF 440-duplicate recovery; a periodic reconciliation sweep so no invoice silently fails to reach KSeF; UMES widget injection on the sales-invoice host; integration tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single source-of-truth design doc for @open-mercato/financial-pl: KSeF 2.0 send flow, FA(3), crypto, data models, 3-layer idempotency + reconciliation sweep, token auth + certificate roadmap, per-org multi-tenant config, API contracts, activation, and a manual-testing checklist. Consolidated from the prior enterprise spec + round plans, verified against the module code, MIT-framed (no enterprise env-flags / overlay framing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The esbuild post-step rewrote `from "./x"` and dynamic `import("./x")` to explicit
`/index.js`, but not bare side-effect `import "./x"` (e.g. the module index's
`import './commands'`). Under strict node ESM (the queue worker / any non-bundler
consumer) a directory import is unresolvable, so the submit subscriber and the
reconciliation worker failed to load. Rewrite side-effect imports too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The country-agnostic sales-invoice CrudForm host renders the injected field label without the financial_pl i18n bundle, so a bare key showed raw as 'financial_pl.fields.plVatMeta'. Use a human-readable default label; labelKey still lets a host that can resolve it localize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…model jury reconciled) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndalone on released core) Module-owned full invoice UI over core SalesInvoice: list/create/edit/detail pages, KSeF panel (send/retry/UPO/PDF/offline/correction), full PL-VAT meta editor, JPK + certificate backoffice. List + single-invoice read APIs; fail-closed KSeF-immutability interceptors on sales.invoices PUT/DELETE + invoice-meta. Removes dead sales.invoices injection wiring (no host on released core); keeps the response enricher. i18n x4 (synced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…I, interceptor lock, ACL, JPK/cert gating) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w round 1) - invoice-meta PUT: server-side KSeF-immutability lock (custom route didn't run the interceptor) - interceptors: catch DELETE /api/sales/invoices?id= (resolve id from URL, was fail-open) - certificates page: fix inverted hasAllFeatures(granted,required); standardize import - invoice detail page: useParams() (Next 15 params-as-Promise) - CorrectionForm: read creditMemoId; positive quantities; send currencyCode+issueDate - PlVatMetaForm: add orderSnapshot editor (full invoiceMetaPutSchema) - edit form: map flat API meta -> InvoiceMeta; reactive currency - tests: TC-UI-002 (meta+DELETE 409), TC-UI-004 (real correction payload); +8 i18n keys x4 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- detail/edit pages: read invoice id from the sync `props.params.id` (catch-all routing provides it as a prop; useParams() returned the slug array -> empty id -> 308 -> 'Failed to load'). Verified live in the sandbox preview. - JPK generate UI: add required kodUrzedu (+ optional contextNip) so generate/export work. - KsefActions: gate Issue-offline on financial_pl.manage (matches the route). - invoice-meta PUT: composed gate [financial_pl.manage, sales.invoices.manage]. - CorrectionForm payload: default currencyCode to PLN. +12 i18n keys x4 (synced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… record Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ofill (SPEC-014) Buyer (Nabywca) capture -> core SalesInvoice.metadata.buyerSnapshot (exact buildBuyer keys), MF Wykaz NIP company-lookup proxy (fail-open), inline validations, VAT-rate/unit pickers, searchable GTU/procedure/OSS selects. No entity/migration/core change. Four-model jury (Claude + Codex + Kimi + DeepSeek) all pass on the final diff; 6 reproducible blockers reconciled. Live KSeF TEST + cert auth re-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… design + spec-stage 4-model jury Inbound receiving, JPK_V7->MF e-submission, full offline+KOD II PDF, token->cert cutover, NBP FX, batch session, PDF pagination. Spec-stage jury (Claude+Codex+Kimi+ DeepSeek) reconciled before coding: JPK package/crypto format, separate qualified JPK signer (3 voters), materialize idempotency (3 voters), offline-mode terminology, PUT transport, F4 auto opt-in (preserves SPEC-007). All 7 confirmed live-verifiable on test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rt + offline/auth types + NBP/cert-health libs + PDF pagination Codex-implemented (optimized profile): ReceivedInvoice + ReceiveCursor entities, JpkVatFiling submission columns, KsefSubmission batchReference, niedostepnosc offline mode + auto auth opt-in (preserves SPEC-007 guard), RSA PKCS#1v1.5 JPK key-wrap, signJpkInitUpload (2-ref XAdES), putToAbsoluteUrl, nbp-fx, credential-health, invoice PDF pagination. Gate: build + tsc 0 + 53 tests + generate + i18n all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ceived-invoice mappers + JPK submission client Codex-implemented: queryReceivedInvoices/downloadInvoiceByKsefNumber + batch session methods (ksef-client); received-invoice→PurchaseVatRecord mappers; MF JPK gateway client (InitUploadSigned→PUT→FinishUpload→Status/UPO) + metadata builder. Orchestrator seam fix: null-guard in parseUploadRequest. Gate: build + tsc 0 + 32 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…package builder Codex-implemented: zod schemas (received-invoices list/sync, jpk-submit, nbp-rate, batch); batch-package builder (no-dep STORE-mode ZIP + AES-256-CBC + RSA-OAEP wrap + manifest hashes). Gate: build + tsc 0 + 43 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…->MF submission Codex: receiveInvoicesCommand (Subject2 paging + isTruncated windowing + idempotent no-clobber upsert + cursor) + materializePurchaseRecordCommand (idempotent/transactional); 3 received-invoices routes; submitFilingCommand (CAS generated->submitting + early-persist + UPO) + jpk/submit route; separate JPK-signer credential. Gate: build + tsc 0 + 445 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm wire Codex: invoice-pdf route renders KOD II (offline + Offline cert) via buildKodIIUrl; nbp-rate fail-open route + PlVatMetaForm 'Fetch NBP rate' (stale-request-guarded) filling exchangeRate/exchangeRateDate. Gate: build + tsc 0 + 13 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (wsadowa) send Codex: niedostepnosc wired through issueOfflineCommand + schema (unavailabilityEndsAt, next-business-day-after deadline); total-awaria = issuedOutsideKsef/BFK (documented); sendBatchCommand (per-invoice FA(3) + self-billed guard + buildBatchPackage + open/upload/ close batch session + batchReference rows) + batch route. Gate: build + tsc 0 + 54 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ubmit + cert-health UI, integration tests, i18n Codex: received-invoices backoffice page (list/sync/view-xml/add-to-ledger); JPK 'Submit to MF' action + credential-health badge; TC-KSEF-RECV-001/JPK-002/BATCH-001 integration tests; i18n keys (en/pl/de/es parity). Full gate: build + generate + i18n sync + tsc 0 + 451 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…v-gated) Self-addressed invoice (seller==buyer==context NIP) → query Subject2 → download FA(3). LIVE-VERIFIED on the KSeF TEST env this session: send accepted, Subject2 query found it, FA(3) XML downloaded (1349 B). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rly-persist/signer, PDF threshold, batch race 4-model jury (Opus+Codex+Kimi+DeepSeek) reconciled: (1) CRITICAL added the missing Migration20260630000000 (2 tables + 5 cols + partial-unique idx) + snapshot — migrate-from-zero verified on a fresh DB; (2) JPK real STORE-mode ZIP (not raw deflate), early-persist submissionReference + resume, 60s status poll, separate JPK-signer credential in integration.ts, GET jpk/submit/status; (3) PDF byte-stable <=45 lines; (4) batch claims local rows before the external session (no dup fiscal submission) + reconcile worker resolves batch UPO. Gate: build + tsc 0 + 454 tests; migrate-from-zero PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…heck regression Opus round-2 blocker: the new batch-reconcile passed a field-projected Loaded<KsefSubmission>[] to groupBatchRows(KsefSubmission[]) (TS2345). Runtime-correct (reads projected fields, writes via nativeUpdate) — widened the static type with a documented cast. our-source typecheck now 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ materialize lock + auto cert validity 4-model round-2 jury (Opus+DeepSeek pass; Kimi+Codex edges, all confirmed): (1) JPK non-timeout failure WITH a reference stays resumable (re-poll), not reset-to-generated (no duplicate InitUpload); (2) batch ambiguous post-open failure leaves rows processing+batchReference (resumable), not rejected (no duplicate fiscal batch); (3) auto auth checks cert validity window, else token (preserves SPEC-007); (4) MF JPK cert via config resolveJpkMfPublicCert; (5) materializePurchaseRecord fetches XML BEFORE the locked transaction (no external IO under PESSIMISTIC_WRITE); credentials auto test uses a real cert. Gate: build + generate + i18n + tsc 0 + 458 tests; migrate-from-zero (unchanged) PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…el jury record Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t block, sale date, smart date defaults, line-edit safety Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nstall compliance - README: scannable Features tables (KSeF connectivity, auth, FA(3) content, offline/QR, JPK, PDF/backoffice, reliability); canonical `mercato module add` install steps replacing the non-standard activation commands. - Add Omdyo maintainer attribution + logo (assets/omdyo-logo.svg). - Module metadata: ejectable: true + default export (matches official-module convention); package.json: prepack build hook + core peer range >=0.6.6 <0.7.0 to match the real runtime floor. - Refresh changeset to cover the full compliance surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The code-jury analysis note was an internal development artifact and does not belong in the shipped spec set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zielivia
left a comment
There was a problem hiding this comment.
Review — @open-mercato/financial-pl (KSeF)
Really strong contribution — the architecture is clean (clear lib/commands/subscribers split, XAdES/RSA-OAEP conformed to v2.6.1, idempotency via partial-unique indexes, encrypted XML/UPO at rest, i18n complete across en/pl/de/es, ACL consistent). I set the whole module up locally, drove the full author → send → Accepted + KSeF number + UPO/PDF flow end to end, and exercised JPK, received invoices and credentials. Requesting changes for the blockers and a set of confirmed correctness/compliance issues below — none are architectural, they're fixable in place.
Each item was verified against the code; I've noted the few that are lower-confidence.
🔴 Blockers — a fresh clone of this branch will not build
-
The module isn't a dependency of the sandbox.
apps/sandbox/src/modules.tsenables{ id: 'financial_pl', from: '@open-mercato/financial-pl' }, butapps/sandbox/package.jsonhas zero references to the package, soyarn installnever links it andyarn generatefails with "Package not installed". Please add"@open-mercato/financial-pl": "workspace:*"to the sandbox dependencies. -
Platform version skew.
packages/financial-pl/package.jsondeclares peer@open-mercato/core|queue|shared|ui: ">=0.6.6 <0.7.0"and devDeps pinned to0.6.6-develop.6366, while the committedapps/sandbox/package.jsonpins the whole app to0.6.3-develop.3778. The changeset itself says "Requires @open-mercato/core ≥ 0.6.6", contradicting the branch baseline — yarn ends up resolving two incompatible copies of core/shared. Either bump the sandbox baseline to ≥0.6.6 in this PR (platform:sync), or align the module's pins to the branch baseline the waycarrier-inpostdoes.
🔴 Correctness / compliance (all confirmed by tracing the code)
-
Offline-issued invoices can be re-sent as ONLINE, dropping the offline marking.
subscribers/ksef-repoll.ts:138resets a stuckprocessingrow toqueuedand emits the onlinefinancial_pl.ksef_submission.queuedevent with nosubmission.modecheck;subscribers/ksef-submit.ts:79then callssubmitInvoiceToKsefwithoutofflineMode:true(andworkers/ksef-reconcile.worker.tsdoesn't exclude offline-mode rows from the processing sweep).retryCommandroutes offline modes tooffline_send_requestedprecisely to preserve the flag — repoll needs the same guard. Consequence: an offline24/awaryjny invoice loses the offline issue date + KOD I/II justification. -
A submitted JPK filing's period can be rewritten.
commands/jpk.tsupsertFilingCommand's update branch (~:326)Object.assignsyear/month/variant/celZlozenia/contextNipwith no status guard, whilegenerateCommand(~:428) correctly blocks regenerating asubmittedfiling. So the header can be changed to a different period whilegenerated_xml/upo_xml/submissionReferencestill describe the filed one — a falsified compliance record (and it re-opens the active-unique slot for a double filing). Editing a submitted filing should 409. -
Received-invoice sync silently skips a page on a transient error.
commands/ksef-receive.ts:370— on a page fetch throw the catch doespageOffset += PAGE_SIZE(advances past the unfetched page instead of retrying it);consecutiveErrorsresets on any later success, so a single transient error is never surfaced, andupdateCursorstill advances the HWM — so up toPAGE_SIZEinbound invoices are permanently dropped and never reach the JPK purchase register. -
Sending an already
offline_issuedinvoice returns a raw 500.commands/ksef-submission.ts:239activeStatuses = ['queued','processing','accepted']omitsoffline_issued, but the partial unique index (data/entities.ts:75) includes it. The pre-check misses the row, the insert hits a unique violation, and the recoveryfindOnereuses the same offline-excluding filter → returns null →throw errbubbles a DB error as an unhandled 500.issueOfflineandsendBatchboth use the full 4-status set — only this path is missingoffline_issued.
🟠 Security / UI
-
invoice-pdfroute is under-gated.api/ksef/invoice-pdf/route.ts:28requires onlyfinancial_pl.view, while the siblinginvoiceslist/detail routes deliberately require bothfinancial_pl.viewandsales.invoices.manage(with a comment noting that gating onfinancial_pl.viewalone is a permission bypass). A view-only user blocked from the invoice JSON can still fetch the full rendered invoice PDF. -
The net/gross ("netto/brutto") toggle is dead in the correction (KOR) form.
components/CorrectionForm.tsx:174renders<InvoiceLinesField>withoutpriceMode/onPriceModeChange, so clicking the toggle is a silent no-op (priceModedefaults tonet) — yetonChange(convertLinesPriceMode(...,'gross'))still runs, round-tripping unit prices through net→gross→net rounding with no visual feedback. The same toggle works on the create/edit form; only the correction form is unwired. (This is the toggle bug reported during testing.) -
Hydration mismatch on the invoices month label.
backend/financial/invoices/page.tsx:165formatMonthLabelcallstoLocaleDateString(undefined, …)— the SSR (serveren) renders "July 2026" while the client (pl) hydrates "lipiec 2026", so React logs a hydration error and flashes on load. Pass'pl-PL'explicitly (the module is PL-specific) or gate the label behind a mounted flag.formatDate/formatCurrencyshare the same runtime-locale pattern.
🟡 Worth a look (lower confidence)
- FA(3) line quantity may collapse to
1.lib/fa3-mapping.ts:458asString(row.quantity) ?? '1'returns'1'when the query engine hydratesquantityas a JS number (value math uses the real quantity viatoScaled4) →P_8B=1butP_11=qty×price.quantityis a pgnumeric(string by default), so this depends on the query-engine type parser — worth a quick check with a real FA(3) where qty ≠ 1. - Immutability lock is scoped to the selected org only.
api/interceptors.ts:59counts locking submissions withorganizationId = context.organizationId, so for a multi-org user acting on an invoice in a non-selected org the accepted submission may not be found and the edit/delete lock won't fire (reachability depends on core's write org-scoping). lib/fa3-mapping.tstoIsoDateusestoISOString().slice(0,10)(UTC) — a local-midnightDateunderTZ=Europe/Warsawshifts P_1 / KOD I date one day back.
🧹 Cleanup / altitude (non-blocking, good follow-ups)
- Route boilerplate (auth + org-scope + guard + catch) is hand-rolled across ~26
api/ksef/**files; seven carry a literal "mirror submissions/route.ts" comment — the leak-critical org-scope contract lives in prose, not a mechanism. AwithFinancialPlRoute()wrapper (ormakeCrudRoute) would make the safe behaviour the default. - No shared KSeF session — the creds → public-keys → authenticate handshake (incl. duplicated
selectCertificate+ poll constants) is copy-pasted across ~6 call sites; a reconcile sweep re-authenticates per batch group. A DIksefSessionServicecaching the access token per (org, env, NIP) would cut both latency and duplication. - List endpoints do avoidable work:
api/ksef/submissions/route.tsselects + decryptsinvoice_xml/upo_xmlfor every row only to drop them intoRow;api/ksef/invoices/route.tsruns a second 1000-row scan per request just to sum the summary card (and per keystroke on search). Project plaintext-only fields / use an aggregate. - Duplication:
formatDate/formatCurrencycopied across 5 pages;applyOssMarkersforked in the credit-memo resolver (OSS-rate drift risk on corrections); KSeF status label maps duplicated between the badge and the list filter. - Conventions: the enroll-certificate and add-purchase-record dialogs don't support
Cmd/Ctrl+Entersubmit (the received-sync dialog does — mirror it); theDataTablehosts omit a stableextensionTableId, which blocks widget injection.
🧪 Tests
The env-gated TC-KSEF-* integration suite currently imports its helpers from @open-mercato/core/modules/core/__integration__/helpers/*, a path the published @open-mercato/core doesn't ship (the exported surface is @open-mercato/core/helpers/integration/*) — so the suite can't load against an npm-installed core in this repo. Also, POST /api/sales/invoices returns 500 in the sandbox at the current pin, which blocks the invoice-authoring tests; and core 0.6.6 now requires a per-line currencyCode (old payloads 400).
I've added seven CI-safe UX flow specs alongside the existing ones (accepted/rejected state rendering, send-to-KSeF entry + confirmation, JPK stage→generate→list, received list + sync, credential-health), including two test.fail() regression guards that stay green until #4 (submitted-filing period lock) and #8 (correction toggle) are fixed.
Happy to pair on any of these — the module is close, these are the things I'd want resolved before merge.
…, KSeF/JPK compliance, security, UI Fixes all findings from zielivia's review: - sandbox: add @open-mercato/financial-pl workspace dep; sync platform to 0.6.6-develop.6366 (peers match, commandLoaderEntries wired in bootstrap) - KSeF recovery (repoll subscriber + reconcile worker) preserves offline modes: offline24/awaryjny/niedostepnosc rows re-drive via offline_issued + offline_send_requested (new isOfflineSubmissionMode, also used by retryCommand) - JPK: a submitted (or mid-submission 'submitting') filing can no longer be edited via upsertFilingCommand, nor rewritten/soft-deleted via its undo - received-invoice sync: failed pages retry at the same offset; persistent page or row-level failures abort loud (502) without advancing the cursor HWM - send command: activeStatuses includes offline_issued (matches the partial unique index) — idempotent return instead of a unique-violation 500 - invoice-pdf route requires financial_pl.view + sales.invoices.manage (with a regression test pinning parity with the invoices routes) - CorrectionForm wires the netto/brutto toggle (priceMode state -> lines field) - invoices page formatters pinned to pl-PL + UTC (hydration mismatch) - FA(3) mapping: asNumericString keeps numerically-hydrated quantity, fx_rate, discounts and OSS line rates; toIsoDate formats from local date components (no UTC day shift under Europe/Warsaw) - immutability interceptor locks tenant-wide (selected-org independent) - integration specs import the published @open-mercato/core/helpers/integration/* Suite: 556 passed / 14 env-gated skipped; typecheck + package build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Invoice numbering series — needs a model-level decision@haxiorz — leaving this one to you, because it is an architectural call rather than a UI detail. What is missing. The invoice form should let the operator choose a numbering series (a separate series for domestic sales and exports, say, or one per branch). Today that is not possible. Why I did not just add it. Core keeps one counter per (organization, tenant, document kind) — The workaround I rejected. Series could live in Two options worth weighing:
The first looks cleaner to me, but this is your area. What already exists here: |
Next batch of UI work — branch to pull in@haxiorz — I do not have write access to your fork, so I cannot add this to the PR directly. I pushed a branch to
Conflicts I resolved — please double-check these
Platform versions ( Verification
|
Summary
Adds
@open-mercato/financial-pl— a complete Polish KSeF 2.0 e-invoicing & VAT-compliance module for Open Mercato. It issues and sends FA(3) invoices to the Krajowy System e-Faktur directly via the Ministry of Finance REST API (no middleware), receives inbound invoices, generates and files JPK_V7, renders compliant PDFs, and covers offline issuance, corrections, VAT margin, and NBP FX — all layered onto the OSSsalesinvoices through UMES extension seams (widget injection, API interceptors, entity extensions), without patching core.Requires
@open-mercato/core≥ 0.6.6. Developed & maintained by Omdyo.Changes
ksef_plprovider).ejectable/prepack/exports compliance; core peer range>=0.6.6 <0.7.0; changeset.Specification
Does a spec exist for this feature/module?
Spec file path:
.ai/specs/SPEC-005-2026-07-01-financial-pl-ksef-connector-submission.md.ai/specs/SPEC-006-2026-07-01-financial-pl-fa3-documents-corrections-jpk.md.ai/specs/SPEC-007-2026-07-01-financial-pl-invoice-pdf.md.ai/specs/SPEC-008-2026-07-01-financial-pl-invoice-authoring-ui.md.ai/specs/SPEC-009-2026-07-02-financial-pl-midmarket-completeness.mdTesting
yarn workspace @open-mercato/financial-pl typecheck→ clean (exit 0).yarn workspace @open-mercato/financial-pl test→ 530 passed, 14 skipped (env-gated live tests), 59/60 suites.yarn workspace @open-mercato/financial-pl build→ 138 files built successfully.localhost:3005) and drove the invoice backoffice: invoice list with month summary, invoices showing live KSeF statuses (Accepted with a KSeF number, and Rejected), nav wired under Financials (PL).ksef-live): full auth → send → status → UPO againstapi-test.ksef.mf.gov.pl(FA(3)1-0EXSD, OpenAPI v2.6.1), plus integration suitesTC-KSEF-*.Checklist
develop.apps/docs/cla.md)..ai/specs/with a changelog entry (if applicable).Linked issues
N/A — new module contribution.