From 27c45b356a608466ec1f3984bc5c35949f00b06396014fe70bb84fc1508cd5d9 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Sun, 25 Oct 2026 17:07:20 +0000 Subject: [PATCH] schemas: add ds/receipts v1.0.0, ds/per_diem v1.0.0, expense.canonical v1.2.0, sink.db.expense v1.2.0 (+ examples) --- schemas/README.md | 18 ++- .../per_diem/examples/v1.0.0/berlin-day.json | 25 +++ schemas/ds/per_diem/v1.0.0.json | 87 +++++++++++ .../examples/v1.0.0/single-receipt.json | 23 +++ schemas/ds/receipts/v1.0.0.json | 88 +++++++++++ .../v1.2.0/with-receipts-and-per-diem.json | 71 +++++++++ schemas/expense/canonical/v1.2.0.json | 147 ++++++++++++++++++ .../v1.2.0/with-receipts-and-per-diem.json | 104 +++++++++++++ schemas/sink/db/expense/v1.2.0.json | 100 ++++++++++++ 9 files changed, 660 insertions(+), 3 deletions(-) create mode 100644 schemas/ds/per_diem/examples/v1.0.0/berlin-day.json create mode 100644 schemas/ds/per_diem/v1.0.0.json create mode 100644 schemas/ds/receipts/examples/v1.0.0/single-receipt.json create mode 100644 schemas/ds/receipts/v1.0.0.json create mode 100644 schemas/expense/canonical/examples/v1.2.0/with-receipts-and-per-diem.json create mode 100644 schemas/expense/canonical/v1.2.0.json create mode 100644 schemas/sink/db/expense/examples/v1.2.0/with-receipts-and-per-diem.json create mode 100644 schemas/sink/db/expense/v1.2.0.json diff --git a/schemas/README.md b/schemas/README.md index 3d47dae..f88d038 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -9,12 +9,16 @@ Versionierte JSON-Schemas für die kanonischen Business-, Datenquellen- und Senk | Pfad | Rolle | Status | Verwendet in | | ----------------------------------------------- | ------------------------------ | --------- | -------------------------------------------------- | | `expense/canonical/v1.0.0.json` | Kanonische Business-Form | legacy | Approval-Attestierung (`subject`, `materials`), Agent-A-Transform-Output | -| `expense/canonical/v1.1.0.json` | Kanonische Business-Form | **aktiv** | wie v1.0.0, zusätzlich mit Zeilen-Allokationen (Multi-Projekt pro Item) | +| `expense/canonical/v1.1.0.json` | Kanonische Business-Form | legacy | wie v1.0.0, zusätzlich mit Zeilen-Allokationen (Multi-Projekt pro Item) | +| `expense/canonical/v1.2.0.json` | Kanonische Business-Form | **aktiv** | wie v1.1.0, zusätzlich mit `receipt_content_hash`/`receipt_attestation_ref` je Item und neuen Item-Typen `PER_DIEM_*` | | `ds/expense/project_assignment/v1.0.0.json` | Datenquellen-Rückgabe | aktiv | Query Attestation von `ds:expense-db-app` / `project_assignments` | +| `ds/receipts/v1.0.0.json` | Datenquellen-Rückgabe | **aktiv** | Query Attestation von `ds:receipts` / `receipt` (Metadaten; Blob kommt separat) | +| `ds/per_diem/v1.0.0.json` | Datenquellen-Rückgabe | **aktiv** | Query Attestation von `ds:per-diem-rates` / `rates` | | `sink/db/expense/v1.0.0.json` | Senken-Payload | legacy | Transform-Attestierung `expense.map_to_sink` → `sink:expense-db-app` (ohne Splits) | -| `sink/db/expense/v1.1.0.json` | Senken-Payload | **aktiv** | wie v1.0.0, zusätzlich mit per-Item-Allocation-Rows für die DB-App | +| `sink/db/expense/v1.1.0.json` | Senken-Payload | legacy | wie v1.0.0, zusätzlich mit per-Item-Allocation-Rows für die DB-App | +| `sink/db/expense/v1.2.0.json` | Senken-Payload | **aktiv** | wie v1.1.0, zusätzlich mit Receipts- und PER_DIEM_*-Item-Support | -Neue Workflows SOLLEN die v1.1.0-Fassungen nutzen. v1.0.0 bleibt parallel bestehen, damit Attestierungen aus deren Laufzeit verifizierbar bleiben. +Neue Workflows SOLLEN die v1.2.0-Fassungen nutzen. Ältere Versionen bleiben parallel bestehen, damit Attestierungen aus deren Laufzeit verifizierbar bleiben. ## Zeilen-Allokationen (v1.1.0) @@ -24,6 +28,14 @@ Die Senkenfassung (`sink.db.expense` v1.1.0) erwartet immer absolute Beträge Beispiele unter `*/examples/vX.Y.Z/*.json` (werden in CI gegen das Schema derselben Version validiert). +## Receipts (v1.2.0) + +Jedes `expense_items[]` darf ein `receipt_content_hash` + `receipt_attestation_ref` tragen. Der eigentliche Beleg liegt im PII-Store, nur Hash und Attestations-Referenz landen in der kanonischen Fassung. Lesender Zugriff auf den Blob erfolgt über `ds:receipts` + Session-Scope-Token nach OPA-Prüfung (siehe `policies/data_sources/receipts.rego` und `docs/architecture/v1.1.md` §4.7). + +## Per-Diem-Items (v1.2.0) + +Neue Item-Typen `PER_DIEM_FULL`, `PER_DIEM_PARTIAL`, `PER_DIEM_ARRIVAL`, `PER_DIEM_DEPARTURE` kennzeichnen vom `agent-per-diem-calculator` deterministisch erzeugte Zeilen. Diese Items MÜSSEN ein `computed_from.transform_attestation_ref` tragen (JSON-Schema erzwingt es per `if/then`), damit die Rückverfolgung zur Berechnungs-Attestation und Rate-Query-Attestation gewährleistet ist (siehe `docs/architecture/v1.1.md` §6.2). + ## Namensschema - `expense/canonical` — kanonische (senken-unabhängige) Business-Form diff --git a/schemas/ds/per_diem/examples/v1.0.0/berlin-day.json b/schemas/ds/per_diem/examples/v1.0.0/berlin-day.json new file mode 100644 index 0000000..57dd2ee --- /dev/null +++ b/schemas/ds/per_diem/examples/v1.0.0/berlin-day.json @@ -0,0 +1,25 @@ +{ + "schema": { "name": "ds.per_diem.rate", "version": "1.0.0" }, + "data_source": "ds:per-diem-rates", + "resource": "rates", + "filters": { + "country": "DE", + "valid_on": "2026-10-02" + }, + "returned_at": "2026-10-25T18:05:00Z", + "rates": [ + { + "rate_id": "PDR-42", + "country": "DE", + "city": null, + "currency": "EUR", + "full_day": "28.00", + "partial_day_min8h": "14.00", + "arrival_day": "14.00", + "departure_day": "14.00", + "valid_from": "2026-01-01", + "valid_to": null, + "source_regulation": "BMF-Schreiben 2025-11-21 Az. IV C 5 - S 2353/19/10011 :005, Anlage 1, Deutschland Inland (Beispielwerte)" + } + ] +} diff --git a/schemas/ds/per_diem/v1.0.0.json b/schemas/ds/per_diem/v1.0.0.json new file mode 100644 index 0000000..a96db7c --- /dev/null +++ b/schemas/ds/per_diem/v1.0.0.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/ds/per_diem/v1.0.0.json", + "title": "DataSource Response: ds:per-diem-rates /rates v1.0.0", + "description": "Rückgabe-Schema für den Lesezugriff auf versionierte Verpflegungsmehraufwands-Pauschalen (z.B. BMF-Sätze für Deutschland, analog für andere Länder). Query-Filter kombinieren country (Pflicht), optional city und valid_on (Pflicht); Rückgabe listet alle am Stichtag gültigen passenden Rates (meistens eine, bei großen Städten mit abweichenden Sätzen gelegentlich mehrere).", + "type": "object", + "additionalProperties": false, + "required": ["schema", "data_source", "resource", "filters", "rates", "returned_at"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "ds.per_diem.rate" }, + "version": { "const": "1.0.0" } + } + }, + "data_source": { "const": "ds:per-diem-rates" }, + "resource": { "const": "rates" }, + "filters": { + "type": "object", + "additionalProperties": false, + "required": ["country", "valid_on"], + "properties": { + "country": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 alpha-2." }, + "city": { "type": "string", "minLength": 1, "maxLength": 100 }, + "valid_on": { "type": "string", "format": "date" } + } + }, + "returned_at": { "type": "string", "format": "date-time" }, + "rates": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "rate_id", + "country", + "currency", + "full_day", + "partial_day_min8h", + "arrival_day", + "departure_day", + "valid_from", + "source_regulation" + ], + "properties": { + "rate_id": { "type": "string", "pattern": "^PDR-[0-9]+$" }, + "country": { "type": "string", "pattern": "^[A-Z]{2}$" }, + "city": { "type": ["string", "null"], "maxLength": 100 }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "full_day": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]{2}$", + "description": "Pauschale für vollen Reisetag (24h Abwesenheit)." + }, + "partial_day_min8h": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]{2}$", + "description": "Pauschale bei >8h bis <24h Abwesenheit (sofern anwendbar)." + }, + "arrival_day": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]{2}$", + "description": "Pauschale Anreisetag (unabhängig von der Abwesenheitsdauer)." + }, + "departure_day": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]{2}$", + "description": "Pauschale Abreisetag (unabhängig von der Abwesenheitsdauer)." + }, + "valid_from": { "type": "string", "format": "date" }, + "valid_to": { "type": ["string", "null"], "format": "date" }, + "source_regulation": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "Freitext-Zitierung der Rechtsquelle, z.B. 'BMF-Schreiben 2026-11-15 Az. IV C 5 - S 2353/19/10011 :005'." + } + } + } + } + } +} diff --git a/schemas/ds/receipts/examples/v1.0.0/single-receipt.json b/schemas/ds/receipts/examples/v1.0.0/single-receipt.json new file mode 100644 index 0000000..1c611d1 --- /dev/null +++ b/schemas/ds/receipts/examples/v1.0.0/single-receipt.json @@ -0,0 +1,23 @@ +{ + "schema": { "name": "ds.receipts", "version": "1.0.0" }, + "data_source": "ds:receipts", + "resource": "receipt", + "filters": { + "content_hash": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + }, + "returned_at": "2026-10-25T18:00:00Z", + "receipt": { + "content_hash": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "pii_store_ref": "pii://receipts/WF-2026-0042/01HFXYZ.pdf", + "mime_type": "application/pdf", + "size_bytes": 184723, + "uploaded_by_agent": "agent-travel", + "on_behalf_of_user": { + "iss": "https://auth.basicstack.de", + "sub": "user-12345", + "session_id": "SES-2026-10-25-abc" + }, + "uploaded_at": "2026-10-25T17:55:12Z", + "attestation_ref": "sha256:deadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeed" + } +} diff --git a/schemas/ds/receipts/v1.0.0.json b/schemas/ds/receipts/v1.0.0.json new file mode 100644 index 0000000..a945864 --- /dev/null +++ b/schemas/ds/receipts/v1.0.0.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/ds/receipts/v1.0.0.json", + "title": "DataSource Response: ds:receipts /receipt v1.0.0", + "description": "Metadaten-Rückgabe des Receipt-Adapters für einen bekannten Beleg (identifiziert über content_hash). Der eigentliche Beleg-Blob wird separat per /api/v1/receipts/{content_hash}/content geladen (nur nach OPA-Prüfung). Diese Response enthält ausschließlich Metadaten und Referenzen — keine PII-Inhalte.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "data_source", "resource", "filters", "receipt", "returned_at"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "ds.receipts" }, + "version": { "const": "1.0.0" } + } + }, + "data_source": { "const": "ds:receipts" }, + "resource": { "const": "receipt" }, + "filters": { + "type": "object", + "additionalProperties": false, + "required": ["content_hash"], + "properties": { + "content_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + } + }, + "returned_at": { "type": "string", "format": "date-time" }, + "receipt": { + "type": "object", + "additionalProperties": false, + "required": [ + "content_hash", + "pii_store_ref", + "mime_type", + "size_bytes", + "uploaded_by_agent", + "on_behalf_of_user", + "uploaded_at", + "attestation_ref" + ], + "properties": { + "content_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "pii_store_ref": { + "type": "string", + "pattern": "^pii://[A-Za-z0-9/._-]+$", + "description": "Logischer Pfad im PII-Store (verschlüsselter Bucket). KEIN Download-Link; der Blob kommt über einen separaten authorisierten Endpunkt." + }, + "mime_type": { + "type": "string", + "enum": ["application/pdf", "image/png", "image/jpeg", "image/webp"] + }, + "size_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 20971520, + "description": "Beleg-Blob-Grösse in Bytes. Hartes Oberlimit: 20 MiB." + }, + "uploaded_by_agent": { + "type": "string", + "description": "agent_id der uploadenden Komponente (typischerweise agent-travel)." + }, + "on_behalf_of_user": { + "type": "object", + "additionalProperties": false, + "required": ["iss", "sub", "session_id"], + "properties": { + "iss": { "type": "string" }, + "sub": { "type": "string" }, + "session_id": { "type": "string" } + } + }, + "uploaded_at": { "type": "string", "format": "date-time" }, + "attestation_ref": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 der Receipt Attestation (predicateType https://basicstack.de/attestations/receipt_ingest/v1)." + }, + "deleted_at": { + "type": ["string", "null"], + "format": "date-time", + "description": "Wenn gesetzt: der Beleg wurde gelöscht (Retention oder DSGVO-Antrag). GET auf den Blob-Endpunkt liefert dann 410 Gone." + } + } + } + } +} diff --git a/schemas/expense/canonical/examples/v1.2.0/with-receipts-and-per-diem.json b/schemas/expense/canonical/examples/v1.2.0/with-receipts-and-per-diem.json new file mode 100644 index 0000000..f309aaa --- /dev/null +++ b/schemas/expense/canonical/examples/v1.2.0/with-receipts-and-per-diem.json @@ -0,0 +1,71 @@ +{ + "schema": { "name": "expense.canonical", "version": "1.2.0" }, + "workflow_id": "WF-2026-0044", + "employee_id": "EMP-12345", + "trip": { + "start_date": "2026-10-01", + "end_date": "2026-10-03", + "destination": "Berlin", + "purpose": "Kundentermin + Workshop", + "countries_visited": [ + { "country": "DE", "city": "Berlin", "from_date": "2026-10-01", "to_date": "2026-10-03" } + ] + }, + "currency": "EUR", + "total_amount": "484.50", + "project_assignment": { + "project_code": "P-42", + "cost_center": "CC-100", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + }, + "expense_items": [ + { + "type": "HOTEL", + "amount": "328.50", + "incurred_on": "2026-10-01", + "description": "Hotel Berlin Mitte, 2 Nächte", + "receipt_ref": "pii://receipts/WF-2026-0044/01HFXYZ.pdf", + "receipt_content_hash": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "receipt_attestation_ref": "sha256:deadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeed" + }, + { + "type": "PER_DIEM_ARRIVAL", + "amount": "14.00", + "incurred_on": "2026-10-01", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + } + }, + { + "type": "PER_DIEM_FULL", + "amount": "28.00", + "incurred_on": "2026-10-02", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + } + }, + { + "type": "PER_DIEM_DEPARTURE", + "amount": "14.00", + "incurred_on": "2026-10-03", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + } + }, + { + "type": "TRANSPORT_RAIL", + "amount": "100.00", + "incurred_on": "2026-10-01", + "description": "ICE Hin- und Rückfahrt", + "receipt_ref": "pii://receipts/WF-2026-0044/01HFABC.pdf", + "receipt_content_hash": "sha256:b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", + "receipt_attestation_ref": "sha256:cafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeef" + } + ] +} diff --git a/schemas/expense/canonical/v1.2.0.json b/schemas/expense/canonical/v1.2.0.json new file mode 100644 index 0000000..140ee61 --- /dev/null +++ b/schemas/expense/canonical/v1.2.0.json @@ -0,0 +1,147 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/expense/canonical/v1.2.0.json", + "title": "Expense (canonical) v1.2.0", + "description": "Additive gegenüber v1.1.0: (1) pro Item optional receipt_content_hash + receipt_attestation_ref, damit die Belegbindung in die Signatur-Kette eingeht. (2) Neue Item-Typen PER_DIEM_FULL, PER_DIEM_PARTIAL, PER_DIEM_ARRIVAL, PER_DIEM_DEPARTURE für vom per-diem-calculator erzeugte Pauschalen. (3) Pro Item optional computed_from — Rückbindung an die Transform- und Query-Attestationen, aus denen das Item deterministisch gerechnet wurde.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "workflow_id", "employee_id", "trip", "currency", "total_amount", "expense_items"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "expense.canonical" }, + "version": { "const": "1.2.0" } + } + }, + "workflow_id": { "type": "string", "pattern": "^WF-[0-9]{4}-[0-9]{4,}$" }, + "employee_id": { "type": "string", "pattern": "^EMP-[0-9]+$" }, + "trip": { + "type": "object", + "additionalProperties": false, + "required": ["start_date", "end_date", "destination"], + "properties": { + "start_date": { "type": "string", "format": "date" }, + "end_date": { "type": "string", "format": "date" }, + "destination": { "type": "string", "minLength": 1, "maxLength": 200 }, + "purpose": { "type": "string", "maxLength": 500 }, + "countries_visited": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["country", "from_date", "to_date"], + "properties": { + "country": { "type": "string", "pattern": "^[A-Z]{2}$" }, + "city": { "type": ["string","null"], "maxLength": 100 }, + "from_date": { "type": "string", "format": "date" }, + "to_date": { "type": "string", "format": "date" } + } + } + } + } + }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "total_amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "project_assignment": { + "type": "object", + "additionalProperties": false, + "required": ["project_code", "cost_center", "source_attestation_ref"], + "properties": { + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "source_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + } + }, + "expense_items": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["type", "amount", "incurred_on"], + "properties": { + "type": { + "type": "string", + "enum": [ + "HOTEL", "MEALS", "TRANSPORT_FLIGHT", "TRANSPORT_RAIL", "TRANSPORT_CAR", + "PARKING", "COMMUNICATION", "CONFERENCE_FEE", "OTHER", + "PER_DIEM_FULL", "PER_DIEM_PARTIAL", "PER_DIEM_ARRIVAL", "PER_DIEM_DEPARTURE" + ] + }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "incurred_on": { "type": "string", "format": "date" }, + "description": { "type": "string", "maxLength": 500 }, + "receipt_ref": { "type": "string", "pattern": "^pii://[A-Za-z0-9/._-]+$" }, + "receipt_content_hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 des hochgeladenen Belegs. Wird vom Receipt-Adapter beim Upload geliefert und ins Item eingetragen; vom Approval mit-signiert." + }, + "receipt_attestation_ref": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 der Receipt Attestation (ingest). Verification kann so prüfen, dass genau dieser Beleg vorlag." + }, + "computed_from": { + "type": "object", + "additionalProperties": false, + "required": ["transform_attestation_ref"], + "description": "Nur gesetzt für deterministisch berechnete Items (z.B. PER_DIEM_*). Bindet an die Transform-Attestation, die dieses Item erzeugt hat, und optional an die Rate-Query-Attestation.", + "properties": { + "transform_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "rate_query_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "rate_id": { "type": "string", "pattern": "^PDR-[0-9]+$" } + } + }, + "allocations": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["project_code", "cost_center", "mode", "source_attestation_ref"], + "properties": { + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "mode": { "enum": ["percentage", "absolute"] }, + "percentage": { "type": "string", "pattern": "^(100\\.00|[0-9]{1,2}\\.[0-9]{2})$" }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "source_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + }, + "allOf": [ + { + "if": { "properties": { "mode": { "const": "percentage" } }, "required": ["mode"] }, + "then": { "required": ["percentage"], "not": { "required": ["amount"] } } + }, + { + "if": { "properties": { "mode": { "const": "absolute" } }, "required": ["mode"] }, + "then": { "required": ["amount"], "not": { "required": ["percentage"] } } + } + ] + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": ["PER_DIEM_FULL", "PER_DIEM_PARTIAL", "PER_DIEM_ARRIVAL", "PER_DIEM_DEPARTURE"] + } + }, + "required": ["type"] + }, + "then": { "required": ["computed_from"] } + } + ] + } + } + } +} diff --git a/schemas/sink/db/expense/examples/v1.2.0/with-receipts-and-per-diem.json b/schemas/sink/db/expense/examples/v1.2.0/with-receipts-and-per-diem.json new file mode 100644 index 0000000..d977fb5 --- /dev/null +++ b/schemas/sink/db/expense/examples/v1.2.0/with-receipts-and-per-diem.json @@ -0,0 +1,104 @@ +{ + "schema": { "name": "sink.db.expense", "version": "1.2.0" }, + "workflow_id": "WF-2026-0044", + "approval_id": "APR-2026-0044", + "decision_id": "DEC-2026-0044", + "verification_ref": "sha256:4444000000000000000000000000000000000000000000000000000000000004", + "canonical_payload_hash": "sha256:5555000000000000000000000000000000000000000000000000000000000005", + "expense": { + "employee_id": "EMP-12345", + "currency": "EUR", + "total_amount": "484.50", + "trip": { + "start_date": "2026-10-01", + "end_date": "2026-10-03", + "destination": "Berlin", + "purpose": "Kundentermin + Workshop" + }, + "items": [ + { + "type": "HOTEL", + "amount": "328.50", + "incurred_on": "2026-10-01", + "description": "Hotel Berlin Mitte, 2 Nächte", + "receipt_ref": "pii://receipts/WF-2026-0044/01HFXYZ.pdf", + "receipt_content_hash": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "receipt_attestation_ref": "sha256:deadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeed", + "allocations": [ + { + "project_code": "P-42", "cost_center": "CC-100", "gl_account": "640100", + "amount": "328.50", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "type": "PER_DIEM_ARRIVAL", + "amount": "14.00", + "incurred_on": "2026-10-01", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + }, + "allocations": [ + { + "project_code": "P-42", "cost_center": "CC-100", "gl_account": "646100", + "amount": "14.00", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "type": "PER_DIEM_FULL", + "amount": "28.00", + "incurred_on": "2026-10-02", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + }, + "allocations": [ + { + "project_code": "P-42", "cost_center": "CC-100", "gl_account": "646100", + "amount": "28.00", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "type": "PER_DIEM_DEPARTURE", + "amount": "14.00", + "incurred_on": "2026-10-03", + "computed_from": { + "transform_attestation_ref": "sha256:2222000000000000000000000000000000000000000000000000000000000002", + "rate_query_attestation_ref": "sha256:3333000000000000000000000000000000000000000000000000000000000003", + "rate_id": "PDR-42" + }, + "allocations": [ + { + "project_code": "P-42", "cost_center": "CC-100", "gl_account": "646100", + "amount": "14.00", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "type": "TRANSPORT_RAIL", + "amount": "100.00", + "incurred_on": "2026-10-01", + "description": "ICE Hin- und Rückfahrt", + "receipt_ref": "pii://receipts/WF-2026-0044/01HFABC.pdf", + "receipt_content_hash": "sha256:b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", + "receipt_attestation_ref": "sha256:cafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeefcafefeeddeadbeef", + "allocations": [ + { + "project_code": "P-42", "cost_center": "CC-100", "gl_account": "640200", + "amount": "100.00", + "source_attestation_ref": "sha256:1111000000000000000000000000000000000000000000000000000000000001" + } + ] + } + ] + } +} diff --git a/schemas/sink/db/expense/v1.2.0.json b/schemas/sink/db/expense/v1.2.0.json new file mode 100644 index 0000000..08927e3 --- /dev/null +++ b/schemas/sink/db/expense/v1.2.0.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/sink/db/expense/v1.2.0.json", + "title": "Sink Payload: sink:expense-db-app, action expense.post v1.2.0", + "description": "Additive gegenüber v1.1.0: items[] können PER_DIEM_* Typen tragen, items[] können receipt_content_hash + receipt_attestation_ref + computed_from mitführen. allocations[]-Vertrag unverändert (absolut, Summenerhalt pro Item).", + "type": "object", + "additionalProperties": false, + "required": ["schema", "workflow_id", "approval_id", "decision_id", "verification_ref", "canonical_payload_hash", "expense"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "sink.db.expense" }, + "version": { "const": "1.2.0" } + } + }, + "workflow_id": { "type": "string", "pattern": "^WF-[0-9]{4}-[0-9]{4,}$" }, + "approval_id": { "type": "string", "pattern": "^APR-[0-9]{4}-[0-9]{4,}$" }, + "decision_id": { "type": "string", "pattern": "^DEC-[0-9]{4}-[0-9]{4,}$" }, + "verification_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "canonical_payload_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "expense": { + "type": "object", + "additionalProperties": false, + "required": ["employee_id", "currency", "total_amount", "trip", "items"], + "properties": { + "employee_id": { "type": "string", "pattern": "^EMP-[0-9]+$" }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "total_amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "trip": { + "type": "object", + "additionalProperties": false, + "required": ["start_date", "end_date", "destination"], + "properties": { + "start_date": { "type": "string", "format": "date" }, + "end_date": { "type": "string", "format": "date" }, + "destination": { "type": "string", "maxLength": 200 }, + "purpose": { "type": "string", "maxLength": 500 } + } + }, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["type", "amount", "incurred_on", "allocations"], + "properties": { + "type": { + "type": "string", + "enum": [ + "HOTEL", "MEALS", "TRANSPORT_FLIGHT", "TRANSPORT_RAIL", "TRANSPORT_CAR", + "PARKING", "COMMUNICATION", "CONFERENCE_FEE", "OTHER", + "PER_DIEM_FULL", "PER_DIEM_PARTIAL", "PER_DIEM_ARRIVAL", "PER_DIEM_DEPARTURE" + ] + }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "incurred_on": { "type": "string", "format": "date" }, + "description": { "type": "string", "maxLength": 500 }, + "receipt_ref": { "type": "string", "pattern": "^pii://[A-Za-z0-9/._-]+$" }, + "receipt_content_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "receipt_attestation_ref":{ "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "computed_from": { + "type": "object", + "additionalProperties": false, + "required": ["transform_attestation_ref"], + "properties": { + "transform_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "rate_query_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "rate_id": { "type": "string", "pattern": "^PDR-[0-9]+$" } + } + }, + "allocations": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["project_code", "cost_center", "gl_account", "amount", "source_attestation_ref"], + "properties": { + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "gl_account": { "type": "string", "pattern": "^[0-9]{4,6}$" }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "percentage": { "type": "string", "pattern": "^(100\\.00|[0-9]{1,2}\\.[0-9]{2})$" }, + "source_attestation_ref": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + } + } + } + } + } + } + } + } + } +}