# DEKRA 1.0 — structure and rules

Normative companion to [STANDARD.md](STANDARD.md). Everything here is enforced by
`App\Services\Dekra\DekraValidatorService`; every rule names the `code` the validator reports, so a
finding in the console can be traced straight back to a line in this document.

The machine-readable form of this document is [`dekra-1.0.schema.json`](dekra-1.0.schema.json),
generated from the same constants the validator enforces. It carries shape, presence and enumerated
values. It cannot carry the arithmetic cross-checks or the signatures — a file that satisfies the
schema is **well-formed**, and only the validator can say it is **valid**.

Conventions used below:

- **required** — the key must be present. A key whose value may be `null` is still required to be
  *present*: absent keys are an error, `null` values are not. This is what keeps every document the
  same shape regardless of type, and what lets `null` mean "the document does not say" instead of
  being indistinguishable from "nobody wrote this field".
- **decimal** — an exact decimal **string** (`"12.10"`), never a JSON number. See §10.
- **money** — a decimal string with exactly as many places as the currency's ISO 4217 minor unit.
- **percent** — a decimal string, 2 places, `0.00`…`100.00`.
- Levels: **E** = error (file invalid), **W** = warning (file valid, data suspicious).

Closed value lists live in [`registry/`](registry/) and are named per field below.

---

## 1. Container (root object)

| Key | Type | Req. | Rule | Codes |
|---|---|:--:|---|---|
| `format` | string | ✔ | Exactly `DEKRA` | E `key.missing`, E `key.type`, E `key.empty`, E `format.unknown` |
| `version` | string | ✔ | `^\d+\.\d+$`; a supported version (currently `1.0`) | E `version.malformed`, E `version.unsupported` |
| `profile` | string | ✔ | Conformance profile, §9. From `registry/profiles.json` | E `profile.unknown` |
| `signature_profile` | string | ✔ | `local` or `issuer`, §8 | E `signature_profile.unknown` |
| `signature_algorithm` | string | ✔ | Must be the algorithm the declared profile uses | E `signature_algorithm.mismatch` |
| `key_id` | string\|null | ✔ | Which key signed. Absent under `issuer` → W `key_id.missing` | E `key.type` |
| `issuer_key_url` | string\|null | ✔ | Where the public key is published (§8) | E `key.type` |
| `generated_at` | string | ✔ | Parseable timestamp; SHOULD be ISO-8601 with offset | E `generated_at.format`, W `generated_at.loose` |
| `document_count` | integer | ✔ | Must equal `documents.length` | E `document_count.missing`, E `document_count.mismatch` |
| `documents` | array | ✔ | 1…1000 objects (§11 for larger batches) | E `documents.missing`, E `documents.empty`, E `documents.too_many`, E `document.not_object` |
| `extensions` | object\|null | ✔ | §12 | E `extensions.type`, E `extensions.namespace` |
| `signature` | string | ✔ | 64 or 128 lowercase hex; must verify (STANDARD §4) | E `signature.missing`, E `signature.malformed`, E `signature.collection_invalid` |

Any other top-level key: W `key.unknown`. Readers must ignore unknown keys, never fail on them.

Whole-file failures: E `file.unreadable`, E `json.invalid`, E `json.not_object`.

---

## 2. Document (`documents[i]`)

| Key | Type | Req. | Rule | Codes |
|---|---|:--:|---|---|
| `document_type` | string | ✔ | From `registry/document-types.json` | E `document_type.missing`, E `document_type.unknown` |
| `document_subtype` | string\|null | ✔ | Sender-defined refinement, free text | E `key.missing_nullable`, E `key.type` |
| `document_number` | string\|null | ✔ | May be `null` — a shop receipt often has no usable number | E `key.type`, W `document_number.absent` |
| `issuer` | object | ✔ | Party block, §3. **Who wrote the document** | see §3 |
| `recipient` | object | ✔ | Party block, §3. **Who it was written to** | see §3 |
| `references` | array | ✔ | 0…n references to other documents, §4 | see §4 |
| `items` | array | ✔ | 0…n item objects, §5. May be empty | E `items.missing`, W `items.empty` |
| `allowances_charges` | array | ✔ | 0…n document-level allowances and charges, §6 | see §6 |
| `totals` | object | ✔ | §7 | see §7 |
| `dates` | object | ✔ | §7.3 | see §7.3 |
| `payment` | object | ✔ | §7.4 | see §7.4 |
| `fiscal` | object\|null | ✔ | What an authority stamped on it, §7.5 | see §7.5 |
| `source_document` | object\|null | ✔ | Fingerprint of the file it was read from, §7.6 | see §7.6 |
| `comment` | string\|null | ✔ | A note about the document as a whole | E `key.type` |
| `extensions` | object\|null | ✔ | §12 | see §12 |
| `signature` | string | ✔ | 64 or 128 lowercase hex; must verify | E `signature.*` |

