schemas: add expense.canonical v1.1.0 and sink.db.expense v1.1.0 — per-line project allocations (percentage or absolute)
This commit is contained in:
parent
36aa391e59
commit
81e5f334b5
6 changed files with 360 additions and 5 deletions
|
|
@ -6,11 +6,23 @@ Versionierte JSON-Schemas für die kanonischen Business-, Datenquellen- und Senk
|
|||
|
||||
## Vorhandene Schemas
|
||||
|
||||
| Pfad | Rolle | Verwendet in |
|
||||
| ----------------------------------------------- | ------------------------------ | -------------------------------------------------- |
|
||||
| `expense/canonical/v1.0.0.json` | Kanonische Business-Form | Approval-Attestierung (`subject`, `materials`), Agent-A-Transform-Output |
|
||||
| `ds/expense/project_assignment/v1.0.0.json` | Datenquellen-Rückgabe | Query Attestation von `ds:expense-db-app` / `project_assignments` |
|
||||
| `sink/db/expense/v1.0.0.json` | Senken-Payload | Transform-Attestierung `expense.map_to_sink` → `sink:expense-db-app` |
|
||||
| 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) |
|
||||
| `ds/expense/project_assignment/v1.0.0.json` | Datenquellen-Rückgabe | aktiv | Query Attestation von `ds:expense-db-app` / `project_assignments` |
|
||||
| `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 |
|
||||
|
||||
Neue Workflows SOLLEN die v1.1.0-Fassungen nutzen. v1.0.0 bleibt parallel bestehen, damit Attestierungen aus deren Laufzeit verifizierbar bleiben.
|
||||
|
||||
## Zeilen-Allokationen (v1.1.0)
|
||||
|
||||
Jedes `expense_items[]` darf ein optionales `allocations[]`-Array tragen. Jede Allocation referenziert ein `project_code`+`cost_center`+`source_attestation_ref` und trägt entweder einen Prozentsatz (`mode: "percentage"`, Feld `percentage`) oder einen absoluten Betrag (`mode: "absolute"`, Feld `amount`). Alle Entries desselben Items müssen denselben Modus haben. OPA erzwingt Summenerhalt: `sum(percentage) == 100.00` bzw. `sum(amount) == item.amount`.
|
||||
|
||||
Die Senkenfassung (`sink.db.expense` v1.1.0) erwartet immer absolute Beträge — Agent B löst percentage→absolute vor der Senkenübergabe auf und trägt die ursprüngliche `percentage` als informatives Feld mit.
|
||||
|
||||
Beispiele unter `*/examples/vX.Y.Z/*.json` (werden in CI gegen das Schema derselben Version validiert).
|
||||
|
||||
## Namensschema
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"schema": { "name": "expense.canonical", "version": "1.1.0" },
|
||||
"workflow_id": "WF-2026-0042",
|
||||
"employee_id": "EMP-12345",
|
||||
"trip": {
|
||||
"start_date": "2026-10-01",
|
||||
"end_date": "2026-10-03",
|
||||
"destination": "Berlin",
|
||||
"purpose": "Kundentermin"
|
||||
},
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
"project_assignment": {
|
||||
"project_code": "P-42",
|
||||
"cost_center": "CC-100",
|
||||
"source_attestation_ref": "sha256:0000000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
"expense_items": [
|
||||
{ "type": "HOTEL", "amount": "328.50", "incurred_on": "2026-10-01" },
|
||||
{ "type": "MEALS", "amount": "100.00", "incurred_on": "2026-10-02" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"schema": { "name": "expense.canonical", "version": "1.1.0" },
|
||||
"workflow_id": "WF-2026-0043",
|
||||
"employee_id": "EMP-12345",
|
||||
"trip": {
|
||||
"start_date": "2026-10-01",
|
||||
"end_date": "2026-10-03",
|
||||
"destination": "Berlin",
|
||||
"purpose": "Dual-Projekt-Workshop"
|
||||
},
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
"expense_items": [
|
||||
{
|
||||
"type": "HOTEL",
|
||||
"amount": "328.50",
|
||||
"incurred_on": "2026-10-01",
|
||||
"allocations": [
|
||||
{
|
||||
"project_code": "P-42", "cost_center": "CC-100",
|
||||
"mode": "percentage", "percentage": "60.00",
|
||||
"source_attestation_ref": "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"project_code": "P-99", "cost_center": "CC-200",
|
||||
"mode": "percentage", "percentage": "40.00",
|
||||
"source_attestation_ref": "sha256:bbbb000000000000000000000000000000000000000000000000000000000002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MEALS",
|
||||
"amount": "100.00",
|
||||
"incurred_on": "2026-10-02",
|
||||
"allocations": [
|
||||
{
|
||||
"project_code": "P-42", "cost_center": "CC-100",
|
||||
"mode": "absolute", "amount": "70.00",
|
||||
"source_attestation_ref": "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"project_code": "P-99", "cost_center": "CC-200",
|
||||
"mode": "absolute", "amount": "30.00",
|
||||
"source_attestation_ref": "sha256:bbbb000000000000000000000000000000000000000000000000000000000002"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
125
schemas/expense/canonical/v1.1.0.json
Normal file
125
schemas/expense/canonical/v1.1.0.json
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://basicstack.de/schemas/expense/canonical/v1.1.0.json",
|
||||
"title": "Expense (canonical) v1.1.0",
|
||||
"description": "Kanonische Reisekostenstruktur. Additive gegenüber v1.0.0: expense_items[] erhalten ein optionales allocations[]-Array, mit dem einzelne Zeilen auf mehrere Projekte prozentual oder absolut aufgeteilt werden können. Das top-level project_assignment ist optional (für Vorgänge ohne Zeilen-Splits) — es gilt als Default-Zuordnung für alle items ohne allocations. OPA erzwingt: pro Item entweder top-level project_assignment oder item.allocations, nicht beides zugleich; alle Allocations eines Items im selben Modus; Summenerhalt (percentage=100.00 bzw. amount=item.amount).",
|
||||
"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.1.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 }
|
||||
}
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{3}$"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "string",
|
||||
"pattern": "^-?[0-9]+\\.[0-9]{2}$"
|
||||
},
|
||||
"project_assignment": {
|
||||
"description": "Optional. Default-Projektzuordnung für alle items ohne eigenes allocations[]. Wenn ein Item eigene allocations hat, wird dieser Default für dieses Item NICHT angewendet.",
|
||||
"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"]
|
||||
},
|
||||
"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/._-]+$"
|
||||
},
|
||||
"allocations": {
|
||||
"description": "Optional. Aufteilung dieser Zeile auf ein oder mehrere Projekte. Wenn gesetzt, überschreibt das top-level project_assignment für dieses Item. Alle Entries desselben Items MÜSSEN denselben mode haben (OPA-Regel). Bei mode=percentage: Summe der percentage == 100.00. Bei mode=absolute: Summe der amount == item.amount.",
|
||||
"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})$",
|
||||
"description": "Vorhanden wenn mode=percentage; abwesend sonst. Zwei Nachkommastellen, 0.00..100.00."
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"pattern": "^-?[0-9]+\\.[0-9]{2}$",
|
||||
"description": "Vorhanden wenn mode=absolute; abwesend sonst."
|
||||
},
|
||||
"source_attestation_ref": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[0-9a-f]{64}$",
|
||||
"description": "SHA-256 der Query Attestation (ds:expense-db-app / project_assignments), aus der dieses project_code+cost_center stammt."
|
||||
}
|
||||
},
|
||||
"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"] } }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
schemas/sink/db/expense/examples/v1.1.0/split-resolved.json
Normal file
55
schemas/sink/db/expense/examples/v1.1.0/split-resolved.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"schema": { "name": "sink.db.expense", "version": "1.1.0" },
|
||||
"workflow_id": "WF-2026-0043",
|
||||
"approval_id": "APR-2026-0043",
|
||||
"decision_id": "DEC-2026-0043",
|
||||
"verification_ref": "sha256:cccc000000000000000000000000000000000000000000000000000000000003",
|
||||
"canonical_payload_hash": "sha256:dddd000000000000000000000000000000000000000000000000000000000004",
|
||||
"expense": {
|
||||
"employee_id": "EMP-12345",
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
"trip": {
|
||||
"start_date": "2026-10-01",
|
||||
"end_date": "2026-10-03",
|
||||
"destination": "Berlin",
|
||||
"purpose": "Dual-Projekt-Workshop"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"type": "HOTEL",
|
||||
"amount": "328.50",
|
||||
"incurred_on": "2026-10-01",
|
||||
"allocations": [
|
||||
{
|
||||
"project_code": "P-42", "cost_center": "CC-100", "gl_account": "640100",
|
||||
"amount": "197.10", "percentage": "60.00",
|
||||
"source_attestation_ref": "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"project_code": "P-99", "cost_center": "CC-200", "gl_account": "640100",
|
||||
"amount": "131.40", "percentage": "40.00",
|
||||
"source_attestation_ref": "sha256:bbbb000000000000000000000000000000000000000000000000000000000002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MEALS",
|
||||
"amount": "100.00",
|
||||
"incurred_on": "2026-10-02",
|
||||
"allocations": [
|
||||
{
|
||||
"project_code": "P-42", "cost_center": "CC-100", "gl_account": "646000",
|
||||
"amount": "70.00",
|
||||
"source_attestation_ref": "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"project_code": "P-99", "cost_center": "CC-200", "gl_account": "646000",
|
||||
"amount": "30.00",
|
||||
"source_attestation_ref": "sha256:bbbb000000000000000000000000000000000000000000000000000000000002"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
92
schemas/sink/db/expense/v1.1.0.json
Normal file
92
schemas/sink/db/expense/v1.1.0.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://basicstack.de/schemas/sink/db/expense/v1.1.0.json",
|
||||
"title": "Sink Payload: sink:expense-db-app, action expense.post v1.1.0",
|
||||
"description": "Senken-Payload für die eigene DB-App mit Zeilen-Allokationen. Agent-sink-mapping löst alle percentage-Modi aus expense.canonical v1.1.0 in absolute Beträge auf; die Senke erhält ausschließlich absolute amounts. Jedes Item trägt sein eigenes allocations[]-Array (Pflicht, mindestens 1 Entry). Die DB-App persistiert diese als expense_item_allocations-Zeilen (siehe docs/architecture/v1.1.md Abschnitt 7.2).",
|
||||
"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.1.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"]
|
||||
},
|
||||
"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/._-]+$" },
|
||||
"allocations": {
|
||||
"description": "Immer gesetzt in der Senken-Fassung. Mindestens 1 Entry. Alle amounts sind absolut (percentage ist informativ, falls bekannt). Summe aller amount == item.amount.",
|
||||
"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})$",
|
||||
"description": "Optional. Nur gesetzt, wenn canonical mode=percentage war — als Herkunftsnachweis."
|
||||
},
|
||||
"source_attestation_ref": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[0-9a-f]{64}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue