Skip to content

feat: financial + full KSeF feature set integration - #29

Open
haxiorz wants to merge 37 commits into
open-mercato:developfrom
haxiorz:feat/financial-pl-ksef-compliance
Open

feat: financial + full KSeF feature set integration#29
haxiorz wants to merge 37 commits into
open-mercato:developfrom
haxiorz:feat/financial-pl-ksef-compliance

Conversation

@haxiorz

@haxiorz haxiorz commented Jul 7, 2026

Copy link
Copy Markdown

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 OSS sales invoices 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 connectivity — FA(3) submission over online and batch (wsadowa) sessions, status polling with KSeF reference numbers, full signed UPO retrieval, inbound receiving/sync → JPK purchase records, RSA-OAEP + AES-256-CBC payload encryption, test/demo/prod environments, 440-duplicate recovery, and a reconciliation sweep worker.
  • Authentication — KSeF token and certificate (XAdES) auth with automatic token→certificate cutover and credential-health monitoring.
  • FA(3) documents — multi-rate VAT, payment block, per-line discounts, gross-price entry, VAT margin, NBP FX, corrections/credit memos, statutory annotations, and MF Wykaz (Biała lista) NIP autofill.
  • Offline & QR — offline24 / awaryjny / niedostępność issuance with statutory deadlines, KOD I/II QR, and ZBP payment QR.
  • JPK — JPK_V7M/V7K generation and e-submission to the Ministry gateway.
  • PDF & backoffice — Polish Faktura VAT PDF with embedded QR; operator pages for invoices, JPK, received invoices, and certificates; per-organization encrypted credentials (ksef_pl provider).
  • Docs & packaging — feature-showcase README + Omdyo attribution; ejectable/prepack/exports compliance; core peer range >=0.6.6 <0.7.0; changeset.

Specification

Does a spec exist for this feature/module?

  • Yes
  • No (created a new spec)
  • N/A (minor change, no spec needed)

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.md

Testing

  • yarn workspace @open-mercato/financial-pl typecheck → clean (exit 0).
  • yarn workspace @open-mercato/financial-pl test530 passed, 14 skipped (env-gated live tests), 59/60 suites.
  • yarn workspace @open-mercato/financial-pl build → 138 files built successfully.
  • Ran the sandbox locally (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).
  • Env-gated live TEST round-trip smoke test (ksef-live): full auth → send → status → UPO against api-test.ksef.mf.gov.pl (FA(3) 1-0E XSD, OpenAPI v2.6.1), plus integration suites TC-KSEF-*.

Checklist

  • This pull request targets develop.
  • I have read and accept the Open Mercato Contributor License Agreement (see apps/docs/cla.md).
  • I updated documentation, locales, or generators if the change requires it.
  • I added or adjusted tests that cover the change.
  • I created or updated the spec in .ai/specs/ with a changelog entry (if applicable).

Linked issues

N/A — new module contribution.

haxiorz and others added 30 commits June 26, 2026 21:05
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>
haxiorz and others added 6 commits July 1, 2026 16:18
…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 zielivia left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. The module isn't a dependency of the sandbox. apps/sandbox/src/modules.ts enables { id: 'financial_pl', from: '@open-mercato/financial-pl' }, but apps/sandbox/package.json has zero references to the package, so yarn install never links it and yarn generate fails with "Package not installed". Please add "@open-mercato/financial-pl": "workspace:*" to the sandbox dependencies.

  2. Platform version skew. packages/financial-pl/package.json declares peer @open-mercato/core|queue|shared|ui: ">=0.6.6 <0.7.0" and devDeps pinned to 0.6.6-develop.6366, while the committed apps/sandbox/package.json pins the whole app to 0.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 way carrier-inpost does.