Any other key inside a document: W `key.unknown`.

### 2.1 Document types

`invoice`, `credit_note`, `debit_note`, `corrective_invoice`, `self_billed_invoice`, `proforma`,
`receipt`, `contract`, `waybill`, `other`.

**A credit note is a type, not a subtype.** As free text no consumer could tell a refund from a
charge, so every sum over an archive was too high by the value of its returns.

**Amounts are always positive.** Direction is carried by the type: a consumer summing an archive
negates **`credit_note`** (`DekraRegistry::CREDIT_TYPES`) and nothing else. A sign convention is
the fastest way to get two implementations disagreeing about a refund. The rule binds the producer;
the validator does not check the sign of an amount outside `payment` (§7.4), so a negative figure
elsewhere is a wrong file that still validates.

### 2.2 What a document never describes

**No field describes the archive holding the document, or the software that wrote the file.** One
invoice is *incoming* to the buyer and *outgoing* to the seller, and it arrives on a different day
in each of their archives: a field carrying either would make the same signed bytes assert two
different things depending on who opened the file. Which side of a transaction you are on is
answered by comparing `issuer` and `recipient` with your own identity — a question only the holder
can answer. Likewise a producer's own record number: it means something in exactly one database and
carries out how many records that database holds.

---

## 3. Party (`issuer`, `recipient`)

The **same** key set for both sides and both directions — this is the rule that lets a reader treat
invoices and receipts identically.

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `name` | string\|null | ✔ | Legal name. Empty → W `party.name_missing` |
| `entity_type` | string\|null | ✔ | Legal form as printed (`SIA`, `GmbH`, `Ltd`, `LLC`, …). Free text: the world's legal forms are not a list anyone can close |
| `country` | string\|null | ✔ | **ISO 3166-1 alpha-2.** Absent → W `party.country_missing`; malformed → E `party.country_invalid` |
| `identifiers` | array | ✔ | §3.1 |
| `is_vat_payer` | bool\|null | ✔ | `null` = unknown, not "no" |
| `address` | string\|null | ✔ | Address as printed, one line |
| `address_parts` | object\|null | ✔ | `{ street, city, postal_code, subdivision }`, each string\|null, all four required to be present |
| `postal_address` | string\|null | ✔ | Only when it differs from `address` |
| `email` | string\|null | ✔ | Malformed → W `party.email_invalid` |
| `phone` | string\|null | ✔ | As stored, not normalised |
| `endpoint_id` | string\|null | ✔ | Electronic address for e-invoicing (PEPPOL `EndpointID`). Only when the document states one |
| `endpoint_scheme` | string\|null | ✔ | Four-digit PEPPOL EAS code. Missing while `endpoint_id` is set → W `party.endpoint_scheme_missing`; not four digits → W `party.endpoint_scheme_invalid` |
| `bank` | object | ✔ | `{ name, account, swift }`, each string\|null, all three required to be present |

Codes: E `party.missing`, E `party.key_missing`, E `party.type`, E `party.type_bool`,
E `party.country_invalid`, E `party.bank_missing`, E `party.bank_key_missing`, E `party.bank_type`,
E `address_parts.type`, E `address_parts.key_missing`, E `address_parts.type_field`,
E `identifiers.missing`, W `party.name_missing`, W `party.country_missing`,
W `party.email_invalid`, W `party.endpoint_scheme_missing`, W `party.endpoint_scheme_invalid`.

