{
    "openapi": "3.2.0",
    "info": {
        "title": "Mosaic Hub API",
        "version": "1.0.0",
        "summary": "Public REST API for Mosaic Hub.",
        "description": "Public REST API for Mosaic Hub. v1 surface covers spending-method ingest for the Numbers module.\n\nGlobal version-first path scheme: `/v1/...`. Payload-level evolution is tracked separately via the `schema_version` field on the request body, so additive schema changes do not require a URL version bump.\n\nThe spending-methods endpoint accepts a **batch** of one or more items in a single request, with attachments referenced by multipart part name. Up to 100 items per batch, up to 5 attachments per item.\n\nAll envelope-level error responses are RFC 7807 `application/problem+json` documents. Per-item failures are returned inside `results[].problem` on a 200 response.",
        "contact": {
            "name": "Mosaic Family Office (Pty) Ltd",
            "url": "https://hub.mosaic.co.za/mosaic-hub-developer.php",
            "email": "developers@mosaic.co.za"
        },
        "license": {
            "name": "Proprietary - Mosaic Family Office (Pty) Ltd"
        }
    },
    "servers": [
        { "url": "https://api.mosaic.co.za", "description": "Production" }
    ],
    "tags": [
        { "name": "Numbers / Spending Methods", "description": "Ingest spending methods (supplier bills in v1) into the Numbers ledger." },
        { "name": "Numbers / Receivables", "description": "Batch ingest plus lifecycle (commit / void / quote-accept / quote-convert) endpoints for receivables invoices, credit notes and quotes." }
    ],
    "security": [
        { "MosaicHmac": [] }
    ],
    "paths": {
        "/v1/healthz": {
            "get": {
                "summary": "Liveness probe.",
                "description": "Anonymous endpoint used by Azure App Service health checks. Always returns 200.",
                "security": [],
                "responses": {
                    "200": {
                        "description": "Service is up.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": { "status": { "type": "string", "const": "ok" } }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/numbers-spending-methods": {
            "post": {
                "tags": ["Numbers / Spending Methods"],
                "summary": "Ingest a batch of spending methods.",
                "description": "Ingests a batch of 1\u2013100 spending methods as a `multipart/form-data` upload. For v1 the only `type` accepted is `supplier-bill`. Each item is persisted as a draft transaction (`status='d'`) in the Numbers module and surfaces in the existing UI for commit/void/VAT processing.\n\n**Multipart layout**\n- `batch` (application/json, required) - body conforming to SpendingMethodBatchRequest.\n- `attachment-<name>` (binary, optional, repeated) - up to 5 per item. Each item lists the part names that belong to it via `attachment_part_names[]`. Every reference must resolve; every uploaded attachment must be referenced exactly once (no sharing, no orphans). Permitted MIME: `application/pdf`, `image/jpeg`, `image/png`, `image/webp`. Max 25 MB per file, 150 MB total per request.\n\n**Partial success** - the response is `200` whenever at least one item was persisted; per-item outcomes are in `results[]`. When every item fails, the same body shape is returned with status `422`.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "required": ["batch"],
                                "properties": {
                                    "batch": {
                                        "description": "JSON body conforming to SpendingMethodBatchRequest.",
                                        "$ref": "#/components/schemas/SpendingMethodBatchRequest"
                                    }
                                },
                                "additionalProperties": {
                                    "description": "Attachment parts. Field name must match the pattern `attachment-[A-Za-z0-9_-]{1,60}` and be referenced by an item's `attachment_part_names[]`.",
                                    "type": "string",
                                    "format": "binary"
                                }
                            },
                            "encoding": {
                                "batch": { "contentType": "application/json" }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Batch processed. At least one item was persisted; per-item outcomes are in `results[]`.",
                        "headers": {
                            "Idempotent-Replay": {
                                "description": "Present and `true` when the response was served from the idempotency log.",
                                "schema": { "type": "string", "enum": ["true"] }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": { "$ref": "#/components/schemas/SpendingMethodBatchResponse" },
                                "examples": {
                                    "partial-success": {
                                        "value": {
                                            "batch_identifier": "019f5555-aaaa-7bbb-8ccc-1234567890ab",
                                            "schema_version": "1.0.0",
                                            "submitted_count": 2,
                                            "succeeded_count": 1,
                                            "failed_count": 1,
                                            "results": [
                                                {
                                                    "item_index": 0,
                                                    "status": "created",
                                                    "spending_method_identifier": "019f1234-5678-7abc-9def-0123456789ab",
                                                    "external_identifier": "019f0000-aaaa-7bbb-8ccc-1234567890ab",
                                                    "_links": { "self": "/v1/numbers-spending-methods/019f1234-5678-7abc-9def-0123456789ab" }
                                                },
                                                {
                                                    "item_index": 1,
                                                    "status": "failed",
                                                    "problem": {
                                                        "type": "https://api.mosaic.co.za/problems/supplier-not-found",
                                                        "title": "Unprocessable Entity",
                                                        "status": 422,
                                                        "detail": "Supplier account number not found."
                                                    }
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "413": { "$ref": "#/components/responses/PayloadTooLarge" },
                    "415": { "$ref": "#/components/responses/UnsupportedMediaType" },
                    "422": {
                        "description": "Either the envelope failed validation (RFC 7807) **or** every item in the batch failed (SpendingMethodBatchResponse with `succeeded_count = 0`).",
                        "content": {
                            "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } },
                            "application/json": { "schema": { "$ref": "#/components/schemas/SpendingMethodBatchResponse" } }
                        }
                    },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-invoices": {
            "post": {
                "operationId": "createReceivablesInvoicesBatch",
                "tags": ["Numbers / Receivables"],
                "summary": "Ingest a batch of receivables invoices.",
                "description": "Persists 1\u2013500 receivables invoices in a single JSON request. Each item lands as a separate transaction header with `capture_channel='api_partner'`, three legs per line (trading, VAT bucket, AR control). `post_status` per item controls whether the header lands in draft (`d`) or committed (`c`). The response is `200` whenever at least one item was persisted; per-item failures are returned inside `results[].problem` as RFC 7807. When every item fails the same body shape is returned with status `422`.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" }
                ],
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceBatchRequest" } } }
                },
                "responses": {
                    "200": { "description": "Batch processed.", "headers": { "Idempotent-Replay": { "description": "Present and `true` when the response was served from the idempotency log.", "schema": { "type": "string", "enum": ["true"] } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "413": { "$ref": "#/components/responses/PayloadTooLarge" },
                    "422": { "description": "Either the envelope failed validation (RFC 7807) **or** every item in the batch failed (ReceivablesBatchResponse with `succeeded_count = 0`).", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-invoices/{id}/commit": {
            "post": {
                "operationId": "commitReceivablesInvoice",
                "description": "Transitions the invoice header from draft (`d`) to committed (`c`). 409 `state-transition-invalid` if the current status is not `d`.",
                "tags": ["Numbers / Receivables"],
                "summary": "Commit a draft invoice (status d \u2192 c).",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Transition applied." },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-invoices/{id}/void": {
            "post": {
                "operationId": "voidReceivablesInvoice",
                "tags": ["Numbers / Receivables"],
                "summary": "Void an invoice (status d|c \u2192 v).",
                "description": "Marks the invoice voided. Refused with 409 `vat-return-locked` if the transaction date sits on or before any active lock (vat_period_locked_date, numbers_date_locked_final, numbers_date_locked). Refused with 409 `asset-register-linked` if the invoice has active asset-register cost entries.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Voided." },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-credit-notes": {
            "post": {
                "operationId": "createReceivablesCreditNotesBatch",
                "tags": ["Numbers / Receivables"],
                "summary": "Ingest a batch of receivables credit notes.",
                "description": "Same envelope shape as the invoices endpoint. Credit-note line trading legs post on the debit side (reverses the original invoice movement); the counterparty control leg posts on the credit side. Optional `originating_invoice_identifier` links the credit note to its source invoice.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" }
                ],
                "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditNoteBatchRequest" } } } },
                "responses": {
                    "200": { "description": "Batch processed.", "headers": { "Idempotent-Replay": { "schema": { "type": "string", "enum": ["true"] } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "413": { "$ref": "#/components/responses/PayloadTooLarge" },
                    "422": { "description": "Envelope problem or all items failed.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-credit-notes/{id}/commit": {
            "post": {
                "operationId": "commitReceivablesCreditNote",
                "description": "Transitions the credit note header from draft (`d`) to committed (`c`).",
                "tags": ["Numbers / Receivables"],
                "summary": "Commit a draft credit note (status d \u2192 c).",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Transition applied." },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-credit-notes/{id}/void": {
            "post": {
                "operationId": "voidReceivablesCreditNote",
                "tags": ["Numbers / Receivables"],
                "summary": "Void a credit note (status d|c \u2192 v).",
                "description": "Same lock guards as invoice void.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Voided." },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-quotes": {
            "post": {
                "operationId": "createReceivablesQuotesBatch",
                "tags": ["Numbers / Receivables"],
                "summary": "Ingest a batch of receivables quotes.",
                "description": "Quotes do not post to a real AR control account — the counterparty leg is a non-control zero-value placeholder on the trade-receivables account. Convert a quote to an invoice via the `/convert` endpoint.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" }
                ],
                "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuoteBatchRequest" } } } },
                "responses": {
                    "200": { "description": "Batch processed.", "headers": { "Idempotent-Replay": { "schema": { "type": "string", "enum": ["true"] } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "413": { "$ref": "#/components/responses/PayloadTooLarge" },
                    "422": { "description": "Envelope problem or all items failed.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ReceivablesBatchResponse" } } } },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-quotes/{id}/commit": {
            "post": {
                "operationId": "commitReceivablesQuote",
                "description": "Transitions the quote header from draft (`d`) to sent (`c`).",
                "tags": ["Numbers / Receivables"],
                "summary": "Commit (send) a draft quote (status d \u2192 c).",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Transition applied." },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-quotes/{id}/accept": {
            "post": {
                "operationId": "acceptReceivablesQuote",
                "description": "Transitions a sent quote (`c`) to accepted (`a`).",
                "tags": ["Numbers / Receivables"],
                "summary": "Accept a sent quote (status c \u2192 a).",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Quote accepted." },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-quotes/{id}/void": {
            "post": {
                "operationId": "voidReceivablesQuote",
                "description": "Marks the quote voided.",
                "tags": ["Numbers / Receivables"],
                "summary": "Void a quote (status d|c|a \u2192 v).",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "responses": {
                    "204": { "description": "Voided." },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        },
        "/v1/numbers-receivables-quotes/{id}/convert": {
            "post": {
                "operationId": "convertReceivablesQuoteToInvoice",
                "tags": ["Numbers / Receivables"],
                "summary": "Convert a Sent or Accepted quote into a new invoice.",
                "description": "Creates a brand-new invoice header (status `draft` unless `post_status='committed'`) by copying the quote's trading lines and appending a real AR counterparty debit at gross. The source quote is marked accepted (`a`). The new invoice is linked back to the quote via `numbers_transaction_headers.related_transaction`.",
                "parameters": [
                    { "$ref": "#/components/parameters/IdempotencyKey" },
                    { "$ref": "#/components/parameters/MosaicTimestamp" },
                    { "$ref": "#/components/parameters/MosaicNonce" },
                    { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
                ],
                "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuoteConvertRequest" } } } },
                "responses": {
                    "200": { "description": "Invoice created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuoteConvertResponse" } } } },
                    "400": { "$ref": "#/components/responses/BadRequest" },
                    "401": { "$ref": "#/components/responses/Unauthorised" },
                    "403": { "$ref": "#/components/responses/Forbidden" },
                    "404": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "409": { "$ref": "#/components/responses/Conflict" },
                    "422": { "$ref": "#/components/responses/UnprocessableEntity" },
                    "500": { "$ref": "#/components/responses/InternalError" }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "MosaicHmac": {
                "type": "http",
                "scheme": "Mosaic-HMAC-SHA256",
                "description": "HMAC-SHA256 request signing.\n\nHeaders:\n- `Authorization: Mosaic-HMAC-SHA256 key-id=<UUID>,signature=<base64>`\n- `X-Mosaic-Timestamp: <RFC 3339 UTC>` (within \u00b1300s of server time)\n- `X-Mosaic-Nonce: <UUID v7>` (single-use, retained for 10 minutes)\n\nCanonical string (LF-joined):\n\n```\nMETHOD\nPATH\nSORTED_QUERY\nSHA256_HEX(RAW_BODY)\nTIMESTAMP\nNONCE\n```\n\nSignature: `base64(HMAC_SHA256(secret, canonical))`.\n\nSee https://hub.mosaic.co.za/mosaic-hub-developer-authentication.php for reference implementations."
            }
        },
        "parameters": {
            "IdempotencyKey": {
                "name": "Idempotency-Key",
                "in": "header",
                "required": true,
                "description": "Client-generated UUID v7 (recommended) up to 64 characters. Reusing the same key with an identical payload replays the original response; reusing it with a different payload returns 422 `idempotency-key-conflict`.",
                "schema": { "type": "string", "maxLength": 64 }
            },
            "MosaicTimestamp": {
                "name": "X-Mosaic-Timestamp",
                "in": "header",
                "required": true,
                "description": "RFC 3339 UTC timestamp used in the HMAC canonical string. Must be within \u00b1300s of server time.",
                "schema": { "type": "string", "format": "date-time" }
            },
            "MosaicNonce": {
                "name": "X-Mosaic-Nonce",
                "in": "header",
                "required": true,
                "description": "UUID v7 nonce. Single-use; the server retains it for 10 minutes to detect replay.",
                "schema": { "type": "string", "format": "uuid" }
            }
        },
        "responses": {
            "BadRequest":            { "description": "Request was malformed.",                 "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "Unauthorised":          { "description": "Authentication failed.",                 "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "Forbidden":             { "description": "Authenticated key lacks the required scope.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "NonceReplay":           { "description": "X-Mosaic-Nonce has already been used.",  "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "Conflict":              { "description": "Conflict - either X-Mosaic-Nonce replay or external_identifier already used for this client.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "PayloadTooLarge":       { "description": "Request body exceeded a size limit.",    "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "UnsupportedMediaType":  { "description": "Media type of a part is not supported.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "UnprocessableEntity":   { "description": "Schema or business rule validation failed.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
            "InternalError":         { "description": "An unexpected server error.",            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } }
        },
        "schemas": {
            "Problem": {
                "type": "object",
                "description": "RFC 7807 problem document.",
                "required": ["type", "title", "status"],
                "properties": {
                    "type":    { "type": "string", "format": "uri", "description": "Identifier for the problem type (https://api.mosaic.co.za/problems/...)." },
                    "title":   { "type": "string" },
                    "status":  { "type": "integer" },
                    "detail":  { "type": "string" },
                    "instance":{ "type": "string", "format": "uri" }
                },
                "additionalProperties": true
            },
            "Money": {
                "type": "object",
                "additionalProperties": false,
                "required": ["amount", "currency_code"],
                "properties": {
                    "amount":        { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$", "description": "Decimal string, up to 4 fractional digits." },
                    "currency_code": { "type": "string", "pattern": "^[A-Z]{3}$" }
                }
            },
            "SupplierBillLine": {
                "type": "object",
                "additionalProperties": false,
                "required": ["description", "quantity", "unit_amount", "account_code", "vat201_rate_code", "line_total"],
                "properties": {
                    "description":      { "type": "string", "minLength": 1, "maxLength": 250 },
                    "quantity":         { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "unit_amount":      { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "account_code":     { "type": "string", "minLength": 1, "maxLength": 50 },
                    "vat201_rate_code": { "type": "string", "pattern": "^(\\d{8}-[A-Za-z0-9]+|[A-Za-z][A-Za-z0-9]*)$", "maxLength": 40, "description": "Public Mosaic VAT rate identifier. Either YYYYMMDD-<suffix> for SARS-aligned rates (date = effective start date, or end date for historical rates; suffix = VAT201 field number such as 1, 1A, 15A) or a bare PascalCase token for date-independent rows (NotRegisteredVendor, OutOfScope, ZeroRatedPurchase). Examples: 20180401-15, 20180401-1A, OutOfScope." },
                    "line_total":       { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$", "description": "Net amount per line (VAT-exclusive). Sum of line_total across all lines must equal subtotal." }
                }
            },
            "SupplierBill": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "supplier_account_number", "invoice_number", "invoice_date",
                    "currency_code", "lines", "subtotal", "vat_total", "total"
                ],
                "properties": {
                    "supplier_account_number": { "type": "string", "minLength": 1, "maxLength": 50, "description": "Matches numbers_counterparty_information.account_number for the client." },
                    "invoice_number":          { "type": "string", "minLength": 1, "maxLength": 100 },
                    "invoice_date":            { "type": "string", "format": "date" },
                    "due_date":                { "type": "string", "format": "date" },
                    "currency_code":           { "type": "string", "pattern": "^[A-Z]{3}$", "description": "Required. ISO 4217 alpha-3 currency code (uppercase, e.g. ZAR, USD, EUR, GBP). Persisted verbatim to numbers_transaction_headers.transaction_currency; line amounts and the AP credit leg all post in this currency." },
                    "narration":               { "type": "string", "maxLength": 250 },
                    "external_identifier":     { "type": "string", "format": "uuid", "description": "Optional partner-supplied UUID v7 that uniquely identifies the source document in the partner's system. When supplied, persisted to numbers_transaction_headers.external_identifier and enforced unique per hub_client_identifier. Re-sending the same value (with a different Idempotency-Key) returns 409 external-identifier-conflict." },
                    "lines":                   { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/SupplierBillLine" } },
                    "subtotal":                { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "vat_total":               { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "total":                   { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" }
                }
            },
            "SpendingMethodItem": {
                "type": "object",
                "additionalProperties": false,
                "required": ["type"],
                "discriminator": { "propertyName": "type" },
                "description": "One spending method inside a batch. `attachment_part_names[]` cross-references multipart parts in the same request.",
                "properties": {
                    "type":                  { "type": "string", "enum": ["supplier-bill"] },
                    "bill":                  { "$ref": "#/components/schemas/SupplierBill" },
                    "attachment_part_names": {
                        "type": "array",
                        "maxItems": 5,
                        "uniqueItems": true,
                        "description": "Names of multipart parts (each `attachment-<token>`) that belong to this item. Each name must be uploaded as a file part and must not be referenced by any other item.",
                        "items": { "type": "string", "pattern": "^attachment-[A-Za-z0-9_-]{1,60}$" }
                    }
                },
                "allOf": [
                    {
                        "if": { "properties": { "type": { "const": "supplier-bill" } } },
                        "then": { "required": ["bill"] }
                    }
                ]
            },
            "SpendingMethodBatchRequest": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "items"],
                "properties": {
                    "schema_version": { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$", "description": "Body-level schema version. Bumped for additive payload changes." },
                    "items": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 100,
                        "items": { "$ref": "#/components/schemas/SpendingMethodItem" }
                    }
                },
                "example": {
                    "schema_version": "1.0.0",
                    "items": [
                        {
                            "type": "supplier-bill",
                            "attachment_part_names": ["attachment-acme-001"],
                            "bill": {
                                "supplier_account_number": "ACME-001",
                                "invoice_number": "INV-2026-0042",
                                "invoice_date": "2026-05-28",
                                "due_date": "2026-06-27",
                                "currency_code": "ZAR",
                                "narration": "Office stationery - May 2026",
                                "external_identifier": "019f0000-aaaa-7bbb-8ccc-1234567890ab",
                                "lines": [
                                    { "description": "Paper, A4",    "quantity": "5", "unit_amount": "100.00", "account_code": "5100", "vat201_rate_code": "20180401-15", "line_total": "500.00" },
                                    { "description": "Toner, black", "quantity": "1", "unit_amount": "750.00", "account_code": "5100", "vat201_rate_code": "20180401-15", "line_total": "750.00" }
                                ],
                                "subtotal":  "1250.00",
                                "vat_total": "187.50",
                                "total":     "1437.50"
                            }
                        }
                    ]
                }
            },
            "SpendingMethodItemResult": {
                "type": "object",
                "additionalProperties": false,
                "required": ["item_index", "status"],
                "description": "Outcome for a single item in the batch. Either `spending_method_identifier` (status='created') or `problem` (status='failed') is present.",
                "properties": {
                    "item_index": { "type": "integer", "minimum": 0, "description": "Zero-based index into the request `items[]` array." },
                    "status":     { "type": "string", "enum": ["created", "failed"] },
                    "spending_method_identifier": { "type": "string", "format": "uuid" },
                    "external_identifier":        { "type": "string", "format": "uuid", "description": "Echoed when the request supplied bill.external_identifier." },
                    "_links": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": ["self"],
                        "properties": { "self": { "type": "string" } }
                    },
                    "warnings": {
                        "type": "array",
                        "description": "Non-fatal advisories emitted during ingest. The bill was still persisted as a draft.",
                        "items": {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["code", "message"],
                            "properties": {
                                "code":             { "type": "string", "minLength": 1, "maxLength": 80 },
                                "message":          { "type": "string", "minLength": 1, "maxLength": 500 },
                                "line_index":       { "type": "integer", "minimum": 0 },
                                "vat201_rate_code": { "type": "string" }
                            }
                        }
                    },
                    "problem": { "$ref": "#/components/schemas/Problem" }
                }
            },
            "SpendingMethodBatchResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": ["batch_identifier", "schema_version", "submitted_count", "succeeded_count", "failed_count", "results"],
                "properties": {
                    "batch_identifier": { "type": "string", "format": "uuid", "description": "Server-assigned identifier for this submission. Useful for log correlation." },
                    "schema_version":   { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "submitted_count":  { "type": "integer", "minimum": 0 },
                    "succeeded_count":  { "type": "integer", "minimum": 0 },
                    "failed_count":     { "type": "integer", "minimum": 0 },
                    "results": {
                        "type": "array",
                        "items": { "$ref": "#/components/schemas/SpendingMethodItemResult" }
                    }
                }
            },
            "ReceivablesLine": {
                "type": "object",
                "additionalProperties": false,
                "required": ["order", "account_number", "description", "line_amount", "vat201_rate_code"],
                "properties": {
                    "order":              { "type": "integer", "minimum": 1, "description": "1-based position of the line within the document." },
                    "account_number":     { "type": "string", "minLength": 1, "maxLength": 50, "description": "Client-scoped account number; resolved server-side to account_code." },
                    "description":        { "type": "string", "minLength": 1, "maxLength": 250 },
                    "quantity":           { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "unit_price":         { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$" },
                    "line_amount":        { "type": "string", "pattern": "^-?\\d+(\\.\\d{1,4})?$", "description": "Net amount per line (VAT-exclusive)." },
                    "vat201_rate_code":   { "type": "string", "pattern": "^(\\d{8}-[A-Za-z0-9]+|[A-Za-z][A-Za-z0-9]*)$", "maxLength": 40, "description": "Public Mosaic VAT rate identifier. See SupplierBillLine.vat201_rate_code." },
                    "counterparty_identifier": { "type": "string", "format": "uuid", "description": "Optional per-line counterparty override (rare; default is the item-level counterparty)." }
                }
            },
            "ReceivablesItemBase": {
                "type": "object",
                "additionalProperties": false,
                "required": ["counterparty_identifier", "transaction_date", "transaction_currency", "narration", "lines"],
                "properties": {
                    "external_identifier":     { "type": "string", "format": "uuid", "description": "Optional partner-supplied UUID v7. Unique per hub_client_identifier; reuse returns 409 external-identifier-conflict." },
                    "counterparty_identifier": { "type": "string", "format": "uuid" },
                    "transaction_date":        { "type": "string", "format": "date" },
                    "due_date":                { "type": "string", "format": "date" },
                    "transaction_currency":    { "type": "string", "pattern": "^[A-Z]{3}$" },
                    "narration":               { "type": "string", "minLength": 1, "maxLength": 250 },
                    "purchase_order_number":   { "type": "string", "maxLength": 50 },
                    "document_number":         { "type": "string", "maxLength": 100, "description": "Optional client-supplied document number. When omitted the server allocates the next number using the per-source prefix." },
                    "post_status":             { "type": "string", "enum": ["draft", "committed"], "default": "draft" },
                    "lines":                   { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/ReceivablesLine" } }
                }
            },
            "InvoiceItem": {
                "allOf": [
                    { "$ref": "#/components/schemas/ReceivablesItemBase" }
                ]
            },
            "CreditNoteItem": {
                "allOf": [
                    { "$ref": "#/components/schemas/ReceivablesItemBase" },
                    {
                        "type": "object",
                        "properties": {
                            "originating_invoice_identifier": { "type": "string", "format": "uuid", "description": "Optional UUID of the invoice this credit note reverses. Must belong to the same client and have source='invoice'." }
                        }
                    }
                ]
            },
            "QuoteItem": {
                "allOf": [
                    { "$ref": "#/components/schemas/ReceivablesItemBase" }
                ]
            },
            "InvoiceBatchRequest": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "items"],
                "properties": {
                    "schema_version": { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "items": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "$ref": "#/components/schemas/InvoiceItem" } }
                }
            },
            "CreditNoteBatchRequest": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "items"],
                "properties": {
                    "schema_version": { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "items": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "$ref": "#/components/schemas/CreditNoteItem" } }
                }
            },
            "QuoteBatchRequest": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "items"],
                "properties": {
                    "schema_version": { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "items": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "$ref": "#/components/schemas/QuoteItem" } }
                }
            },
            "ReceivablesItemResult": {
                "type": "object",
                "additionalProperties": false,
                "required": ["index", "status"],
                "properties": {
                    "index":                          { "type": "integer", "minimum": 0 },
                    "status":                         { "type": "string", "enum": ["created", "failed"] },
                    "transaction_header_identifier":  { "type": "string", "format": "uuid" },
                    "external_identifier":            { "type": "string", "format": "uuid" },
                    "post_status_effective":          { "type": "string", "enum": ["draft", "committed"] },
                    "document_number":                { "type": "string" },
                    "_links": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": ["self"],
                        "properties": { "self": { "type": "string" } }
                    },
                    "warnings": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["code", "detail"],
                            "properties": {
                                "code":   { "type": "string", "minLength": 1, "maxLength": 80 },
                                "detail": { "type": "string", "minLength": 1, "maxLength": 500 }
                            }
                        }
                    },
                    "problem": { "$ref": "#/components/schemas/Problem" }
                }
            },
            "ReceivablesBatchResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "submitted_count", "succeeded_count", "failed_count", "results"],
                "properties": {
                    "schema_version":  { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "submitted_count": { "type": "integer", "minimum": 0 },
                    "succeeded_count": { "type": "integer", "minimum": 0 },
                    "failed_count":    { "type": "integer", "minimum": 0 },
                    "results":         { "type": "array", "items": { "$ref": "#/components/schemas/ReceivablesItemResult" } }
                }
            },
            "QuoteConvertRequest": {
                "type": "object",
                "additionalProperties": false,
                "required": ["schema_version", "invoice_date"],
                "properties": {
                    "schema_version": { "type": "string", "pattern": "^1\\.\\d+\\.\\d+$" },
                    "invoice_date":   { "type": "string", "format": "date" },
                    "due_date":       { "type": "string", "format": "date" },
                    "post_status":    { "type": "string", "enum": ["draft", "committed"], "default": "draft" }
                }
            },
            "QuoteConvertResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": ["invoice_identifier", "invoice_number", "quote_identifier", "post_status_effective"],
                "properties": {
                    "invoice_identifier":    { "type": "string", "format": "uuid" },
                    "invoice_number":        { "type": "string" },
                    "quote_identifier":      { "type": "string", "format": "uuid" },
                    "post_status_effective": { "type": "string", "enum": ["draft", "committed"] },
                    "_links": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": ["self"],
                        "properties": { "self": { "type": "string" } }
                    },
                    "warnings": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["code", "detail"],
                            "properties": {
                                "code":   { "type": "string" },
                                "detail": { "type": "string" }
                            }
                        }
                    }
                }
            }
        }
    }
}