🔴 Correctness / compliance (all confirmed by tracing the code)

  1. Offline-issued invoices can be re-sent as ONLINE, dropping the offline marking. subscribers/ksef-repoll.ts:138 resets a stuck processing row to queued and emits the online financial_pl.ksef_submission.queued event with no submission.mode check; subscribers/ksef-submit.ts:79 then calls submitInvoiceToKsef without offlineMode:true (and workers/ksef-reconcile.worker.ts doesn't exclude offline-mode rows from the processing sweep). retryCommand routes offline modes to offline_send_requested precisely to preserve the flag — repoll needs the same guard. Consequence: an offline24/awaryjny invoice loses the offline issue date + KOD I/II justification.

  2. A submitted JPK filing's period can be rewritten. commands/jpk.ts upsertFilingCommand's update branch (~:326) Object.assigns year/month/variant/celZlozenia/contextNip with no status guard, while generateCommand (~:428) correctly blocks regenerating a submitted filing. So the header can be changed to a different period while generated_xml/upo_xml/submissionReference still 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.

  3. Received-invoice sync silently skips a page on a transient error. commands/ksef-receive.ts:370 — on a page fetch throw the catch does pageOffset += PAGE_SIZE (advances past the unfetched page instead of retrying it); consecutiveErrors resets on any later success, so a single transient error is never surfaced, and updateCursor still advances the HWM — so up to PAGE_SIZE inbound invoices are permanently dropped and never reach the JPK purchase register.

  4. Sending an already offline_issued invoice returns a raw 500. commands/ksef-submission.ts:239 activeStatuses = ['queued','processing','accepted'] omits offline_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 recovery findOne reuses the same offline-excluding filter → returns null → throw err bubbles a DB error as an unhandled 500. issueOffline and sendBatch both use the full 4-status set — only this path is missing offline_issued.


🟠 Security / UI

  1. invoice-pdf route is under-gated. api/ksef/invoice-pdf/route.ts:28 requires only financial_pl.view, while the sibling invoices list/detail routes deliberately require both financial_pl.view and sales.invoices.manage (with a comment noting that gating on financial_pl.view alone is a permission bypass). A view-only user blocked from the invoice JSON can still fetch the full rendered invoice PDF.

  2. The net/gross ("netto/brutto") toggle is dead in the correction (KOR) form. components/CorrectionForm.tsx:174 renders <InvoiceLinesField> without priceMode/onPriceModeChange, so clicking the toggle is a silent no-op (priceMode defaults to net) — yet onChange(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.)

  3. Hydration mismatch on the invoices month label. backend/financial/invoices/page.tsx:165 formatMonthLabel calls toLocaleDateString(undefined, …) — the SSR (server en) 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/formatCurrency share the same runtime-locale pattern.


🟡 Worth a look (lower confidence)

  • FA(3) line quantity may collapse to 1. lib/fa3-mapping.ts:458 asString(row.quantity) ?? '1' returns '1' when the query engine hydrates quantity as a JS number (value math uses the real quantity via toScaled4) → P_8B=1 but P_11=qty×price. quantity is a pg numeric (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:59 counts locking submissions with organizationId = 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.ts toIsoDate uses toISOString().slice(0,10) (UTC) — a local-midnight Date under TZ=Europe/Warsaw shifts 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. A withFinancialPlRoute() wrapper (or makeCrudRoute) 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 DI ksefSessionService caching the access token per (org, env, NIP) would cut both latency and duplication.
  • List endpoints do avoidable work: api/ksef/submissions/route.ts selects + decrypts invoice_xml/upo_xml for every row only to drop them in toRow; api/ksef/invoices/route.ts runs 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/formatCurrency copied across 5 pages; applyOssMarkers forked 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+Enter submit (the received-sync dialog does — mirror it); the DataTable hosts omit a stable extensionTableId, 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>
@zielivia

zielivia commented Jul 27, 2026

Copy link
Copy Markdown

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)sales_document_sequences, with a unique index on (organization_id, tenant_id, document_kind). It has no notion of a "series". For series to work properly, each one needs its own counter.

The workaround I rejected. Series could live in financial_pl (name + format), with the number rendered on our side and passed to core explicitly as invoiceNumber. The problem: numbers produced outside core's counter can collide with numbers core assigns to invoices created through any other path, and with two sources of truth for numbering you get duplicates or gaps. For a Polish VAT invoice that is a compliance risk, not a cosmetic one — which is why I did not want to slip it into a UI pull request.

Two options worth weighing:

  1. extend sales_document_sequences with a series column and pull it into the unique key — counter per series, owned by core;
  2. or decide deliberately that series live in financial_pl with their own counter table, and core stops assigning numbers to PL invoices.

The first looks cleaner to me, but this is your area.

What already exists here: GET /api/financial_pl/next-invoice-number suggests the next number without consuming the counter (it reads current_value rather than calling claimSequence), so opening and abandoning the form cannot punch a gap in the series. It shows up as the placeholder in the number field and on the document preview, flagged as provisional.

@zielivia

zielivia commented Jul 27, 2026

Copy link
Copy Markdown

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 open-mercato/official-modules:

feat/financial-pl-invoice-ux — three commits rebased onto 423864b, so your post-review fix is included and nothing reverts it.

17c575c  feat(financial-pl): add a bank account inline from the invoice payment section
2761a0c  feat(financial-pl): invoice settings, multi-account payments, and JPK screen fixes
9002465  feat(financial-pl): invoice generator UX pass — full-width layout, drawer preview, field-level validation
423864b  (yours) fix(financial-pl): address PR #29 review — …

git fetch + git merge origin/feat/financial-pl-invoice-ux on your branch brings it into #29.

Conflicts I resolved — please double-check these

CorrectionForm.tsx — both sides added net/gross mode state. I kept your prop name (priceMode: initialPriceMode) and one state, activePriceMode, which the totals recompute, the payload and the effect deps all use. Your const [priceMode, setPriceMode] went as a duplicate: straight after the rebase there were two states for the same thing, and mine referenced yours before its declaration (TDZ). The toggle works, and it now also carries handleLinesChange (recomputes totals on edit) and originalLines (strikethrough of the original values).

Platform versions (apps/sandbox, carrier-inpost, forms, test-package, yarn.lock) — I took your side (0.6.6-develop.6366). My earlier bump to 6422 was local environment setup, not a deliverable.

Verification

yarn test556 passed / 14 env-gated skipped (two runs), typecheck clean, node build.mjs → 148 files. The correction suites (3) pass.

⚠️ Separate issue: the dev server does not boot

Locally yarn dev returns HTTP 500 with ⨯ ./apps/sandbox/src/app/globals.cssError evaluating Node.js code. I checked whether this is mine: it happens on 423864b alone, without any of my commits, so it is not a regression from this branch. Tailwind compiles that file fine on its own (@tailwindcss/cli 4.2.4 → OK), so the failure is on the Turbopack/runtime side. Clearing .next and running dev:reset did not help. Could you check on your machine?

Because of that I could not re-verify this batch in a browser after the rebase — everything below was tested live before it, and confirmed by the automated checks afterwards.

What is in it

  • Invoice settings (new screen): logo + footer printed on the document, defaults for a new invoice, and a list of bank accounts with exactly one default. Deliberately excludes seller identity (it lives on the ksef_pl credential) and numbering (core owns it).
  • Bank accounts as a list, picked on the invoice instead of typed, with an inline dialog to add one without leaving the invoice. The migration backfills the old scalar into the first entry.
  • JPK: the generate step was unreachable from the UI — the "Generate" button only created a draft, download refuses a draft, and submit requires generated. Adds a "Generate XML" row action and renames the card button to "Create draft". Server-side sorting on both tables. Statuses: submitted was amber in one column and green in the next — now consistent. Two errors were leaking an [internal] marker in English.
  • Invoice form: per-line GTU (the union is filed at document level, since JPK_V7 marks GTU on the document), signature block, contract number and transport terms, and validation that marks the offending fields.
  • i18n: pl/de/es at full parity with en.

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.

2 participants