**`country` is never inferred.** Not from the address text, not from the document's language, not
from the producer's own location. Without it a consumer cannot decide which tax rules the document
falls under, and a *guessed* country decides them wrongly while looking exactly as confident as a
read one. `subdivision` is not decoration either: in the United States, Canada, Brazil and India
the rate is decided by the subdivision rather than the country.

### 3.1 Identifiers (`identifiers[k]`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `scheme` | string | ✔ | `VAT`, `GLN`, `LEI`, `DUNS`, or `<ISO 3166 alpha-2>:<local abbreviation>` (`LV:REGN`, `US:EIN`, `IN:GSTIN`, `BR:CNPJ`) |
| `value` | string | ✔ | As printed |

Codes: E `identifiers.missing`, E `identifier.not_object`, E `identifier.key_missing`,
E `identifier.type`, E `identifier.scheme_malformed`, W `identifier.scheme_unknown`.

**Why a list and not two fields.** `registration_number` plus `vat_number` is a European company's
paperwork written into a format that claims to be international: it holds no EIN, no TIN, no GSTIN,
no ABN, no CNPJ, no UEN, no CUIT, and no company that has three at once. And an unqualified string
says nothing about whose register it came from — short numbers collide across countries, and a
consumer matching one against its own database is matching two different things.

A scheme outside `registry/identifier-schemes.json` is a **warning**, not an error: there are more
registers in the world than in that list, and refusing a document because a country is missing from
it would be exactly the provincialism the qualifier exists to remove. The *shape* is still required.

**Empty strings are not allowed as "unknown".** A producer must emit `null`. The validator
enforces this on `identifiers[].scheme` and `value` (E `identifier.type`) and reports an empty
`name` (W `party.name_missing`); an empty string in any other party field passes the type check
and is simply a wrong file.

**Neither party may be inferred.** Both blocks describe what the source document says. A producer
that fills `recipient` from whoever happens to be filing the document is writing a claim the paper
does not make into a signed record — and a cash receipt names no buyer at all, so "not stated" has
to be representable. An all-null party block is the correct output in that case.

---

## 4. References (`references[k]`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `type` | string | ✔ | From `registry/reference-types.json` |
| `document_number` | string | ✔ | The referenced document's number, as printed |
| `issue_date` | string\|null | ✔ | `YYYY-MM-DD` |
| `signature` | string\|null | ✔ | The referenced document's DEKRA signature, when the producer holds it |

Codes: E `references.missing`, E `reference.not_object`, E `reference.key_missing`,
E `reference.number_required`, E `reference.type_unknown`, E `reference.date_format`,
E `reference.signature_malformed`, W `references.correction_unlinked`.

A `credit_note`, `debit_note` or `corrective_invoice` with no reference of type `corrects` or
`replaces` gets W `references.correction_unlinked`: the receiver can book the amount and can tie it
to nothing.

**`signature` is the strongest field in this block.** A number can be reused, reissued or typed
wrongly; a signature pins the reference to one exact prior version. With it, a chain of corrections
is verifiable by anyone holding the documents, without access to the issuer's database — which is
what an archive needs and what a bare document number cannot give.

---

## 5. Item (`items[j]`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `description` | string | ✔ | Empty → W `item.description_empty` |
| `quantity` | decimal | ✔ | 4 places |
| `unit` | string | ✔ | UN/ECE Recommendation 20 code, the list PEPPOL uses in `unitCode` (`H87`, `KGM`, `LTR`, `HUR`, …). 1–3 chars, `[A-Z0-9]` |
| `unit_name` | string\|null | ✔ | The unit as a person reads it: the word the document printed (`gab.`, `vnt.`, `tk`, `шт`), or the producer's own short name for the code. **Never the code repeated** |
| `price` | decimal | ✔ | Unit price **excluding tax**, 4 places |
| `discount_percent` | percent | ✔ | Line-level discount |
| `taxes` | array | ✔ | 0…n tax entries, §5.1 |
| `total` | money | ✔ | **Line net: `quantity × price × (1 − discount_percent/100)`. Excludes tax. Excludes the document-level discount.** |
| `comment` | string\|null | ✔ | A note about this line |

