Skip to content

docs(architecture): ADR for the fiscalisation capability + VAT-rate annex on ADR-026 - #2056

Merged
piotrswierzy merged 10 commits into
mainfrom
2009-adr-fiscalisation-capability
Aug 14, 2026
Merged

docs(architecture): ADR for the fiscalisation capability + VAT-rate annex on ADR-026#2056
piotrswierzy merged 10 commits into
mainfrom
2009-adr-fiscalisation-capability

Conversation

@norbert-kulus-blockydevs

@norbert-kulus-blockydevs norbert-kulus-blockydevs commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Documentation only. No production code, no schema, no runtime behaviour change.

What this adds

1. docs/architecture/adrs/042-fiscalization-capability.md (new)

Records the pre-code decisions #1908 depends on, taken from product spec #1902 §4a/§5 rather than re-derived:

  • Why fiscalization is a capability of its own and not an InvoicingPort document type: different issuer, device dependency, legal basis, retry semantics. The near-miss is named explicitly, because ADR-026 already carries a receipt document type for a provider-issued document, which is not a fiscal registration.
  • What adapter Epic: Integrations & Setup #1 actually is: eparagony.pl is a private e-receipt distribution hub (Platforma Detalistów), not a certified fiscalizer and not the state HUB paragonowy - it requires vendor software driving a physical online fiscal printer. The capability's job is to hand the sale to a provider that performs or brokers the registration, never to perform it.
  • The base contract is the invariant transaction operation both published middlewares converged on (fiskaltrust Sign, efsta /register). efsta's second mandatory endpoint /cfg is connection setup, not a per-sale operation, so it is named as such rather than silently dropped.
  • The trust anchor lives in the adapter, never in the contract. Four observed classes, and the class is not stable per country (Italy is mid-migration; Czechia's return is pending legislation), so PL = device never becomes a type.
  • No vendor name and no country assumption in the shared contract, with a litmus test mirroring ADR-026's.
  • The physical-device dependency is a sub-capability ([IMPL] Device/peripheral sub-capability for fiscal-printer regimes #1910), with its shape settled from day one because Poland is a device regime - but no v1 adapter implements it, since eparagony brokers to a printer driven by the vendor's own software. Manifest posture stated: advertised for discovery, resolved only by guard-narrowing.
  • Exactly-once registration is a core-owned contract guarantee: a mandatory idempotency key, a plain per-connection unique index, and an atomic in-flight lease. A repeat resumes status-aware - issued returns verbatim, a live lease returns without a second call, in-doubt is surfaced, and only a terminal rejected re-crosses the boundary - mirroring InvoiceService.resumeExisting / claimForIssue (invoicing: close R2/R3 exactly-once gaps in InvoiceService (retryable discriminator + pending lease) #1200). ADR-005 is cited only for the durable-dedup precedent; its delete-on-failure step is explicitly not adopted, because the row is the in-doubt evidence.
  • An indeterminate outcome is non-terminal and never auto-retried, with a FiscalRegistrationLocator sub-capability to confirm it - both artefacts taken from ADR-035, which solved the same problem for clearance. The failure-mode union is declared in the fiscalization context, mirroring invoicing's shape by design rather than value-importing it.
  • Tax calculation is out of scope: the VAT rate arrives from the ProductMaster and a fiscal registration must not recompute the amounts it transmits.
  • Deferred items are stated as deferred with reasons: journal/audit export (PL kasy transmit to the CRK themselves, so there is no journal to export), fiscal corrections and voids (a paragon cannot be corrected on the device at all - which makes persisting the receipt number and numer unikatowy a [IMPL] Surface receipt status + link on the order (BaseLinker parity) #1909 requirement, not a deferral), a second adapter, the per-order legal-obligation determination, the Portugal certification question, and who builds the eparagony connector ([IMPL] Secure eparagony.pl integrator-class listing + API/sandbox access (BLOCKING, non-engineering) #1907).
  • Spelling is settled explicitly: -ization everywhere - code, capability value, context path, and prose - matching the repository's house style by a wide margin. The GitHub issue titles keep -isation; renaming them would break inbound links for no gain.

The legal paragraph cites art. 111 ust. 6a (what a kasa must do) plus ust. 6b (who must obtain the potwierdzenie Prezesa GUM), applied odpowiednio by art. 111b ust. 2, and carries the spec's not-legal-advice caveat. Both new documents also note that most PL e-commerce is exempt and has no receipt to issue at all.

2. VAT-rate amendment on docs/architecture/adrs/026-country-agnostic-invoicing-domain.md

Records the 13 Aug 2026 decision, appended as an amendment rather than an edit to the accepted body: the rate arrives from the ProductMaster, the withdrawn TaxCalculationPort is recorded as the rejected alternative, the rate is a code and not a number (PL invoice markers 23/8/5/0/zw/np/oo, and paragon rates carried as letters A-G), a missing rate blocks issuance, a channel-reported rate is a cross-check that can raise a tax-rate conflict blocking both invoice issue and fiscalization, and the order snapshot persists net and gross per line. The no-migration claim is scoped to the invoicing side; the product-side field, the conflict state and the per-line columns are named as #2054's. The conflict flag rides the Order projection AutoIssueTriggerService already receives, preserving its documented F3 one-way edge.

3. Index and overview

ADR index row, plus a new 16. Fiscalization (planned) section in docs/architecture-overview.md where the capability is described once (with the standard italic ADR pointer), a one-line FiscalizationPort pointer under Future Capability Ports, and two policy bullets on the Invoicing section - placed above the adapter roster, as in the other contexts.

Numbering

038 was taken on main in the meantime, so this ADR is not 038 as the issue's acceptance criterion once said. It landed as 040 at review time, then main moved again: #2055 (the sibling sales-document routing ADR) merged as ADR-041, and two more numbers (039, 040) were claimed by unrelated in-flight PRs before this branch could rebase past them. This branch is now merged forward past all of that and the fiscalization ADR is ADR-042, cross-linked to the real, now-merged ADR-041-sales-document-routing-policy.md. No dead links remain anywhere in this diff.

Deliberately not in this PR

Notes for the reviewer

Part of #2009. Deliberately not Closes - scope item 2 of that issue (the #1908 implementation plan) is excluded here, because #1907 has not settled who builds the connector, so merging this must not auto-close the issue while that acceptance criterion is unmet.

🤖 Generated with Claude Code

…nnex on ADR-026

Records, ahead of #1908's code, why fiscalisation is a capability of its own
rather than an InvoicingPort document type, and where each of its contract-level
guarantees lives.

- ADR-040 (new): fiscalisation as a capability distinct from invoicing. Base
  contract taken from the published fiskaltrust / efsta shape (register a
  transaction, receive back what must be printed), trust anchor confined to the
  adapter, the device dependency expressed as a sub-capability (#1910), and
  exactly-once registration owned by core because a double fiscal registration
  is a legal event for the seller. Journal export, fiscal corrections, a second
  adapter and the legal-obligation determination are recorded as deferred with
  reasons.
- ADR-026: annex recording where a line's tax rate comes from - the master, not
  a computation - with the rejected TaxCalculationPort alternative and the
  tax-rate-conflict state that blocks both invoice issue and fiscalisation. The
  ADR-014 supersession it requires is noted and left to #2054.
- ADR index + architecture-overview: register ADR-040 and describe the planned
  fiscalisation capability where a reader first meets it.

Numbering note: 038 was taken by the rate-limiting ADR (#2019, merged today), so
fiscalisation is 040 and the sibling routing ADR (#2051) keeps 039.

Refs #2009

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpwFwSVZYF7nopZ5S3Peet
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
…nt routing

The sibling sales-document routing ADR is now numbered (039, #2051), so refer to
it by number and link it, instead of citing the issue alone.

Refs #2009

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

Copy link
Copy Markdown
Collaborator Author

Tech lead review: ADR-040 fiscalisation capability + ADR-026 VAT-rate annex

Scope: 4 files, +188/-0, documentation only. docs/architecture/adrs/040-fiscalisation-capability.md (new), the VAT-rate annex on 026-country-agnostic-invoicing-domain.md, ### 15. Fiscalisation + a FiscalisationPort bullet in docs/architecture-overview.md, one ADR index row.

Summary

This is a well-constructed ADR and the central decision is right. Fiscalisation genuinely is not an InvoicingPort document type, the four-reason justification holds, and calling out the near-miss (ADR-026's documentType really is open-world and really does already carry a receipt value, so "it would even type-check") is the kind of thing that saves a future contributor a week. Decision 5's FiscalDeviceOperator matches the shipped guard-narrowing pattern exactly. Decisions 3 and 4 are direct reuses of ADR-026's proven neutrality posture, litmus test included. The deferral list says why for every item, which is rare and worth keeping.

The problems cluster in three places, none of which is the core decision:

  1. Decision 6 + 7 do not compose. As written, "return the existing record on a repeat" plus "never blind-retry" leaves a terminal rejected registration with a permanently burned idempotency key, and the cited precedent does not behave the way the ADR describes it.
  2. Adapter Epic: Integrations & Setup #1 does not match the capability's own framing. eparagony.pl is a receipt-distribution hub, not a certified fiscal provider, which leaves decision 5's day-one device sub-capability without a v1 implementer.
  3. Five links point at an ADR that does not exist yet.

Plus a handful of legal-citation corrections that matter because a compliance ADR gets quoted at sellers.

Below is the consolidated set. Line references are to the files as they stand on this branch.


Blocking

B1. eparagony.pl is not a "certified fiscal provider", and that breaks decision 5's premise

040-fiscalisation-capability.md Decision 1 and architecture-overview.md § 15 both describe the capability as "register a sale with a certified fiscal provider", with eparagony.pl as adapter #1.

eparagony.pl is Platforma Detalistów sp. z o.o., a private e-receipt distribution hub. It does not fiscalise; it explicitly requires communication with a fiscal printer and positions itself as "independent of the state system and simultaneously its equivalent" (the state one being MF's HUB paragonowy). Sources: https://www.eparagony.pl/panstwowy-hub-paragonowy/ , https://www.podatki.gov.pl/aplikacja-e-paragony/hub-paragonowy/ . This PR's own source spec agrees, at docs/specs/product-spec-1902-eparagony-e-receipts.md:43: "eparagony.pl itself requires vendor-proprietary software running on a machine that drives a physical online fiscal printer".

The knock-on is what makes this blocking rather than cosmetic. Decision 5 justifies shipping FiscalDeviceOperator from day one on the grounds that "Poland, the first market, is a device regime". True, but the printer is driven by the vendor's software, not by OL through eparagony.pl. So FiscalDeviceOperator has no v1 implementer, and decision 5 as argued is unfalsifiable.

Suggested fix: reword the capability to "a provider that performs or brokers the fiscal registration", state plainly that adapter #1 is a distribution hub sitting in front of a device somebody else operates, and then either name who implements FiscalDeviceOperator in v1 or defer it with that reason.

B2. Decision 6 + 7 burn the idempotency key on a terminal rejected, and misdescribe the cited precedent

040-fiscalisation-capability.md decisions 6 and 7.

Decision 6 says core "returns the existing record on a repeat". Decision 7 says in-doubt is never auto-retried. Composed, every repeat of a key returns whatever is stored, so a terminal rejected (the one failure mode that is safe to retry) has no path back across the boundary. The operator is stuck: key burned, nothing registered, and the only escape is minting a fresh key, which defeats the guarantee the decision exists to provide.

The cited precedent does not work that way. InvoiceService.resumeExisting (libs/core/src/invoicing/application/services/invoice.service.ts:284-316) is status-aware:

  • issued returns verbatim
  • live lease returns with no provider call
  • failed and not isReattemptableFailure returns with no provider call
  • otherwise (pending, expired issuing lease, terminal rejected failed) re-crosses the boundary

with isReattemptableFailure = status === 'failed' && failureMode === 'rejected' (invoice-record.entity.ts:147-149). The ADR cites the gate but describes only its issued branch.

It also drops the single-flight half. The invoicing gate is a unique index plus an atomic lease claim (claimForIssue(recordId, leaseExpiresAt), invoice.service.ts:329-348; isLeaseLive, invoice-record.entity.ts:156). Without the lease, two concurrent same-key calls both pass the read gate and both call the provider. That is a double fiscal registration, precisely the legal event decision 6 exists to prevent.

Suggested replacement for decision 6's outcome clause:

On a repeat the record is resumed under a status-aware fiscal-safety invariant, not returned blindly: an already-registered record returns verbatim; a record under a live in-flight lease returns without a second outbound call; an in-doubt failure returns for manual reconciliation (decision 7); a terminal rejected failure is re-attemptable under the same key. Exactly-once therefore requires the unique index and an atomic in-flight claim (lease), mirroring InvoiceService.resumeExisting / claimForIssue (#1200).

B3. Five dead links to ADR-039, plus a 038 -> 040 index gap

039-sales-document-routing-policy.md does not exist on this branch or on main. It is linked from 040-...md (decision 9 deferral #4, Cons, References) and architecture-overview.md § 15. It lives only in PR #2055 (2051-adr-sales-document-routing-policy, open, based on main).

Two reasons this is blocking rather than a nit:

  • I validated every ](./NNN-*.md) link across all 38 existing ADRs. There are zero broken ADR cross-links in the repo today. This would be the first, and nothing catches it: pnpm lint runs 15 scripts/check-*.mjs invariants, none of which touch docs/, there is no markdownlint or remark config, no link checker in .github/workflows/, and format:check is not wired into CI. Confirmed via gh pr checks 2056: Type Check pass, path filter correctly skipping code jobs.
  • Deferral Epic: Orders & Routing #4 (which order legally requires a receipt) is the most legally sensitive item in the ADR, and it defers to a document the reader cannot open.

The index gap compounds it. README.md § How to write a new ADR, step 1: "Copy template.md to NNN-kebab-case-title.md where NNN is the next free 3-digit number (check the index below)." With the table reading 037, 038, 040, the next contributor allocates 039 and collides with #2055.

Suggested fix: land #2055 first and mark this PR as stacked on it, or downgrade the five references to plain #2051 text until 039 exists. If merge ordering is the answer, say so in the PR description so the reviewer of #2055 knows.

B4. Wrong noun in a normative annex rule

026-country-agnostic-invoicing-domain.md, annex:

- **The carrier is a code, not a number.** \0`, `zw`, reverse charge and intra-EU 0% are four different things...`

The bullet is about the tax rate. carrier is live shipping-domain vocabulary in this repo (DestinationOptionsReader.listCarriers()), so an implementer skimming bold lead-ins reads a shipping statement in the middle of a tax rule. One-word fix:

- **The tax rate is a code, not a number.**


Important

Contract and implementability

I1. "The first non-PL adapter needs no core PR" is false, and the migration path omits why.
IntegrationsService.getCapabilityAdapter gates on connection.enabledCapabilities.includes(capability) (libs/core/src/integrations/application/services/integrations.service.ts:108), and enabledCapabilities is written through DTOs that are strict: @IsIn(CoreCapabilityValues, { each: true }) (apps/api/src/integrations/http/dto/create-connection.dto.ts:114, update-connection.dto.ts:56). CoreCapabilityValues (libs/core/src/integrations/domain/types/adapter.types.ts:22-36) has 8 entries, ending at 'Invoicing'. The FE mirrors it (apps/web/src/features/connections/api/connections.types.ts:21). Without a core edit, POST/PATCH /connections with enabledCapabilities: ['Fiscalisation'] returns 400 and the adapter is unreachable, which breaks #1911 directly. The existing escape (DPD/InPost omit enabledCapabilities and let the service infer) is a documented wart, not a design.
Fix: add to Migration path that #1908 also adds 'Fiscalisation' to CoreCapabilityValues, its spec assertion, and the FE mirror; and soften the Consequences line to "no core domain PR". Also worth stating in decision 1 whether Fiscalisation joins the closed list or rides the open-world string escape (#576).

I2. ADR-005 and ADR-026 are cited as "the same gate" but have opposite failure-path semantics.
ADR-005 step 6: "On publish failure: DELETE the row" (005-postgres-authoritative-job-dedup.md:22), releasing the key so the source's retry re-enters. ADR-026 / InvoiceService never delete: the row goes failed + failureMode and stays, which is exactly what makes in-doubt observable. Those are mutually exclusive answers to "what happens to the pre-written record when the outbound call throws", and for fiscalisation the ADR-005 answer is actively wrong, because deleting on throw is the blind-resend path decision 7 forbids.
Fix: keep the ADR-005 citation only as the durable-Postgres-dedup precedent, and say explicitly that its delete-on-failure step is not adopted, because the row is the in-doubt evidence.

I3. "Partial unique index" copies a predicate that probably does not apply.
The invoicing index is partial for one concrete reason: the column is nullable and keyless calls are never deduplicated. UQ_invoice_records_connection_idempotency ... where: '"idempotencyKey" IS NOT NULL' (invoice-record.orm-entity.ts:44-46), matching the if (key !== undefined) read gate (invoice.service.ts:222-228). ADR-040 says the command carries a caller-supplied idempotencyKey, i.e. mandatory. If it is mandatory, "partial" is copy-paste and it should be a plain unique index. If it is optional, say so, because "exactly-once is a contract-level guarantee owned by core" is then false for keyless calls.

I4. Decision 7's "resolves by re-reading provider state" has no port to read with, and ADR-035 already solved this.
ADR-035's answer to "we crashed mid-submit and cannot tell if it landed" is two artefacts: a non-terminal status (RegulatoryStatus = 'pending-submission', deliberately excluded from TerminalRegulatoryStatusValues so a sweep keeps advancing it), and a RegulatoryRecordLocator sub-capability (locateByQuery(criteria)), because after an indeterminate call you have no provider id and must query by business coordinates. ADR-035 is explicit that an adapter without one gets manual handling, never a blind resubmit. ADR-040 has neither: decision 2 says the base port is one operation "and nothing else", decision 5 names only FiscalDeviceOperator. So the sentence is unimplementable as written. Separately, InvoiceFailureModeValues is a failure discriminator on a terminal failed row (invoicing.types.ts:55-65), not a resumable status, so reusing it gives fiscalisation the taxonomy without the machinery.
Fix: add a FiscalRegistrationLocator sub-capability mirroring ADR-035's, state whether it ships in #1908 or is deferred, and say whether in-doubt is a terminal discriminator (invoicing's shape) or a non-terminal sweep-advanced status (ADR-035's shape). Today the ADR implies the second while reusing the first.

I5. The ProductMaster tax-rate rule has zero contract support today, and "implies no migration" is contradicted two bullets later.
Product (libs/core/src/products/domain/entities/product.entity.ts:19-56) has price, currency, weight, categories, features and no tax field. ProductVariant (product-variant.entity.ts:17-50) likewise. ProductMasterPort returns exactly those two. The only tax resolver in the repo is PrestashopTaxRateResolver (libs/integrations/prestashop/src/infrastructure/provisioners/prestashop-tax-rate.resolver.ts:44), wired into the destination order-processor adapter for cart pricing, not on the ProductMaster read path at all. WooCommerce surfaces tax_class only on the outbound publish side. So the rule implies a new field on Product/ProductVariant, a column, a mapping change in every ProductMaster adapter, and master-sync plumbing. Deferring all that to #2054 is legitimate, but the annex then asserts "the rule itself implies no migration", which is true only of InvoiceLine.taxRate and is contradicted by its own next two bullets, both of which require new persisted state (a tax-rate conflict with actor and timestamp; net and gross per line on the order snapshot).
Fix: scope the claim: "implies no migration on the invoicing side", then name the product-side, conflict-state and per-line net/gross schema as #2054's.

I6. The conflict gate collides with AutoIssueTriggerService's documented one-way edge.
The annex says "AutoIssueTriggerService must not fire on an unresolved conflict". That service carries a hard constraint in its own header: "ONE-WAY EDGE (F3): depends ONLY on CONNECTION_PORT_TOKEN and SYNC_JOBS_SERVICE_TOKEN. It injects NO OrdersModule token" (auto-issue-trigger.service.ts:23-24), because OrdersModule imports InvoicingModule and an orders token closes a CJS cycle (same reason it imports PAYMENT_STATUS from the @openlinker/core/orders/types sub-barrel at :42). If the conflict state needs an orders lookup, honouring the annex breaks that edge. If it rides the Order projection the service already receives, it is free. The annex does not say which, and it also asserts the same conflict blocks fiscalisation, so a third context reads it too.
Fix: state that the flag is carried on the Order projection the trigger already holds, preserving the F3 edge.

I7. Decision 7's cross-context value import contradicts decision 1.
Mechanically it passes: InvoiceFailureModeValues is exported from the invoicing barrel (libs/core/src/invoicing/index.ts:11) and scripts/check-cross-context-imports.mjs allows "any other identifier", with deny patterns limited to *RepositoryPort / *OrmEntity / *Adapter / *Dto / default / namespace. The lint gate would be green. But the documented contract table in architecture-overview.md scopes value constants to UPPER_SNAKE_CASE, and InvoiceFailureModeValues is PascalCase, so the ADR relies on the script being laxer than the doc. More to the point it is a design smell: decision 1's premise is that fiscalisation has a different legal basis and different retry semantics, yet decision 7 makes its failure taxonomy a live runtime dependency on invoicing's. When #1214 extended that area (InvoiceFailureCodeValues), fiscalisation would have silently inherited or diverged.
Fix: declare FiscalRegistrationFailureModeValues = ['rejected', 'in-doubt'] in the fiscalisation context, mirroring the shape by design rather than importing it. If sharing is genuinely wanted, say so and note it creates a fiscalisation -> invoicing edge.

Internal contradictions

I8. The Migration path is the alternative the ADR rejects.
Alternatives: "A registration-only port, with device printing added later. Rejected: registration alone does not cover Poland, so v1 would not work in its own first market". Migration path: "#1908 ships the context, the base port, the neutral command/result and the registration record with its idempotency index; #1910 adds the device sub-capability". That sequence is registration-only followed by device printing later. Decision 5 compounds it by saying "required from day one" while assigning the work to a separate later issue. A reader cannot tell whether #1908 alone is a shippable PL v1.
Fix: keep the rejection (it is about a shipped end state, not an intermediate PR) and add to the Migration path that PL v1 is not usable until both #1908 and #1910 have landed. At decision 5, "required from day one" reads better as "required for the first usable release".

I9. The ADR states a blocker that is already resolved, contradicting its own Context.
Migration path: "#1908 must not ship before #1906 closes." gh issue view 1906 shows CLOSED (2026-07-30). The ADR's own Context says "#1906 closed the certification question for Poland" and References says "closed for PL".
Fix: "#1906 (the certification-liability prerequisite) is closed, so nothing gates #1908 on it; #1907 gates only the eparagony.pl adapter half, not the neutral capability."

Legal and domain accuracy

I10. Wrong provision carries the "feeding vs being" line, and "homologation" is the wrong term.
Context attributes to art. 111 ust. 6b "the GUM homologation duty". 6b assigns who must obtain a potwierdzenie Prezesa Głównego Urzędu Miar ("Producenci krajowi i podmioty dokonujące WNT lub importu kas ... w celu wprowadzenia ich ... do obrotu"). What makes software a kasa at all is art. 111 ust. 6a (functions, technical requirements, CRK transmission, pamięć fiskalna) plus the technical-requirements rozporządzenie. The act says potwierdzenie, not homologacja. Sources: https://sip.lex.pl/akty-prawne/dzu-dziennik-ustaw/podatek-od-towarow-i-uslug-17086198/art-111 , https://www.gum.gov.pl/pl/dla-biznesu/uslugi/kasy-rejestrujace .
The source spec's R1 already gets this right; the ADR compressed it wrong. Fix: "art. 111 ust. 6a (what a kasa must do) plus ust. 6b (who must obtain the potwierdzenie Prezesa GUM), applied odpowiednio by art. 111b ust. 2".
Everything else in that paragraph checks out: art. 111b ust. 2's odpowiednio wording, Dz.U. 2025 poz. 775 as the correct consolidated text, and GUM as the right authority.

I11. The spec's "not legal advice" caveat is dropped in both new documents.
Spec §8 R1 ends: "this establishes what the statute says. It is not legal advice, and no seller-facing compliance claim should be made on it without a professional opinion (per #1906's stated out-of-scope)." #1906 itself lists "Legal advice" under Out of scope. ADR-040's Context instead says the constraint "is non-negotiable and comes from primary law", and architecture-overview.md § 15 states "OpenLinker never issues a fiscal receipt" flatly. These are the two sentences in this PR most likely to be quoted at a seller.
Fix: carry the spec's caveat sentence into both.

I12. Czechia 2027 is pending legislation, stated as settled fact.
Decision 3: "Czechia has no obligation today but returns in 2027 as pure remote reporting". EET 2.0 passed the Chamber of Deputies in third reading on 15 Jul 2026 and went to the Senate; not yet signed. https://www.mf.gov.cz/cs/ministerstvo/media/tiskove-zpravy/2026/snemovna-schvalila-eet-2-0-64564 , https://financnisprava.gov.cz/cs/financni-sprava/media-a-verejnost/tiskove-zpravy-gfr/tiskove-zpravy-2026/snemovna-schvalila-eet-2-0 . The "pure remote reporting, no mandatory printing" characterisation is correct.
Fix: "a bill approved by the lower house on 15 Jul 2026 (Senate and president pending) would return it from 1 Jan 2027". Since this fact carries decision 3's "the class is not stable per country" argument, it should be stated at the confidence the source supports.

I13. "All three inputs are held by the shop, not by an aggregator" is false in OL's own architecture, and the sentence miscounts itself.
The annex says the rate "is a function of (product, delivery country)" (two inputs) and then "all three inputs an engine needs (product tax category, delivery address, seller nexus) are held by the shop". For an Allegro-sourced order OL ingests IncomingOrder.shippingAddress (libs/core/src/orders/domain/types/order.types.ts:99) from the marketplace and pushes it into the shop; the shop only knows the delivery address because OL told it. Seller nexus and registrations are connection-level config OL already holds (KSeF, inFakt). Only the product tax category is unambiguously the shop's.
Fix: keep the ADR-014 source-authoritative argument, which is sound on its own, and drop the delivery-address and nexus legs. Suggested clause: "the rate is a function of (product tax category, delivery address, seller nexus), and the product tax category is the shop's alone".

I14. The tax-code enumeration is incomplete and mixes code with reason.
"0, zw, reverse charge and intra-EU 0% are four different things". PL document markers are 23/8/5/0, zw, np (nie podlega) and oo; np is missing. Intra-EU 0% (WDT) is a use of stawka 0%, not a distinct code. Domestic odwrotne obciążenie was repealed 1 Nov 2019 and replaced by obligatory MPP, surviving mainly for art. 17 and cross-border services. Sources: https://flexiclosing.pl/faktura-bez-stawki-vat-usluga-ksiegowa-zwolnienie/ , https://ksiegowosc.infor.pl/podatki/vat/rozliczenia-vat/3081633,Koniec-procedury-odwrotnego-obciazenia-w-VAT.html .
Also, a stronger supporting fact is available and unused: on a PL paragon, rates are carried as letters A-G. That is a second, fiscal-side reason the value must be a string, and it belongs in an annex that ADR-040 leans on for exactly that reason.

I15. One citation in the annex points at the wrong line.
"one optional flag for the whole order (libs/core/src/orders/domain/types/order.types.ts:264)". Line 264 is PriceTaxTreatmentValues; the optional flag is OrderTotals.taxTreatment at :283. Separately, IncomingOrderItem is not in that file at all: it is incoming-order.types.ts:131 with price: number at :146, and its sibling IncomingOrderTotals.taxTreatment is at :190. The prose claim is correct, only the pointer is off. Worth fixing because the PR description states every code reference was verified at the cited lines; the other five all check out exactly.

I16. The corrections deferral is the right call for a better reason than given, and it hides a v1 data requirement.
The ADR defers corrections/voids/returns on "every regime constrains them differently and v1 has no demand". In PL a paragon cannot be corrected on the device at all: returns and accepted complaints go into a separate ewidencja zwrotów towarów i uznanych reklamacji (§3 ust. 3, rozporządzenie MF 29.04.2019 ws. kas rejestrujących) and obvious errors into a second register (§3 ust. 4). https://poradnikprzedsiebiorcy.pl/-nowe-zasady-ewidencji-korekt-na-kasach-fiskalnych . So the deferral is legally safe for PL, which is a much stronger argument than absence of demand.
But §3 ust. 4 requires, for an electronic paragon, "podanie numeru tego paragonu oraz numeru unikatowego". If #1909 does not persist and surface the receipt number and numer unikatowy, the seller cannot keep the one correction register PL actually mandates. That is a v1 requirement on #1909, not a deferral.


Suggestions

  1. Annex is a new coinage next to the established Amendment. The corpus uses "Amendment" (026:103, 030:124, 009:38; only 024:139 deviates with "Update"); "Annex" appears nowhere. There is also a shape mismatch with its own sibling: the existing block directly above is a bold run-in paragraph, the new one is an ### heading, so a peer note renders as a subsection of the amendment area. Suggest **Amendment (2026-08-13, #2009).** as a run-in bold paragraph.
  2. **Planned location** / **Planned capability** drift from the section vocabulary. Every numbered context uses bare **Location** / **Capability** (§12, §13, §14). The header already carries "(planned)". Relatedly, ### 15. Fiscalisation (planned - no code yet, #1908) is the only header in 1-15 carrying a status and an issue number; the only other parenthetical is ### 6. Listings (Offers), a synonym. Suggest ### 15. Fiscalisation (planned) with "no code yet, [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908" moved into the Responsibility bullet.
  3. Same capability announced twice, unlike the PricingAuthorityPort precedent. PricingAuthorityPort lives only under Future Capability Ports, including its already-shipped seam detail; it got no numbered section. FiscalisationPort gets both, with the base contract restated verbatim in each. Two copies of a not-yet-frozen contract will drift. Suggest trimming the Future-ports bullet to a one-line pointer. Also note this is the first numbered bounded context with no code at all; sections 1-14 all describe shipped contexts, and the existing precedent for unbuilt things is either Future Capability Ports or an inline (future) marker. Not wrong per CLAUDE.md, but worth flagging in the PR description as a deliberate new precedent.
  4. Two policy bullets land after §14's adapter roster. "Not fiscalisation." and "Tax rates are an input" are scope statements appended below the KSeF/Infakt/Subiekt list. Suggest moving both above the roster so the adapter list stays the tail of the section, as in the other contexts.
  5. FiscalDeviceOperator manifest posture is unstated. [IMPL] Fiscalisation provider connection setup + preconditions UI #1911 will want to show device support, which needs the manifest entry, and the pattern carries a live footgun: getCapabilityAdapter(connectionId, 'FiscalDeviceOperator') passes the manifest gate then fails inside dispatchCapability with a generic Error, and in the list path aborts the whole listing. Suggest the standard sentence: declared in the manifest for host/FE discovery, resolved only by narrowing the dispatched Fiscalisation adapter with isFiscalDeviceOperator.
  6. "The invariant core is one operation" over-compresses both vendors. efsta has two mandatory endpoints, /register and /cfg, and /cfg is a company/base-data write, not static config (https://docs.efsta.eu/efr/api/); fiskaltrust v0 exposes three (echo/sign/journal). The ADR's own parenthetical concedes /cfg. Suggest "one invariant transaction operation; company/base-data registration is connection setup, journal is a deferred sub-capability", otherwise the base port silently drops efsta's /cfg obligation.
  7. Two deferrals are safer than argued. Journal export: PL online kasy transmit to the Centralne Repozytorium Kas themselves and hold pamięć fiskalna/chroniona (art. 111 ust. 6a), so OL has no journal to export. Saying that beats "no user has asked" for a legal-adjacent deferral.
  8. Counts and antecedents. "A fourth document-ish capability" (Consequences) and "a TaxCalculationPort engine-facing fourth capability" (annex) both use "fourth" with nothing enumerating 1-3, and cannot both be true. Suggest "a second document-producing capability" and dropping "fourth" from the annex. (For the record: decision 1's "four independent reasons" and decision 3's "four classes" both count correctly.)
  9. Precision on the channel-rate claim. "Today only Erli does" is true for per-line rates; WooCommerce's order source does report an order-level tax amount (woocommerce-order-source.adapter.ts:245), unlike Allegro's hardcoded tax: 0. Suggest "only Erli reports a per-line rate".
  10. References hygiene. ADR-035 is listed as the "indeterminate-outcome lifecycle precedent" but decision 7 never cites it (see I4, where it should be load-bearing). "§5 US-1/US-3/US-5" is listed but only US-5 appears in the body. [IMPL] Fiscalisation provider connection setup + preconditions UI #1911 is cited in architecture-overview.md but missing from ADR-040's Related issues. Also worth a **ADR:** header line in product-spec-1902-eparagony-e-receipts.md pointing back here, matching product-spec-1157-order-status-roundtrip.md:6.
  11. Spelling, worth settling now rather than at [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908. Repo house style is -ize/-ization by a wide margin in docs/ (normaliz* 464 vs normalis* 40, authoriz* 244 vs authoris* 3), and code identifiers follow it. fiscalis* currently leads 8 to 4, but all 8 hits are in this PR's own source spec. The cost is only on the code half: libs/core/src/fiscalisation/ plus a Fiscalisation capability value would be the sole -isation identifier in the tree.
  12. Two readability rewrites. (a) "The neutral capability is ours either way, so this ADR is not blocked by that answer; the implementation plan for [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908 is, and is therefore not part of this change." The elliptical "is," has no readable antecedent. Suggest splitting into two sentences with "is blocked by it". (b) The annex's conflict-state sentence fuses three normative rules into one clause chain; suggest three sentences.
  13. PR description trailer is malformed but safe. "Part of [IMPL] ADR + implementation plan for the fiscalisation capability (pre-code inputs to #1908) #2009 (deliberately NOT : scope item 2 of that issue, ...)" has a stray space before the colon and a missing verb. Confirmed there is no Closes/Fixes/Resolves anywhere in the body, so merging will not auto-close [IMPL] ADR + implementation plan for the fiscalisation capability (pre-code inputs to #1908) #2009, matching the stated intent.
  14. Addressable-market caveat. The spec (§2) establishes that most PL e-commerce is exempt and "has no receipt to issue at all"; only §4-excluded categories are obliged. ADR-040's Context reads as if a PL fiscal v1 has a general market. One sentence would fix it.
  15. Length. 1665 words against the README's "aim for under 500". The overrun is in ## Decision (9 multi-paragraph items), not in Alternatives, which is the escape clause the README sanctions. Mitigating: the corpus routinely ignores this (026=1449, 033=1986, 024=2510, 037=3842), so 040 is roughly 5th longest, not an outlier. Noting for completeness, not asking for a rewrite; decisions 3/5/6/7 are the compressible ones.

Verified correct

Recorded so the next reviewer does not re-do it.

Code citations in the ADR-026 annex (all against this branch): order-to-issue-invoice-command.mapper.ts:228 and :249 are both literally taxRate: ''. invoicing.types.ts:280 is taxRate: string on InvoiceLine; :329-336 is exactly IssuedLineSnapshot. erli-order.types.ts:79 is taxRate?: string. Allegro: allegro-order-source.adapter.ts:404 tax: 0, :412 taxTreatment: 'inclusive', with an in-code comment at :409 saying it does not decompose tax. IncomingOrderItem carries price: number with no tax field, and taxTreatment really is order-level only, so "cannot describe a mixed-rate basket" holds. Only the :264 pointer is off (I15).

"Each provider adapter substitutes its own guess" is true and worse than stated: KSeF connection defaultTaxRate (ksef-invoicing.adapter.ts:1173-1184), Subiekt DEFAULT_PL_VAT_RATE (subiekt-line.mapper.ts:33), inFakt DEFAULT_PL_VAT_SYMBOL/DEFAULT_PL_VAT_RATE (infakt-invoicing.adapter.ts:202,215).

Cross-ADR claims: ADR-026 Decision 1 does define a neutral per-line taxRate and is silent on its supplier. documentType is open-world and already carries receipt (invoicing.types.ts:17-32), so the near-miss framing is fair. InvoiceFailureModeValues = ['rejected', 'in-doubt'] is exact (invoicing.types.ts:64, spec-locked at invoicing.types.spec.ts:38). ADR-026 decision 4 is exactly idempotencyKey plus durable partial-unique index on (connectionId, idempotencyKey) and itself points at the webhook gate, which ADR-005 confirms as uq_webhook_deliveries_event_key. ADR-014 "Alternatives considered" (014:47-49) really does reject "Per-line tax-inclusivity / tax rate on OrderItem", so the supersession claim is grounded. Every link target other than 039 resolves.

Spec claims: §4a supports every finding cited, including fiskaltrust EUPL-1.2 with Sign returning "data that need to be printed", efsta's "only /register and /cfg mandatory", the /peri/* device-regime leak behind decision 5, the four trust-anchor classes, and efsta = PL plus 16 further jurisdictions (17 documented, https://docs.efsta.eu/efr/api/ , https://github.com/fiskaltrust/middleware ). §4 option E really is the ReceiptHub seam and really is marked superseded by §4a. §6.3/6.4/6.5/6.7 and §8 R1 exist as cited, Portugal genuinely access-blocked rather than ambiguous. Issue mapping matches spec §9 exactly.

Legal: art. 111 ust. 6b's text and scope, art. 111b ust. 2's odpowiednio wording, Dz.U. 2025 poz. 775 as the consolidated text, GUM/Prezes GUM as the right authority, Italy mid-migration RT to certified software (art. 24 D.Lgs. 1/2024), and "a completed fiscal registration cannot be un-issued" for PL.

Conventions: ADR-040 matches the template exactly (title line, Status/Date/Authors order, Context -> Decision -> Alternatives considered -> Consequences -> References, nothing missing or extra, **Migration path:** matching ADR-026's precedent). Proposed is the right status, the date and @handle author form are right, numbering and # ADR-NNN: Title are right, all links are relative with no full GitHub URLs (so check-repo-urls.mjs passes), and the index row matches its neighbours column for column. The architecture-overview.md cross-link is verbatim the README's template. docs/capabilities.md is correctly untouched, since its front-matter binds it to libs/core/src/**/domain/ports/** and no port exists yet; docs/lessons.md likewise. ADR-026's body is unedited, honouring append-only. ADR-014 is untouched, as the PR description claims. 038 is genuinely taken on main, so 040 is a free number.


Merge readiness

Request changes. The decision is right and should land; four things should change first.

Priority order:

  1. B3 (dead ADR-039 links plus index gap). Cheapest to fix and the only one with a hard sequencing answer: land docs(adr): add ADR-041 sales-document routing policy #2055 first, or drop the links to plain #2051.
  2. B2 (decisions 6 + 7). This is the one that would actually cost money if [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908 implemented the ADR literally. The fix is a rewritten outcome clause plus the lease, both of which already exist in InvoiceService and just need describing accurately.
  3. B1 (eparagony.pl framing). Reword the capability and say who implements FiscalDeviceOperator in v1.
  4. B4 (carrier -> tax rate). One word.

Then the IMPORTANT set, most of which are single-sentence edits. I2/I3/I4 travel with B2 and are best done in the same pass. I10/I11/I12/I13/I14/I16 are the compliance-accuracy pass and should be done together, since they all touch text that will be quoted at sellers.

On the wider question the PR description raises: yes, merge a Proposed ADR with #1907 unresolved. That is a commercial question about adapter #1, and the ADR's subject is the neutral capability, which is ours either way. Proposed is the correct artefact for exactly this state and the repo has precedent (035 and 037 are both Proposed with downstream work pending). Excluding the #1908 implementation plan from this PR is the right call, and the trailer correctly avoids auto-closing #2009.

…ate amendment

Addresses the tech-lead review on PR #2056.

Blocking:
- eparagony.pl is a private e-receipt distribution hub, not a certified
  fiscaliser; say so, reword the capability to "performs or brokers the
  fiscal registration", and state that no v1 adapter implements
  FiscalDeviceOperator (the printer is driven by the vendor's software).
- Rewrite decision 6's outcome clause: a repeat resumes status-aware
  (issued verbatim / live lease / in-doubt surfaced / terminal rejected
  re-attemptable) and exactly-once needs the unique index AND an atomic
  in-flight lease, matching InvoiceService.resumeExisting / claimForIssue.
- Drop the five links to ADR-039, which does not exist yet (PR #2055),
  down to plain #2051 references, and record 039 as reserved in the index
  so the next contributor does not collide with it.
- "The carrier is a code" -> "The tax rate is a code".

Important:
- Migration path now names the CoreCapabilityValues + FE-mirror additions
  #1908 needs, and Consequences says "no core *domain* PR".
- ADR-005's delete-on-publish-failure step is explicitly NOT adopted.
- Mandatory idempotencyKey means a plain, not partial, unique index.
- Add a FiscalRegistrationLocator sub-capability and state that in-doubt
  is non-terminal, per ADR-035; declare an own failure-mode union instead
  of value-importing invoicing's.
- Scope the amendment's "implies no migration" to the invoicing side; put
  the conflict flag on the Order projection AutoIssueTriggerService
  already receives, preserving its F3 one-way edge.
- Legal accuracy: art. 111 ust. 6a + 6b and "potwierdzenie Prezesa GUM"
  (not homologation); carry the spec's not-legal-advice caveat into both
  documents; Czechia 2027 is pending legislation; the tax-code list adds
  np and drops intra-EU 0% as a separate code; fix three code pointers;
  the PL corrections deferral rests on paragony not being device-correctable,
  which makes receipt number + numer unikatowy a #1909 requirement.

Also: Annex -> Amendment run-in, section vocabulary aligned with 12-14,
the duplicated FiscalisationPort description trimmed to a pointer, the two
invoicing policy bullets moved above the adapter roster, manifest posture
for FiscalDeviceOperator, and the spelling choice recorded.

Part of #2009

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

norbert-kulus-blockydevs commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Review addressed — all 4 blocking, all 16 important, all 15 suggestions

Pushed as 9d67f69. Docs only, still no code. prettier --check and check-repo-urls.mjs pass on every touched file; every ](./NNN-*.md) link in ADR-040 was re-resolved against the filesystem, so the repo still has zero broken ADR cross-links.

Thanks in particular for B2 and I4 — the composition bug in decisions 6+7 was real and would have cost a double registration if #1908 had implemented the ADR literally.


Blocking

B1 — eparagony.pl is not a certified fiscal provider. Correct, and the spec said so at :43 while the ADR compressed it away. Fixed in three places:

  • A new Context paragraph, What adapter Epic: Integrations & Setup #1 actually is: eparagony.pl (Platforma Detalistów sp. z o.o.) is a private e-receipt distribution hub, not a fiscaliser and not the state HUB paragonowy; it requires vendor-proprietary software driving a physical online fiscal printer, so adapter Epic: Integrations & Setup #1 sits in front of a device somebody else operates.
  • The capability wording is now "hand a sale to a provider that performs or brokers its fiscal registration" — in ADR-040, in architecture-overview.md § 15, and in the Future-ports bullet.
  • Decision 5 now says plainly that no v1 adapter implements FiscalDeviceOperator, because the print half is outside OL's reach on adapter Epic: Integrations & Setup #1. The day-one claim is downgraded to what it actually supports: the contract shape is settled from day one (efsta /peri/* proves device regimes exist), not the implementation. What the split still buys, unimplemented, is that "registered but not printed" and "not registered" stay distinguishable.

B2 — decisions 6 + 7 burned the idempotency key on a terminal rejected, and misdescribed the precedent. Your replacement clause is adopted close to verbatim, and I re-read invoice.service.ts:284-316 / invoice-record.entity.ts:147-149 to confirm the four branches. Decision 6 now reads: a repeat is resumed under a status-aware fiscal-safety invariant, not returned blindly — issued returns verbatim; a live in-flight lease returns without a second outbound call; in-doubt is surfaced for manual reconciliation; a terminal rejected (the one outcome where the provider definitely created nothing) is re-attemptable under the same key. And the single-flight half is now explicit: exactly-once requires the unique index and an atomic in-flight claim, mirroring resumeExisting / claimForIssue (#1200), with the sentence naming the failure it prevents (two concurrent same-key calls both passing the read gate).

B3 — five dead ADR-039 links and the index gap. Confirmed: PR #2055 is still open and 039 is not on main. I took the second of your two options, so this PR carries no merge-order dependency:

B4 — carrier in a tax rule. Fixed: The tax rate is a code, not a number. You were right that it reads as a shipping statement mid-paragraph, given listCarriers() is live vocabulary here.


Important

# Fix
I1 Migration path now names the three additions #1908 must make — 'Fiscalisation' in CoreCapabilityValues (adapter.types.ts), its spec assertion, and the FE mirror in connections.types.ts — with the reason (the strict @IsIn(CoreCapabilityValues, { each: true }) DTOs would 400 the connection and block #1911). Consequences softened to "no core domain PR (it still needs the one-line capability-value additions listed in Migration path)". Decision 1 now states that Fiscalisation joins the closed list rather than riding the #576 open-world escape.
I2 Decision 6 now says ADR-005 is cited only as the durable-Postgres-dedup precedent, and that its delete-the-row-on-publish-failure step is deliberately not adopted, because deleting on a throw is the blind-resend path decision 7 forbids — the row is the in-doubt evidence.
I3 The key is mandatory, so the index is now specified as plain, not partial, with the contrast spelled out ("the key is never null, unlike invoicing's optional-key column").
I4 Decision 7 rebuilt on ADR-035's two artefacts: in-doubt is a non-terminal state a reconcile sweep keeps advancing (not a discriminator on a terminal row), and confirming it needs a query surface because OL holds no provider id after an indeterminate call — so a FiscalRegistrationLocator sub-capability (locateByQuery(criteria), mirroring RegulatoryRecordLocator) is now scoped to #1908 alongside the base port, and a provider without one gets manual handling. ADR-035 moved from a bare References mention to load-bearing in the decision, as you suggested. It is also added to the Migration path's #1908 list and to § 15.
I5 The no-migration claim is scoped: "on the invoicing side the rule implies no migration", followed by "The product-side field, the conflict state and the per-line net/gross columns below are new persisted state, and are #2054's."
I6 The conflict flag now rides the Order projection AutoIssueTriggerService already receives, and the amendment says so together with the reason — "without injecting an orders token, preserving its documented one-way edge (F3)".
I7 Decision 7 declares fiscalisation's own FiscalRegistrationFailureModeValues = ['rejected', 'in-doubt'], described as invoicing's shape mirrored by design rather than imported, with the reason (the two regimes diverge, per decision 1's premise, instead of one silently inheriting the other's extensions — the #1214 InvoiceFailureCodeValues case you named). No cross-context value import remains.
I8 Both halves fixed. The rejected alternative is now sharper and no longer collides with the sequence: "A registration-only port that cannot express a device dependency at all" — rejected as an end state, with an explicit "This is not a rejection of the #1908-then-#1910 sequence - see Migration path". Decision 5's "required from day one" became "the shape is settled from day one". Migration path answers the shippability question directly: because no v1 adapter implements FiscalDeviceOperator, #1908 alone is the shippable PL v1 for a broker-fronted device, and #1910 is the seam a future device-exposing provider needs.
I9 Rewritten to your wording: "#1906 (the certification-liability prerequisite) is closed, so nothing gates #1908 on it; #1907 gates only the eparagony.pl adapter half, not the neutral capability." (Re-confirmed: closed 2026-07-30.)
I10 Legal paragraph rebuilt: art. 111 ust. 6a carries what a kasa must do (functions, technical requirements, CRK transmission, pamięć fiskalna), ust. 6b places the duty to obtain the potwierdzenie Prezesa Głównego Urzędu Miar on manufacturers / intra-community acquirers / importers placing such registers on the market, and art. 111b ust. 2 applies art. 111 odpowiednio. "Homologation"/"homologated" is gone from both documents — issuance is now attributed to "a registering device whose type carries a potwierdzenie Prezesa GUM, or a software register meeting the same requirements".
I11 The spec's caveat is carried into both: ADR-040's Context ends "This records what the statute says; it is not legal advice, and no seller-facing compliance claim should be made on it without a professional opinion (per #1906's stated out-of-scope)", and § 15's Responsibility bullet carries the same sentence right after the never-issues claim.
I12 Stated at the confidence the source supports: "a bill approved by the Chamber of Deputies on 15 Jul 2026 (Senate and president pending) would return it from 1 Jan 2027 as pure remote reporting with no mandatory printing."
I13 Rewritten to your suggested clause: the rate "is a function of (product tax category, delivery address, seller nexus), and the product tax category is the shop's alone", with a parenthetical conceding the other two legs (OL ingests the delivery address from the marketplace and pushes it into the shop; seller registrations are connection-level config OL already holds) and noting the ADR-014 argument carries the decision on its own.
I14 The enumeration is now 23/8/5/0, zw, np (nie podlega) and oo, with WDT's stawka 0% called out as a use of 0 rather than a further code. Your stronger fact is in: on a PL paragon the rates are letters A-G, "a second, fiscal-side reason the value must be a string" — which is exactly the reason ADR-040 leans on this amendment.
I15 All three pointers corrected: OrderTotals.taxTreatment is order.types.ts:283 (264 is PriceTaxTreatmentValues), the per-line price is incoming-order.types.ts:131, :146, and IncomingOrderTotals.taxTreatment is :190. Verified by reading each.
I16 The corrections deferral now rests on the legal argument instead of absent demand: a paragon cannot be corrected on the device at all — returns and accepted complaints go into the ewidencja zwrotów towarów i uznanych reklamacji (§3 ust. 3, rozporządzenie MF 29.04.2019) and obvious errors into a second register (§3 ust. 4). And the hidden requirement is promoted out of the deferral: "One consequence is a v1 requirement, not a deferral: §3 ust. 4 requires, for an electronic paragon, the receipt number and the numer unikatowy, so #1909 must persist and surface both or the seller cannot keep the one correction register PL actually mandates." Also carried into the Migration path's #1909 line.

Suggestions

All 15 applied.

  1. AnnexAmendment. Now **Amendment (2026-08-13, #2009) - where a line's tax rate comes from.** as a bold run-in paragraph, matching its sibling at 026:103 and the 030/009 precedent. The ### heading is gone, so it no longer renders as a subsection of the amendment area. Both inbound references ("VAT-rate annex") were updated too.
  2. Section vocabulary. **Planned location** / **Planned capability** → bare **Location** / **Capability**, and the header is ### 15. Fiscalisation (planned); "no code yet — built in [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908" moved into the Responsibility bullet.
  3. No double announcement. The Future-ports bullet is now a one-line pointer ("the contract, its sub-capabilities and their rationale are described once, in § Core Bounded Contexts, 15. Fiscalisation"), matching PricingAuthorityPort's posture. The new-precedent point (first numbered context with no code) is flagged in the PR description as you asked.
  4. Bullet placement. Both policy bullets moved above § 14's adapter roster, so KSeF/Infakt/Subiekt stay the tail of the section.
  5. Manifest posture. Decision 5 carries the standard sentence, including the footgun: declared in the manifest for host/FE discovery, resolved only by narrowing the dispatched Fiscalisation adapter with the guard, never via getCapabilityAdapter(connectionId, 'FiscalDeviceOperator'), "which passes the manifest gate and then throws a generic Error inside dispatchCapability".
  6. /cfg no longer dropped. Decision 2 is now "one invariant transaction operation", and names both exclusions: company/base-data registration (efsta's /cfg) is connection setup, journal export is a deferred sub-capability — "neither belongs on the base port".
  7. Journal deferral, better argument. Leads with the legal fact: PL online kasy transmit to the Centralne Repozytorium Kas themselves and hold their own pamięć fiskalna/chroniona (art. 111 ust. 6a), so OL has no journal to export; the country-shaped formats are now the secondary reason and "no user has asked" is gone.
  8. Counts. "A fourth document-ish capability" → "A second document-producing capability"; "fourth" dropped from the amendment ("an engine-facing TaxCalculationPort").
  9. Channel-rate precision. "Today only Erli reports a per-line rate", with WooCommerce's order-level tax amount noted so the distinction is visible.
  10. References hygiene. [IMPL] Fiscalisation provider connection setup + preconditions UI #1911 added to Related issues; ADR-035 now cited in the decision that needs it and re-labelled ("the indeterminate-outcome lifecycle decision 7 mirrors"); ADR-005 re-labelled to carry its non-adoption; "§5 US-1/US-3/US-5" trimmed to §5 US-5, which is what the body actually cites; and product-spec-1902-eparagony-e-receipts.md gained an **ADR:** header line pointing back, matching product-spec-1157.
  11. Spelling — settled, and easy to flip. Recorded explicitly in the Decision preamble: -isation is deliberate and matches [PRODUCT-DESIGN] Polish e-receipt (e-paragon) support — eparagony.pl connector #1902's spec and issue titles, and code identifiers plus the context path follow the ADR so the feature reads consistently. You are right that this leaves it as the tree's sole -isation identifier; the counter-argument is that the whole feature (spec, three issues, ADR filename) already says fiscalisation, and internal consistency beats corpus consistency for a vocabulary nobody outside this feature uses. Say the word and I flip it to Fiscalization / libs/core/src/fiscalization/ — it costs one file rename plus two links while no code exists, and the ADR now names the decision either way so [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908 cannot re-open it accidentally.
  12. Readability. (a) Split into two sentences with the antecedent restored: "…this ADR is not blocked by that answer. The implementation plan for [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908 is blocked by it, and is therefore not part of this change." (b) The conflict-state clause chain is now three sentences (state / blocking / persistence).
  13. Trailer. Fixed and made explicit: "Part of [IMPL] ADR + implementation plan for the fiscalisation capability (pre-code inputs to #1908) #2009. Deliberately not Closes - scope item 2 of that issue (the [IMPL] Fiscalisation capability + eparagony.pl adapter — register a fiscal transaction #1908 implementation plan) is excluded here…". Confirmed no Closes/Fixes/Resolves anywhere in the body.
  14. Addressable-market caveat. Added to Context: most Polish e-commerce is exempt from the register obligation and has no receipt to issue at all (spec §2); a PL fiscal v1 addresses the §4 excluded categories, not the general market. § 15 references it too.
  15. Length. Noted rather than rewritten, but I did compress. The fixes pushed it from 1665 to ~2490 words, all of it in ## Decision and all of it review findings; a compression pass on the Context paragraph and decisions 5/6/7 took ~120 words back out. Still ~3rd longest in the corpus, which I would rather own than drop a finding to hit a target.

Not changed, and why

Nothing was left unaddressed. Two judgement calls worth naming:

Your Verified correct section is much appreciated — I only re-verified the pointers I was changing (I15's three, the resumeExisting branches, CoreCapabilityValues, the partial-index predicate) and took the rest as given.

@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 — ❌ Request changes (numbering, not the decision)

Substantively this is a strong ADR. Keeping fiscalisation out of InvoicingPort is well argued (four independent reasons, each sufficient), the sub-capability shape matches the ADR-002 pattern precisely, the litmus test deliberately mirrors ADR-026's country-agnostic commitment, and every file:line claim I spot-checked against main is accurate. The blocker is the ADR number and a reservation note that is wrong in both directions.

BLOCKING

1. ADR-040 is claimed twice, off the same base commit. This PR adds 040-fiscalisation-capability.md; #2050 adds 040-order-time-fx-stamping-…md. Both are based on 30cb4799, both open. I verified this independently: main's highest ADR is 038.

The nasty part is that the two filenames differ, so git merges both cleanly with no conflict marker and you end up with two ADR-040s. That's the migration-timestamp-collision failure mode exactly, and nothing in pnpm check:invariants catches it.

Per the coordination table in #2055's body, the allocation the cluster has settled on is 039 → #2014 analytics, 040 → #2050 FX, 041 → #2055 routing, 042 → this PR. Renumbering means: the filename, the # ADR-042: title line, the README index row, the docs/architecture-overview.md § 15 pointer and § Future Capability Ports bullet, and the new **ADR:** line in docs/specs/product-spec-1902-eparagony-e-receipts.md:5.

2. The new README reservation note is wrong on both facts it asserts.

039 is reserved, not free: the sales-document routing ADR is authored in #2051 / PR #2055 and claims that number when it lands. Allocate 041 for the next new ADR.

  • 039 is not the routing ADR — it's the order-analytics read model (#1985 / #2014), already referenced by filename on main at docs/plans/implementation-plan-order-cancellation-record-state.md:164 and :745, plus four prose references.
  • #2055 claims 041, not 039 (its own title says so).

As written, the note steers the next author into 041 — straight into #2055. It should reserve 039 (analytics), 040 (FX), 041 (routing) and point at 043 as next-free once this lands at 042. Given this note is the cluster's coordination mechanism, getting it right matters more than the renumber itself.

3. #2009's acceptance criterion ("040 fiscalisation + 039 routing") no longer holds — correct it on the issue in the same pass, or the next reader re-derives the wrong pairing from it.

IMPORTANT

4. Decision 8 and the ADR-026 annex record as settled a rule whose enabling supersession is still a draft that may be refused. The annex is honest that "supersession required, conducted elsewhere" — but that supersession is #2058, a draft, whose own body says "refuting the argument is a legitimate outcome and kills that work." Meanwhile ADR-014 on main explicitly rejects the premise (014-source-authoritative-order-pricing.md:47-48: "Per-line tax-inclusivity / tax rate on OrderItem. Rejected: … tax rate is destination-catalog knowledge that must not leak into the source").

Merging this puts a rule into ADR-026 that a live, unreversed ADR contradicts, and architecture-overview.md § 14 gains a bullet asserting it as current architecture. Either land #2058 first, or soften decision 8 / the annex / the overview bullet to "proposed, pending the ADR-014 reversal in #2054" so the record can't read as decided while its prerequisite is open.

5. Name the neutral shape for the two PL fields decision 9 makes a v1 requirement. Decision 9 says #1909 "must persist and surface both" the receipt number and the numer unikatowy for the PL correction register. Those are the only concrete field-level requirements in the ADR, and they're Polish. The decision-4 litmus test (zero paragon/kasa/printer/eparagony strings in libs/core/src/fiscalisation) doesn't cover them — so as written, #1909 could put numerUnikatowy on the neutral registration record, which is the exact breach this ADR is otherwise careful to prevent. One sentence fixing them as a neutral provider reference plus a regime-extras bag closes it. This is the single place the country-agnostic boundary is actually at risk.

SUGGESTION

  • -isation in a code identifier. Decision 1 puts 'Fiscalisation' into CoreCapabilityValues, whose eight existing members are all American-spelled. The ADR acknowledges this and justifies it by spec/issue-title consistency — defensible, but it makes this the one British-spelled identifier in the registry, permanently, and it's a wire-visible string in enabledCapabilities. Worth an explicit maintainer yes/no now rather than at #1908 review time.
  • Length (~2,300 words vs the README's "under 500") — universally broken since ADR-026 and #2055 amends the rule. Noting only.

Positive observations

  • Every code claim I checked is CONFIRMED, which is unusual for a design-only ADR this size: taxRate: '' at order-to-issue-invoice-command.mapper.ts:228/:249; InvoiceFailureModeValues = ['rejected','in-doubt']; resumeExisting/claimForIssue at invoice.service.ts:284/:336; the 'receipt' documentType value; CoreCapabilityValues closed at adapter.types.ts:23-37 with the strict @IsIn on both DTOs and the FE mirror — which is what makes the Migration path's "without those, the adapter is unreachable" exactly right.
  • The capability design matches the shipped pattern, including naming the trap that getCapabilityAdapter(connectionId, 'FiscalDeviceOperator') passes the manifest gate then throws a generic Error inside dispatchCapability.
  • The near-miss alternative is the best part of the document. Recording that receipt already exists as an open-world documentType and would type-check, then explaining why folding the two is a category error, is precisely the "why didn't we just do X" the ADR practice exists to answer.
  • The VAT annex doesn't breach country-agnosticism: the PL codes appear as justification for keeping the existing neutral taxRate: string a string, not as a proposed core enum.
  • Supersession protocol followed correctly — ADR-026 is amended under its existing amendments header (matching the ADR-009/026 precedent) rather than superseded, and the ADR-014 reversal is delegated rather than restated so the two can't drift.

Merge readiness: ❌ Blocked on the renumber to 042 and the corrected reservation note (1–3) — both mechanical, and both must land before either this or #2050 merges. Finding 4 needs a maintainer decision on ordering against #2058; finding 5 wants one clarifying sentence. The decision itself I'd approve as-is.

@piotrswierzy

Copy link
Copy Markdown
Collaborator

Research: ADR-040 benchmarked against industry practice

I ran a research pass across the field before signing off on the architecture — open-source middleware (fiskaltrust, source-read), commercial vendors (efsta, fiskaly), platform/ERP layers (Odoo source at 18.0, Dynamics 365, SAP DRC, Oracle, Shopify, Lightspeed), and the 2026 regulatory landscape. Summary: the design holds up well. Eight of the nine decisions match what the industry converged on, one exceeds it, and my findings are wording and omissions rather than architecture.

The framing I'd add up front

OpenLinker is not building fiskaltrust — it is building the cash register that calls it.

fiskaltrust holds its neutral contract at the POS boundary (IPOS, three operations) and lets everything below stay irregular: Austria's signing interface has 4 methods, Germany's has 15, because a German TSE needs an ordered StartTransaction/Update/Finish protocol with device lifecycle and export sessions. A lowest-common-denominator sign(bytes) port would have made the German queue impossible.

OL sits on the caller side of that boundary; eparagony.pl is the middleware. That means the hardest problem in this domain — abstracting over heterogeneous national signing hardware — isn't ours, and several decisions become self-evident rather than needing defence. Worth stating in Context; it would let the ADR assert more and argue less.

What the field validates

  • Decision 1 is industry-universal, not a judgement call. Fiscalisation is modelled as adjacent to invoicing — never inside it — by every vendor examined, with no counter-example: Odoo account.edi.document, D365 fiscal transaction, SAP eDocument, fiskaltrust ftQueueItem, each a separate entity with its own lifecycle. The two whose neutral layer spans the most countries decouple hardest — D365 registers open shift, tender removal, price override and open drawer, which no invoicing model could express. You can cite the convergence instead of arguing from first principles.
  • Decision 2's exclusion of company setup matches everyone (efsta /cfg + /register; fiskaly taxpayer/client/TSS). And the strongest shared invariant in the whole field is yours: the fiscal artefact is returned, never supplied.
  • Decision 3 is exactly fiskaltrust's SCU seam — the signing authority as a plugged-in strategy satisfied interchangeably by hardware, a cloud service, or nothing.
  • Decision 5 was independently re-derived by my regulatory pass, which concluded unprompted that a printer capability belongs as an optional sub-capability resolved by narrowing and never as a base-port method — because in Czechia, Hungary and post-2028 Germany there is nothing to narrow to. Shipping the split with no v1 implementer is right.
  • Decision 9's relevance/transport split is SAP's framework-level split (EDOC_ADAPTOR owns "is this in fiscal scope?", EDOC_INTERFACE_CONNECTOR owns transport). Most designs collapse both into the caller. Splitting it across docs(adr): add ADR-041 sales-document routing policy #2055 and this ADR arrives at the same seam independently.

Where you're ahead of the reference implementations

Decision 6. The mandatory idempotency key + unique index written before the outbound call + atomic in-flight lease is stronger than anything I found:

  • fiskaltrust has no idempotency key — receipt-reference uniqueness plus a caller-driven "was this already processed?" query flag, and no published exactly-once guarantee between signature and queue commit.
  • efsta detects rather than prevents (/restart#DUPLICATE).
  • fiskaly pushes it to the caller (client-minted UUIDs + caller-managed revision).

None of them documents the concurrent-same-key race your lease closes. Worth keeping the reasoning in the ADR verbatim.

Three things I'd change

1. Decision 2 contradicts decision 4. D4 says the base contract must not assume a fiscal printer exists; D2 describes the return value as "what must be printed". The assumption D4 forbids is reintroduced in the phrasing of the operation it governs. Delivery channel is a variable, not a constant — fiskaltrust's contract enumerates PDF, ESC/POS, QR, SMS and e-mail as interchangeable outputs of one operation, and Czechia 2027, Hungary's eNyugta and Germany's proposed 2028 Belegbereitstellungspflicht each abolish mandatory printing. Suggest: a customer-artefact list with an adapter-declared rendering hint.

2. No degraded mode, and it's universal in the field. This is distinct from in-doubt: in-doubt is "we don't know if it landed", degraded is "we know it didn't and we're proceeding anyway". efsta returns an empty fiscal tag marked #OFFLINE after a configurable window plus a UserMessage the POS must display; fiskaltrust circuit-breaks into late-signing where lateness is a legally-recognised flag rather than an error; Lightspeed prints "Sicherungseinrichtung ausgefallen"; D365 offers Postpone with a backup connector. OL's async, non-till context probably doesn't need one — but the ADR should say why, because anyone arriving from those systems will read the absence as an oversight.

3. Journal export is the most likely thing to force a port change. The deferral reasoning is right for PL (the online kasa transmits to CRK itself). But a standardised audit export is otherwise universal — DSFinV-K, SAF-T, NF525, LROE, DEP7 — and it is periodic, not per-transaction, so it won't fit the base port's shape. Worth recording as the known first extension point rather than an open deferral.

Fact-check

  • Italy — overstated. "Mid-migration from RT device to certified software" isn't quite right. Software solutions are genuinely in force (art. 24 D.Lgs. 1/2024, AdE provvedimento 7 Mar 2025, PEM/PEL split), but hardware RT is not being phased out, no sunset date exists in any instrument, ~91% of merchants remain hardware-only, and AdE's own framing is "alongside". The argument survives — it just needs "added alongside".
  • Czechia — confirmed, and time-sensitive. Tisk 189 passed 3rd reading 15 Jul 2026 exactly as you state. It's in the Senate on 19–20 August — six days away, effective 1 Jan 2027, and MF states explicitly it mandates no receipt printing and no new cash equipment. Strongest single piece of evidence for decision 3; worth date-stamping so a future reader knows when the claim was true.
  • Poland — every claim holds. HUB paragonowy live since 15 Sep 2023 but never mandatory and still isn't; the two regulations effective 1 Jul 2025 put e-paragon on equal legal footing with print; the 20,000 zł threshold runs through 2027 — so the scoping claim that a PL v1 serves the excluded categories rather than the general market is correct.

One forward-planning note

From 1 January 2027 Poland's low-value NIP receipt (a faktura uproszczona) is pulled into KSeF — it sits outside only until 31 Dec 2026. So in the one market this work targets, roughly five months out, the same order may need both a fiscal registration and a KSeF clearance.

That isn't an argument for merging the contexts — it's an argument for exactly what you have: separate ports, with one adapter free to implement both. Worth a line in Consequences, since it's the first concrete case where the two regimes touch the same document.


None of this changes the architecture, and the numbering/reservation items from my review above are still the only things blocking. I have a fuller write-up with sources if useful.

…scalisation ADR

Addresses the tech-lead review + industry-research pass on PR #2056. Numbering
is handled in a separate commit.

Contract corrections:
- Decision 2 no longer defines the base operation as returning "what must be
  printed", which is the assumption decision 4 forbids 18 lines later. The
  result now carries a possibly-empty list of customer artefacts, each with an
  adapter-declared medium and disposition hint; an empty list is a successful
  registration, not a failure.
- Decision 9 names the neutral shape for the two PL fields it makes a v1
  requirement, so #1909 cannot put `numerUnikatowy` on the neutral record: a
  small neutral identity set plus one adapter-owned extras bag core never
  indexes. Decision 4's litmus test is extended to cover field names and core
  reads, not just prose.

New decisions:
- Decision 10 records why no degraded/offline mode ships, contrasting it with
  `in-doubt` - OL is never in the buyer-blocking path and can never mint the
  legally-recognised substitute, since it is categorically not the trust anchor.
- Decision 11 promotes journal/audit export from an open deferral to the named
  first extension point, with the reasons a periodic export cannot sit on a
  per-transaction base port.

Tax-rate rule hedged:
- Decision 8 splits its settled negative half (fiscalisation never recomputes a
  rate) from the positive ProductMaster rule, which is marked proposed pending
  the ADR-014 reversal in #2058 - a draft that may be refused. The ADR-026 annex
  and the architecture-overview bullets carry the same caveat, so the record
  cannot read as decided while a live ADR contradicts it.

Framing and facts:
- Context states the boundary explicitly (OL is the cash register calling the
  middleware, not the middleware), which lets decisions 3 and 5 and two
  Alternatives bullets shorten.
- Italy corrected: certified software was added alongside hardware RT, with no
  sunset in any instrument - which strengthens the anchor argument rather than
  weakening it.
- Czechia date-stamped, with the Ministry of Finance statement that it mandates
  neither printing nor new cash equipment.
- Consequences notes that from 1 Jan 2027 a PL low-value NIP receipt enters
  KSeF, so one order can need both a fiscal registration and a clearance.

Naming: code identifiers, the capability value and the context path use the
`-ization` spelling, matching the repository convention and the two
implementation plans. The value is wire-visible and strict-`@IsIn`-gated, so
choosing now is free and revisiting later is a breaking change; prose, spec and
issue titles keep `-isation`.

Refs #2009

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

Three open PRs were authored off the same base commit and 040 was claimed
twice: this PR and #2050 (order-time FX stamping) both added an
`040-*.md`. Because the filenames differ, git merges both cleanly with no
conflict marker and `pnpm check:invariants` does not catch it, so the
collision only surfaces as two ADR-040s on `main`.

Verified independently against `origin/main` and the open PRs: the highest
ADR on `main` is 038, and 039 is the order analytics read model (#2014,
already referenced by filename from
`docs/plans/implementation-plan-order-cancellation-record-state.md`), 040 is
FX (#2050), 041 is sales-document routing (#2055). This PR therefore takes
042 - filename, title line, README index row, both
`docs/architecture-overview.md` pointers, the ADR-026 annex cross-link and
the spec's `**ADR:**` line.

The README reservation note was wrong on both facts it asserted: it named
039 as the routing ADR (it is analytics) and told the next author to
allocate 041 (which #2055 already claims), steering them straight into a
second collision. It now lists all three reserved numbers with their
claiming PRs, points at 043 as next-free, and names the failure mode so the
next author checks the open-PR list rather than the file listing.

Refs #2009

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Two conflicts, both from ADRs that landed on `main` while this branch was in
review:

- `docs/architecture/adrs/README.md` - ADR-041 (sales-document routing, #2055)
  took its index row and rewrote the reservation note. Both rows are kept and
  the two notes are reconciled: 041 is no longer reserved (it merged), 039
  (#2014 analytics) and 040 (#2050 FX) still are, 042 is this PR, and 043 is
  next-free.
- `docs/architecture-overview.md` - `main` added `### 15. Analytics Trust`
  while this branch added `### 15. Fiscalisation (planned)`. Analytics Trust
  keeps 15 (it is merged and describes shipped code); fiscalisation moves to
  **16**, with its two inbound pointers (§ 14 Invoicing "see § 15 below", the
  `FiscalizationPort` bullet under Future Capability Ports) renumbered. The
  invoicing section keeps all three bullets - `main`'s routing-policy bullet
  plus this branch's "not fiscalisation" and tax-rate ones.

Now that the routing ADR has merged, the placeholder `#2051` text references
become real `ADR-041` links, as the PR description said they would.

Refs #2009

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

Copy link
Copy Markdown
Collaborator Author

Review addressed - all findings from both passes

Thanks for both passes; the research one in particular changed the document more than the review did. Everything is in three commits, all pushed after your comments:

  • 4099d9a - substantive findings (contract corrections, two new decisions, the tax-rate hedge, framing + facts)
  • 0c6ee01 - numbering only (renumber to 042 + corrected reservation note)
  • c96f585 - merge of main, which had moved under the branch

Tech-lead review

1. ADR-040 claimed twice - renumbered to 042. Verified the same way you did: highest on main was 038, and the open PRs claim 039 (#2014 analytics), 040 (#2050 FX), 041 (#2055 routing). Applied to the filename, the # ADR-042: title, the README row, both docs/architecture-overview.md pointers, the ADR-026 annex cross-link and the **ADR:** line in docs/specs/product-spec-1902-eparagony-e-receipts.md.

2. Reservation note - rewritten, and you were right on both facts. It named 039 as routing (it is analytics) and pointed the next author at 041 (already #2055's), i.e. straight into a second collision. It now lists each reserved number with its claiming PR, points at 043 as next-free, and names the failure mode explicitly - two PRs claiming one number merge cleanly when their filenames differ, so the check is the open-PR list, not the file listing. Post-merge it reads: 039 and 040 still reserved, 041 landed, 042 is this PR, 043 next.

3. #2009's acceptance criterion - corrected on the issue, with the 13 Aug "040 fiscalisation + 039 routing" pairing explicitly marked dead rather than silently overwritten, so the next reader cannot re-derive it.

4. Decision 8 + the ADR-026 annex - hedged, not landed. Split into its two halves. The negative half (fiscalisation never recomputes a rate) is settled regardless of #2058's outcome and stays unqualified. The positive ProductMaster rule is now marked proposed, pending the ADR-014 reversal in #2058, in all three places: decision 8, the ADR-026 annex, and the architecture-overview.md bullet - which now also states that it is neither current behaviour nor a settled decision, and names the mapper emitting an empty taxRate today. So nothing in the record asserts a rule a live ADR contradicts. De-hedging belongs to #2058, not here.

5. The two PL fields - neutral shape now named. Decision 9 fixes them as a small neutral identity set (provider-assigned reference, the document's own reference, a flat signing identity, a timestamp) plus one adapter-owned jsonb extras bag that core never indexes; a key appearing in a second adapter gets promoted to a neutral field. Decision 4's litmus test was extended to cover field names and core reads, not just prose, which is what closes the numerUnikatowy-on-the-neutral-record hole you spotted.

Suggestion: -isation in a code identifier - taken, flipped. Code identifiers, the capability value and the context path are now -ization (FiscalizationPort, 'Fiscalization', libs/core/src/fiscalization/), matching the other eight registry members. Prose, the spec and the issue titles keep -isation. Deciding now is free; the value is wire-visible and strict-@IsIn-gated, so revisiting it after #1908 would be a breaking change.

Research pass

Framing. Context now states the boundary outright - OL is on the caller side of the POS seam; it is not building the middleware, it is calling one. Decisions 3 and 5 and two Alternatives bullets got shorter as a result, exactly as you predicted.

1. Decision 2 vs decision 4 - real contradiction, fixed. The base operation no longer returns "what must be printed". The result carries a possibly-empty list of customer artefacts, each pairing content with an adapter-declared medium (document / markup / code / link / text) and a disposition hint (print / display / send / retain). An empty list is a successful registration, not a failure - which is what a pure reporting regime returns.

2. Degraded mode - now recorded as a decision, not an omission. New decision 10 contrasts it with in-doubt: in-doubt is a state OL can honestly hold, a degraded artefact is one only the trust anchor may mint, and OL is categorically not the anchor here nor in the buyer-blocking path. Anyone arriving from efsta/fiskaltrust/D365 now reads a reason instead of a gap.

3. Journal export - promoted from open deferral to named first extension point. New decision 11: FiscalJournalExporter, with the reason it cannot sit on the base port (keyed to a period rather than a sale, and freely repeatable).

Fact-check. Italy corrected to "certified software added alongside hardware RT, no sunset in any instrument" - which strengthens the trust-anchor argument rather than weakening it. Czechia date-stamped, with the MF statement that it mandates neither printing nor new cash equipment. 2027 note added to Consequences: from 1 Jan 2027 the PL low-value NIP receipt enters KSeF, so the same order can need both a fiscal registration and a clearance - the first concrete case where the two regimes touch one document, and an argument for exactly this shape rather than against it.

Conflicts resolved (main moved under the branch)

  • README.md - ADR-041 landed and rewrote the reservation note. Both rows kept, the two notes reconciled.
  • architecture-overview.md - main added ### 15. Analytics Trust while this branch added ### 15. Fiscalisation. Analytics Trust keeps 15 (merged, describes shipped code); fiscalisation moved to 16, with both inbound pointers renumbered. The invoicing section keeps all three bullets.
  • Since the routing ADR merged, the placeholder #2051 text references are now real ADR-041 links, as the PR description said they would be.

Still open, deliberately

Ready for re-review.

norbert-kulus-blockydevs added a commit that referenced this pull request Aug 14, 2026
Rewrites the proposed amendment after review of PR #2058.

- Argument 4 corrected: PrestashopTaxRateResolver resolves per (product,
  delivery country) and deliberately DE-prefers state rows; it is injected
  only into the OrderProcessorManager adapter, never the ProductMaster, and
  ProductMasterPort exposes no rate. So the master read is a new port method
  with new code + unknown semantics, not a rewiring - and WooCommerce's
  ProductMaster reads no tax at all.
- Defect restated as reachability rather than absence, and the three cheaper
  routes it admits are now weighed with a rejection rationale each
  (issuance-time read in the invoicing context, per-connection config, a
  mappings rule table), plus the withdrawn TaxCalculationPort.
- Two arguments added that actually select the order contract: chronology
  (ADR-014 predates ADR-026, so it could not have reasoned about the
  invoice-only path) and point-in-time fidelity of a snapshotted rate.
- Decision 3 secured by a binding clause (the order-creation path MUST keep
  resolving destination-side and MUST ignore an OrderItem-carried rate), and
  the rule scoped to the invoice-only path so the issuance gate cannot fire
  on an order whose destination already holds the rate.
- Inline forward pointer added to Decision 2, the sentence a cold reader
  quotes; the append-only self-description corrected to match the diff.
- The restated rule is deleted in favour of the pointer to the ADR-026 annex
  (#2009 / PR #2056), removing the zero-code and rate-domain drift between
  the two documents; merge order phrased conditionally.
- Precedent survey corrected (five amendment sections across four ADRs;
  ADR-037 carries an in-place `Corrected by`, ADR-031 an inline
  `Correction`), so the standalone-superseding-ADR hedge is dropped.
- Limitations recorded: EN 16931's category/rate/exemption-reason triple,
  the Art 14a deemed-supplier case where the channel is authoritative, and
  the open question of which vocabulary a core taxRate carries.
- Downstream site list completed (7 code sites + the implementation plan)
  and a Consequences addendum added for the historical-backlog gate.
- OrderTotals.tax aside trimmed to a pointer at the recorded deferral.

Part of #2054 (step 1 of 7).

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

Follow-up to the review on PR #2056, suggestion 11. The ADR previously kept
`-isation` in prose while using `-ization` for code identifiers, which left the
two halves of one vocabulary disagreeing. Settle on `-ization` everywhere:

- rename 042-fiscalisation-capability.md -> 042-fiscalization-capability.md
  and update every inbound link (ADR index, architecture-overview, ADR-026,
  the #1902 spec header)
- flip prose in ADR-042, ADR-026's tax-rate amendment, architecture-overview
  sections 14 and 16, and the #1902 spec
- flip the four prose mentions in ADR-041, so the sibling routing ADR names
  the capability the way the capability is named. No decision text changed

Matches the repository house style by a wide margin (normalize 464:40,
authorize 244:3) and removes what would have been the tree's only -isation
identifier. The GitHub issue titles (#1902, #1908, #1910, #1911) and this
branch name keep the old spelling - renaming them would break inbound links
for no gain, and the ADR now records that explicitly.

The capability value is the reason to settle this before code: it is
wire-visible in connections.enabledCapabilities behind a strict @isin DTO,
so changing it once a connection exists is a breaking change.

Part of #2009

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

Copy link
Copy Markdown
Collaborator Author

Both remaining items done, pushed as 654fd8d.

Suggestion 11 (spelling) — flipped to -ization everywhere, not just in code. 042-fiscalisation-capability.md042-fiscalization-capability.md, every inbound link updated (index, architecture-overview.md, ADR-026's amendment, the #1902 spec header), and the Decision preamble now records why the GitHub issue titles keep the old spelling (renaming them breaks inbound links for no gain).

B3 (stacking on #2055) — moot: main merged in since the last pass, #2055 landed as ADR-041, and this branch got renumbered along the way to ADR-042 (038/039/040 were all claimed by other in-flight work in the meantime). Every #2051 placeholder is now a real [ADR-041](./041-sales-document-routing-policy.md) link. Re-verified: prettier --check clean, check-repo-urls.mjs clean, and every ](./NNN-*.md) link across the touched files resolves on disk.

One thing that changed underneath this PR and is worth a second look: ADR-026's supersession note for ADR-014 now points at #2058 (opened separately, still open) rather than the general "#2054" it said before — that's a main-side edit from between review passes, not something I introduced, but flagging it since it touches the same paragraph B2/I2 were about.

PR description updated to match current state (numbering + spelling) rather than describing what was true two revisions ago.

@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 re-review — ✅ Approve

Three commits since the last pass. All five prior findings and all four research-comment items are addressed — most properly rather than cosmetically — and the two new decisions that came out of the research pass (10 degraded mode, 11 journal export) are the strongest additions in the diff.

Prior findings

✅ 1 — ADR number. 042 confirmed free. main tops out at 041-sales-document-routing-policy.md; open-PR claims re-verified branch by branch: #2014 → 039, #2050 → 040, #2058 → amends 014 only, #2066 → 043/044/045. Nothing else claims 042.

✅ 2 — reservation note. Corrected in both directions, and the added failure-mode sentence — "two PRs claiming one number merge cleanly when their filenames differ, and pnpm check:invariants does not catch it" — is the right lesson to leave behind. One residual, below.

✅ 3 — issue #2009. Fixed well: scope item 1 now reads "number settled 14 Aug 2026: 042. Every earlier guess in this issue is dead, including the 040 recorded on 13 Aug", with a criterion that explicitly kills the old pairing rather than silently overwriting it. Correcting a stale AC by naming what it replaces is the right habit.

✅ 4 — decision 8 / ADR-026 annex. Honestly stated in all three places now. Decision 8 splits the halves: the negative ("fiscalization never computes a rate of its own") is unqualified; the positive ProductMaster rule is "proposed, not settled … proposed in #2058, which is open and may be refused", with a fallback naming what re-opens if it is. The annex carries "Status: PROPOSED, pending the ADR-014 reversal in #2058" plus "neither document is normative until #2058 lands", and architecture-overview.md adds "This is neither current behaviour nor yet a settled decision".

Since ADR-014 is only Proposed (I had that wrong last round), this is now over-cautious rather than under. Acceptable — and the hedge is #2058's to remove, not yours.

✅ 5 — PL identifiers vs the litmus test. Closed on both sides, which is more than I asked for. Decision 9 fixes a neutral identity set (providerReference, documentReference, flat signingIdentity, registeredAt) plus one jsonb regimeExtras, with PL/IT/DE examples per field and a promotion rule ("a key that shows up in a second adapter is promoted to a neutral field"). Decision 4's litmus widened from prose to field names and core reads: a regime value "may legitimately appear as a key inside adapter-written extras data but never as a column, a TypeScript property, or a key any code under libs/core/src/fiscalization indexes". That is precisely the hole.

✅ spelling. Flipped to -ization in identifier position throughout (FiscalizationPort, 'Fiscalization', libs/core/src/fiscalization/), prose keeps -isation, and the rationale for deciding now (wire-visible, strict-@IsIn-gated, breaking to revisit) is recorded so #1908 can't reopen it.

Research-comment items

✅ (a) D2/D4 contradiction resolved. "What must be printed" is gone. Decision 2 returns "a possibly-empty list of customer artefacts, each pairing content with an adapter-declared medium and disposition hint — a hint, never an instruction, because core neither renders nor delivers", and an empty list is a successful registration. That last clause is the part that actually makes Czechia 2027 and the 2028 Belegbereitstellungspflicht representable.

✅ (b) Degraded mode — argued, not asserted. New decision 10 opens by naming the four reference systems so the absence reads as considered, distinguishes it from in-doubt in one line ("we do not know whether it landed" vs "we know it did not land and we are proceeding anyway under a legally-recognised substitute"), gives three reasons, and closes with a "What would force one" trigger list separating the two lifecycle-changing triggers from the additive #OFFLINE-flag case. The ADR-035 contrast — degraded mode is legitimate there because OL issues — is the sharpest paragraph in the document.

✅ (c) Journal export promoted to decision 11 as the named first extension point, with a five-axis argument for why it can't sit on the base port (period-keyed not sale-keyed; freely repeatable, so the (connectionId, idempotencyKey) index + lease would mint non-registrations; opaque country-schema'd blob; scheduled/operator-initiated; and a base-port method would force no-ops on export-free regimes). The second axis is the one I hadn't thought through.

✅ Fact-check. Italy now reads "added certified software alongside hardware RT … with no sunset for hardware in any instrument and roughly 91% of merchants still hardware-only, so it now runs two classes at once" — which is stronger evidence for decision 3 than "migrating" was. Czechia is date-stamped twice, including an explicit "(Legislative status as at 2026-08-13; a reader after that date should re-check it.)". The 2027 KSeF overlap landed in Consequences, and the POS-boundary framing is now in Context.

New (both minor, non-blocking)

  • The reservation note reproduces the failure it warns about. It ends "Allocate 043 for the next new ADR" — but #2066 already claims 043, 044 and 045. Self-limiting, since the same sentence tells the reader to check the open-PR list rather than the file listing, and #2066 postdates this branch. Either drop the "allocate 043" clause or extend the list with "043–045 by #2066". Whoever merges #2066 should reconcile it.
  • Decision 4's example contradicts decision 9's assignment. D4 uses numerUnikatowy as its example of a value that may live as a key inside extras, while D9 assigns the numer unikatowy to the neutral signingIdentity field (using numer ewidencyjny as its extras example). Consistent in rule, confusing in illustration — swapping D4's example to numerEwidencyjny makes them agree.

Link hygiene re-checked: the five previously-dead ADR-039 links now point at a real ./041-sales-document-routing-policy.md on main, the product-spec path resolves, and the spec gained a reciprocal **ADR:** header. No broken cross-link introduced.

CI

Test, Build, Type Check, Integration Tests, Docker smoke, PHP unit all green. Lint red is inheritedmain carries two migrations at 1833000000002, failing check:invariants on the trunk itself. This PR touches five markdown files and nothing under apps/.

Merge readiness: ✅ Approve — all three blockers resolved, both IMPORTANTs addressed at the level asked, and the research pass produced two genuinely new decisions rather than hedging text. Merge order against #2058 is free: the hedge means this can't contradict ADR-014 whichever way #2058 goes. Keep #2009 open — scope item 2 is deliberately excluded and blocked on #1907.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@piotrswierzy
piotrswierzy merged commit 1a4c7a9 into main Aug 14, 2026
8 checks passed
@piotrswierzy
piotrswierzy deleted the 2009-adr-fiscalisation-capability branch August 14, 2026 11:35
piotrswierzy pushed a commit that referenced this pull request Aug 14, 2026
Two small corrections to the ADR index note, both consequences of #2056
landing as ADR-042:

- 042 is no longer reserved. The note still listed it as claimed-but-pending
  while the row for it sits in the table directly above, so a reader checking
  the note against the table saw a contradiction. 040 was in the same state
  after #2050. Both move to the claimed-and-carried list, leaving 039 (#2014)
  as the only genuine gap, and the note now states the next free number
  outright ("Allocate 046") the way it did before.
- "fiscalisation" -> "fiscalization". #2056 normalised this vocabulary to the
  repository's -ization house style, and this line was the sole remaining
  occurrence of the old spelling anywhere in the tree.

Docs only. No production code, no schema, no runtime behaviour change.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
piotrswierzy pushed a commit that referenced this pull request Aug 14, 2026
…2058)

* docs(adr): propose reversing ADR-014's per-line tax-rate rejection

ADR-014 rejected a per-line tax rate on `OrderItem` on the grounds that the
rate is destination-catalog knowledge. That reasoning holds for the path the
ADR was written about (creating an order into a destination shop, where the
shop is the tax authority) but not for the invoice-only path, which has no
destination shop at all: the rate ends up held nowhere, and the inFakt, Subiekt
and KSeF adapters each substitute their own guess. ADR-014 set out to prevent
fiscally-wrong VAT on invoices and, on that path, is what produces it.

Appends a `## Proposed amendment (#2054)` section carrying the argument, plus a
forward pointer on the rejected bullet. The append-only rule is respected: no
existing text is rewritten and the Status line is unchanged, since the taxonomy
has no per-alternative value and only one entry under "Alternatives considered"
is in question. The section is explicitly framed as a proposal needing a team
accept, not as settled record.

Scope is exactly the tax-rate half of that one alternative. Decision 1
(source-authoritative price) is the foundation the new rule builds on, the
tax-inclusivity half of the rejection stands, and Decision 3 (PrestaShop stays
the tax and rounding authority on the order-creation path) is untouched.

Docs only. No production code, no schema, no test changes. Every cited
file:line was verified against 30cb479; two claims from the issue were
corrected in the process (`OrderTotals.tax` is required, not optional, and the
core mapper's empty-rate comments do not cite ADR-026).

The normative rate rule stays with the ADR-026 annex in #2009 / PR #2056, which
this does not touch, so the two documents cannot drift.

Part of #2054

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

* docs(adr): address review on the ADR-014 per-line tax-rate amendment

Rewrites the proposed amendment after review of PR #2058.

- Argument 4 corrected: PrestashopTaxRateResolver resolves per (product,
  delivery country) and deliberately DE-prefers state rows; it is injected
  only into the OrderProcessorManager adapter, never the ProductMaster, and
  ProductMasterPort exposes no rate. So the master read is a new port method
  with new code + unknown semantics, not a rewiring - and WooCommerce's
  ProductMaster reads no tax at all.
- Defect restated as reachability rather than absence, and the three cheaper
  routes it admits are now weighed with a rejection rationale each
  (issuance-time read in the invoicing context, per-connection config, a
  mappings rule table), plus the withdrawn TaxCalculationPort.
- Two arguments added that actually select the order contract: chronology
  (ADR-014 predates ADR-026, so it could not have reasoned about the
  invoice-only path) and point-in-time fidelity of a snapshotted rate.
- Decision 3 secured by a binding clause (the order-creation path MUST keep
  resolving destination-side and MUST ignore an OrderItem-carried rate), and
  the rule scoped to the invoice-only path so the issuance gate cannot fire
  on an order whose destination already holds the rate.
- Inline forward pointer added to Decision 2, the sentence a cold reader
  quotes; the append-only self-description corrected to match the diff.
- The restated rule is deleted in favour of the pointer to the ADR-026 annex
  (#2009 / PR #2056), removing the zero-code and rate-domain drift between
  the two documents; merge order phrased conditionally.
- Precedent survey corrected (five amendment sections across four ADRs;
  ADR-037 carries an in-place `Corrected by`, ADR-031 an inline
  `Correction`), so the standalone-superseding-ADR hedge is dropped.
- Limitations recorded: EN 16931's category/rate/exemption-reason triple,
  the Art 14a deemed-supplier case where the channel is authoritative, and
  the open question of which vocabulary a core taxRate carries.
- Downstream site list completed (7 code sites + the implementation plan)
  and a Consequences addendum added for the historical-backlog gate.
- OrderTotals.tax aside trimmed to a pointer at the recorded deferral.

Part of #2054 (step 1 of 7).

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

* docs(adr): bind the neutral-vocabulary constraint and cite #2057

Addresses the tech-lead review on PR #2058.

- State the vocabulary constraint for a core OrderItem tax rate instead of
  flagging it open: a neutral string code that maps losslessly onto UNCL 5305
  inside the adapter, with FA(3) keys explicitly inadmissible on the orders
  contract. Accepting the reversal without it would license importing a
  jurisdiction's model one layer further out than ADR-026 permits.
- Cite #2057 as a prerequisite of any master rate read (unknown vs
  resolved-zero) in the evidence bullet and in References.
- Record the settled answer on where the section lives: it stays here, and the
  accepting PR must both drop the proposal preamble and resolve ADR-014's own
  Proposed-while-shipped status.
- Correct the amendment-precedent count to six across four ADRs (ADR-009 x3).

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

* docs(adr): #2057 merged, so the prerequisite it named is satisfied

#2057 merged 2026-08-14 and reached this branch with the main merge in
a474145, so three present-tense claims about PrestashopTaxRateResolver
went stale between writing them and now. The resolver no longer returns a
number fraction conflating untaxed with unresolvable - it reports
{ kind: 'resolved' | 'unknown', reason }. The amendment now says the
prerequisite is met rather than pending, and anchors the old behaviour to
dec889a where it was true.

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

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Opus 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.

2 participants