Codes: E `item.not_object`, E `item.key_missing`, E `item.type`, E `item.unit_code`,
E `key.missing_number`, E `key.not_decimal`, E `key.scale`, E `percent.range`,
W `item.description_empty`, W `item.total_mismatch`, W `item.unit_unknown`, W `key.unknown`.

`item.total_mismatch` is a **warning**, tolerance ±0.02: sources round differently, and an incoming
document's lines come from a parser. The `total` semantics above are nevertheless normative — a
producer that puts a tax-inclusive figure there is producing a wrong file, and the warning is how it
shows up.

**Why the unit is a code.** A document prints `gab.`, `vnt.`, `tk`, `шт` or `pcs` and means the same
thing every time; stored as printed, that fact is available to a human reader and to nobody else —
quantities cannot be summed across two documents, and a billing message cannot be produced without
guessing. So the code is normative and a readable word is kept beside it: the document's own where
it printed one, because a producer does not get to overwrite what the paper said. A malformed `unit`
is an **error**; a well-formed code outside a reader's table is a **warning**, since Rec 20 has some
1800 codes and no implementation carries all of them. `unit_name` is checked for type only — a
producer that repeats the code there is not caught, only wrong.

**`comment` is a note, not data.** It is where a person writes what the paper does not say — why a
quantity looks wrong, which order it belongs to, that the price was agreed by phone. Free text, no
rule beyond its type, and a reader must not parse it. Like the document's own `comment` it is
outside the signature (STANDARD §4), so a filed document can be annotated without becoming
unverifiable.

### 5.1 Tax (`items[j].taxes[t]`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `type` | string | ✔ | From `registry/tax-types.json`: `VAT`, `GST`, `SALES`, `EXCISE`, `WITHHOLDING`, `OTHER` |
| `category` | string | ✔ | UNTDID 5305, from `registry/tax-categories.json` |
| `rate` | percent | ✔ | The rate, not the amount |
| `amount` | money | ✔ | Tax on this line at this rate |
| `jurisdiction` | string\|null | ✔ | `CC` or `CC-SUB` (ISO 3166-1 / 3166-2) — which authority it is owed to |
| `tax_type_name` | string\|null | ✔ | What the document called it, when `type` is `OTHER` |
| `exemption_reason` | string\|null | ✔ | Free text, in the document's own words |
| `exemption_code` | string\|null | ✔ | The legal reference, when the document states one |

Codes: E `taxes.missing`, E `tax.not_object`, E `tax.key_missing`, E `tax.type_unknown`,
E `tax.category_unknown`, E `tax.zero_category_rate`, E `tax.jurisdiction_invalid`,
E `tax.type_field`, W `tax.exemption_unexplained`.

**Why an array.** One rate per line is a European assumption. A Canadian line carries GST and a
provincial tax at once; a Brazilian line carries ICMS, IPI, PIS and COFINS with different bases; a
United States line carries state, county and city rates that are not a VAT at all and are not
recoverable by the buyer. A single `rate` field cannot express any of them, and a format that
requires one is a European format wearing an international name.

A category meaning "no tax is due" (`Z`, `E`, `AE`, `K`, `G`, `O`) with a rate above zero asserts
two opposite things at once: E `tax.zero_category_rate`. A category of `E`, `AE` or `O` with neither
`exemption_reason` nor `exemption_code` gets W `tax.exemption_unexplained` — a tax authority asks
for the provision, and a receiver cannot invent it.

---

## 6. Allowances and charges (`allowances_charges[c]`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `is_charge` | bool | ✔ | `true` = a charge, `false` = an allowance |
| `amount` | money | ✔ | The absolute amount |
| `base_amount` | decimal\|null | ✔ | What `percent` was applied to |
| `percent` | decimal\|null | ✔ | When the document expressed it as a percentage. Checked as a decimal only — neither the 2-place scale nor the 0…100 range of a *percent* is enforced here |
| `reason` | string\|null | ✔ | The document's own wording |
| `reason_code` | string\|null | ✔ | UNTDID 5189 / 7161, from `registry/allowance-reasons.json` |
| `taxes` | array | ✔ | Tax on the allowance or charge, §5.1. The key must be present (E `charge.key_missing`); a `null` value is not reported — the §5.1 checks run only when it is an array |

Codes: E `allowances_charges.missing`, E `charge.not_object`, E `charge.key_missing`,
E `charge.type_bool`, E `charge.type_field`, E `key.not_decimal`, E `key.scale`,
W `charge.reason_unknown`.

Delivery, packing and insurance are not goods. While they had to be written as line items they
distorted both the item list and the tax base, and an absolute discount had to be converted into a
percentage, losing what was printed. An unlisted reason is stated in `reason` with a null
`reason_code` rather than forced into the nearest match.

---

## 7. Totals, dates, payment, fiscal, source

### 7.1 Totals (`totals`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `currency` | string | ✔ | ISO 4217, three uppercase letters. **Decides the scale of every money field in the document** |
| `tax_currency` | string\|null | ✔ | Only when the tax is owed in a different currency. ISO 4217 shape is checked (E `totals.tax_currency`); that it differs from `currency` is not |
| `exchange_rate` | decimal\|null | ✔ | The rate actually applied |
| `exchange_rate_date` | string\|null | ✔ | `YYYY-MM-DD` |
| `discount_percent` | percent | ✔ | Document-level discount, applied **on top of** line discounts |
| `total_without_tax` | money | ✔ | Sum of line nets after the document discount |
| `tax_breakdown` | array | ✔ | §7.2 |
| `total_tax` | money | ✔ | Total tax |
| `total_tax_in_tax_currency` | decimal\|null | ✔ | The same figure in `tax_currency`. Checked as a decimal only — its scale would come from `tax_currency`, and the validator does not apply it |
| `total_amount` | money | ✔ | `total_without_tax + total_tax` |

Two cross-checks:

- **E `totals.mismatch`** — `|total_without_tax + total_tax − total_amount| > 0.01`. Pure
  self-consistency, independent of any source convention, therefore an **error**.
- **W `totals.items_mismatch`** — the line nets after the document discount disagree with
  `total_without_tax` by more than 0.02. A **warning**: rounding and discount conventions
  legitimately differ between sources.

`tax_currency` set with no `exchange_rate` → W `totals.exchange_rate_missing`. An amount in another
currency with no rate is a claim nobody can check and nobody can reproduce.

Other codes: E `totals.missing`, E `totals.key_missing`, E `totals.currency`,
E `totals.tax_currency`, E `totals.exchange_rate_date`, E `key.missing_number`,
E `key.not_decimal`, E `key.scale`, E `percent.range`. The currency is read **first**: it sets the
scale every money field in the document is checked against, and a document with no readable
currency is checked at 2 places.

### 7.2 Tax breakdown (`totals.tax_breakdown[b]`)

`{ type, category, rate, jurisdiction, taxable_amount, tax_amount }` — the same vocabulary as §5.1.

The rows must sum to `total_tax` within 0.01, or E `tax_breakdown.mismatch`. A document with tax and
an empty breakdown is E `tax_breakdown.empty`. Each row's `type`, `category` and `rate` are checked
with the §5.1 rules and codes; the block's own are E `tax_breakdown.missing`,
E `tax_breakdown.not_object`, E `tax_breakdown.key_missing`. The sum is only compared when every row
parsed — a row with an unreadable amount produces its own error, not a second one for the total.

**This is what a tax return is filed from.** A document with lines at 21 % and 12 % cannot state its
base per rate with a single total: it cannot be filed, cannot be checked, and cannot be converted to
a billing message without the reader re-deriving the split and hoping it matches.

Where a producer cannot honestly decompose the tax — no lines, or lines whose rates do not explain
the printed total — it emits **one row at the effective rate** covering the whole base. That keeps
the rate and the amount agreeing with each other and with the totals. Bending one group's amount to
close the gap would leave a row whose rate contradicts its own figure.

### 7.3 Dates (`dates`)

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `invoice_date` | string | ✔ | `YYYY-MM-DD`, a real date, **must not be null** |
| `tax_point_date` | string\|null | ✔ | When the tax became due. `null` = the same as `invoice_date` |
| `supply_date` | string\|null | ✔ | When the goods or services were supplied |
| `due_date` | string\|null | ✔ | Earlier than `invoice_date` → W `dates.due_before_invoice` |
| `period_start` | string\|null | ✔ | For a periodic document |
| `period_end` | string\|null | ✔ | Before `period_start` → E `dates.period_reversed` |

Dates are **calendar dates without a time zone**. Anything with a time in it is E `dates.format`.
Codes: E `dates.missing`, E `dates.key_missing`, E `dates.format`, E `dates.invoice_date_required`,
E `dates.period_reversed`, W `dates.due_before_invoice`.

`tax_point_date` is separate because in most tax regimes it is legally distinct from the date the
document was written, and it is the one that decides which period the document is filed in.

Both dates are dates printed **on the document**. When a copy arrived somewhere is not one: two
archives holding the same invoice disagree about that and both are right.

### 7.4 Payment (`payment`)

Payment state is deliberately **outside the signature** — a document that gets paid must not become
"corrupt".

`{ method, status, paid_amount, remaining_amount }`. `method` and `status` are free text as stored
by the source, not code lists. Amounts are money, ≥ 0 — the one place the validator checks a sign
(E `payment.negative`). `paid_amount` exceeding `total_amount` by more than 0.01 →
W `payment.overpaid`.

Codes: E `payment.missing`, E `payment.key_missing`, E `payment.type`, E `payment.negative`,
E `key.missing_number`, E `key.not_decimal`, E `key.scale`, W `payment.overpaid`.

### 7.5 Fiscal (`fiscal`)

`null` when the document is not fiscal. Otherwise:

| Key | Type | Req. | Rule |
|---|---|:--:|---|
| `scheme` | string | ✔ | From `registry/fiscal-schemes.json` |
| `id` | string\|null | ✔ | The identifier the authority issued |
| `issued_at` | string\|null | ✔ | When the authority registered it |
| `device_id` | string\|null | ✔ | Fiscal register / cash register / fiscal memory serial |
| `sequence` | string\|null | ✔ | Receipt or shift number |
| `qr` | string\|null | ✔ | The QR payload **exactly as printed** |
| `verification_url` | string\|null | ✔ | Where the authority verifies it |
| `authority_signature` | string\|null | ✔ | The authority's own signature, verbatim |
| `previous_hash` | string\|null | ✔ | For chained regimes (PT, ES) |

Codes: E `fiscal.type`, E `fiscal.key_missing`, E `fiscal.type_field`, E `fiscal.scheme_unknown`,
E `fiscal.empty`, E `fiscal.issued_at_format`.

A scheme naming neither an `id` nor a `qr` names nothing: E `fiscal.empty`.

**This block is inside the signature, and nothing in it is normalised.** It is the only part of a
DEKRA body that is not the producer's reading of paper but another party's claim about the document
— and it is the only thing that lets a holder prove the document against the authority that issued
the identifier. A "tidied up" Portuguese QR payload or a reformatted Brazilian access key no longer
verifies against the issuer, which destroys the one property that makes a stored document evidence
rather than a transcript.

### 7.6 Source document (`source_document`)

`null`, or `{ hash_algorithm, hash, media_type, byte_size, filename }` — every key present,
`byte_size` an integer or `null`, the rest a string or `null`. `hash` is lowercase hex, 32–128
characters.

Codes: E `source_document.type`, E `source_document.key_missing`, E `source_document.type_field`,
E `source_document.algorithm_missing`, E `source_document.hash_malformed`.

Not an attachment — DEKRA carries data, not scans. But without the fingerprint the `.dkr` record
and the file it was read from are two unrelated objects that happen to share a folder, and an
archive cannot show that one came from the other. A hash with no algorithm named is not verifiable:
E `source_document.algorithm_missing`.

---

## 8. Signature profiles

| Profile | Algorithm | What it proves |
|---|---|---|
| `local` | HMAC-SHA256 | The file has not been corrupted. **Not** origin: whoever holds the key to verify it holds the key to forge it |
| `issuer` | Ed25519 | Origin. A verifier needs only the public key, published as a JWK set at `issuer_key_url` |

The profile is written into the file rather than left for a reader to infer from the algorithm,
because the difference between "this file is intact" and "this file came from them" is the whole
question a signature is asked, and a reader that guesses wrong guesses in the dangerous direction.

`key_id` names the key so a compromised one can be retired without invalidating everything signed
before it. A retired public key stays published for as long as files signed with it must verify.

---

## 9. Conformance profiles

| Profile | Adds |
|---|---|
| `core` | Structurally valid. Any document type; no promise beyond the shape |
| `billing` | A billing type; both parties named **and placed in a country**; an issue date; a non-empty tax breakdown |
| `fiscal` | `billing` (or a receipt), plus a `fiscal` block with a scheme and an identifier or QR |
| `archive` | `fiscal`, plus a `source_document` hash |

**A collection is only as strong as its weakest document.** A batch claiming `fiscal` with one
document inside carrying nothing from an authority would be exactly the false assurance the profiles
exist to prevent.

---

## 10. Numbers

**Money and quantities are decimal strings, never JSON numbers.**

A JSON number is an IEEE-754 double: it cannot represent `0.10` exactly. A format whose entire
purpose is proving an amount did not change cannot store that amount in a type that changes it. The
danger is not theoretical — it appears the first time an implementation in another language parses
and re-serialises a file, and it appears as a signature that no longer verifies.

**Scale comes from the currency, not from a constant.** "Two decimal places" is a statement about
the euro: JPY and KRW have no minor unit, KWD, BHD, OMR and TND have three. A money field must carry
exactly the number of places ISO 4217 gives its currency — E `key.scale` otherwise, in both
directions. Quantities carry 4 places, unit prices 4, percentages 2.

**Rounding is half-up**, applied at the scale of the field being written. The rule is stated because
without it two implementations differ by a cent and the signature stops matching for a reason
neither can see.

---

## 11. Rules a producer must follow

1. **Emit every key**, using `null` for unknown — never omit and never use `""`.
2. **`items[].total` is net, pre-document-discount.** Getting this wrong is the single most common
   way to produce a file that validates as "warnings only" but means something else. The second is
   a negative amount: outside `payment` the validator does not check the sign, so a refund written
   as `-100.00` instead of as a `credit_note` produces a file that validates and sums wrongly.
3. **Sign after building, never edit a signed field afterwards.** The exceptions are `payment`,
   `generated_at`, the comments and `extensions`, which are outside both signatures by design.
4. **Make the breakdown add up.** A file whose `tax_breakdown` does not sum to `total_tax` cannot be
   filed by its receiver.
5. **Never infer a country, a recipient or an electronic address.** All three look exactly as
   confident when guessed as when read.
6. **One file, one collection signature.** Concatenating two `.dkr` files is not a valid file.
7. **Cap at 1000 documents** in the container form; larger batches use the streaming form
   (STANDARD §6), which carries the same two signature levels without holding the batch in memory.

## 12. Extensions

`extensions` is an object whose keys are reverse-DNS namespaces
(`^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z0-9-]+)+$` — `pl.gov.ksef`, `br.gov.nfe`). It exists at the
container level and per document.

Without namespaces, "a reader ignores unknown keys" means that sooner or later Poland and Brazil add
their own field and name it the same thing. With them a collision is impossible.

**Extensions are outside both signatures.** An unknown key must never be a reason a file fails to
verify — that is the whole point of the ignore-unknown-keys rule, and signing extension content
would quietly reverse it.

Codes: E `extensions.type`, E `extensions.namespace`.

## 13. Rules a reader must follow

1. **Reject an unknown MAJOR version.** Accept an unknown MINOR — it only adds keys that may be null.
2. **Ignore unknown keys.** They are a warning at most, never a reason to refuse a file.
3. **Verify both signature levels** before trusting anything, and **check `signature_profile`**: a
   `local` signature is a corruption check and proves nothing about who produced the file.
4. **Read the profile before the content.** If you need `fiscal` and the file says `billing`, refuse
   at the door.
5. **Treat the codes as authoritative.** The format carries no display text: translate the codes.
6. **Do not parse the comments.** Free text written by a person, in whatever language they used.
7. **Do not infer accounting treatment.** DEKRA carries no ledger account; book the document under
   your own rules.
