{"openapi":"3.1.0","info":{"title":"Remizo Engine API","version":"1.0.0","summary":"Runtime API of a Remizo workspace engine (spoke) — native endpoints, management endpoints used by the hub, and the WsOne-compatible adapter.","description":"One engine process serves exactly one workspace. It exposes:\n\n* **Native runtime API** (`/v1/...`) — authenticated by **API key** (`X-Api-Key`), used by cash registers, e-commerce and any integrator. Listens on the public port (`REMIZO_ENGINE_HTTP_ADDR`, default `:47500`).\n* **WsOne adapter** (`/rest/...`) — same port, same API key auth; drop-in replacement for the Beone WsOne transaction/person web services.\n* **Management API** (`/mgmt/v1/...`) + `/healthz`, `/readyz`, `/metrics` — listens on the management port (`REMIZO_ENGINE_MGMT_ADDR`, default `:47501`), authenticated by the shared engine token (`Authorization: Bearer <REMIZO_ENGINE_MGMT_TOKEN>`), only reachable by the hub on the workspace network.\n\nConventions (see `docs/architecture.md`):\n* All money is integer cents (int64). Percentages are basis points.\n* Errors are RFC 9457 `application/problem+json` (`Problem` schema).\n* Mutating endpoints accept `Idempotency-Key`; ticket endpoints are additionally idempotent on the **ticket key** `<storeCode>:<posId>:<businessDate>:<ticketNumber>`.\n* Every response carries `X-Request-Id`. Replayed responses carry `Idempotent-Replay: true`.\n","contact":{"name":"Remizo","url":"https://remizo.dev"},"license":{"name":"Proprietary"}},"servers":[{"url":"http://localhost:47500","description":"Local — workspace slot 0, engine 1 (public port)"},{"url":"http://localhost:47501","description":"Local — workspace slot 0, engine 1 (management port; /mgmt, /healthz, /readyz, /metrics only)"},{"url":"https://{workspace}.engine.remizo.dev","description":"Production pattern behind Traefik","variables":{"workspace":{"default":"acme"}}}],"tags":[{"name":"Transactions","description":"Simulate, evaluate (pending), commit, cancel, refund tickets."},{"name":"Points","description":"Counter balances and burn with reserve / confirm / cancel."},{"name":"Coupons","description":"Check and redeem coupons outside of a ticket."},{"name":"Customers","description":"Lookup, balances, available offers, coupons."},{"name":"WsOne","description":"Beone WsOne-compatible endpoints. Field names are those of WsOne; amounts are decimal (not cents) in this adapter."},{"name":"Management","description":"Hub-only endpoints on the management port."},{"name":"Ops","description":"Health, readiness, metrics."}],"security":[{"ApiKey":[]}],"paths":{"/v1/transactions/simulate":{"post":{"tags":["Transactions"],"operationId":"simulateTransaction","summary":"Simulate a ticket (no side effect)","description":"Evaluates all published offers against the ticket. Nothing is persisted; limits are checked but not consumed; coupons are checked but not reserved. Equivalent of WsOne `verifyTransaction`.","parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"$ref":"#/components/requestBodies/Ticket"},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/transactions/evaluate":{"post":{"tags":["Transactions"],"operationId":"evaluateTransaction","summary":"Evaluate a ticket and store it as pending","description":"Evaluates the ticket, persists a **pending** transaction (TTL `transactions.pendingTtlSeconds`, default 24h) and returns `transactionId`.\nCoupons presented are **reserved**, offer limits are **soft-reserved** (see architecture). Nothing is posted to the ledger until `commit`.\nIdempotent on the ticket key: re-sending the same ticket key returns the stored result (`Idempotent-Replay: true`); sending a *different* body with the same ticket key while pending re-evaluates and replaces it.\nEquivalent of WsOne `sendTransaction`.\n","parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"$ref":"#/components/requestBodies/Ticket"},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Ticket key already committed (cannot re-evaluate a committed ticket).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/transactions/commit":{"post":{"tags":["Transactions"],"operationId":"evaluateAndCommitTransaction","summary":"Evaluate and commit in one call","description":"Shortcut for `evaluate` + `commit`. Use when the POS cannot do a two-phase call. Equivalent of WsOne `sendAndValidTransaction`.","parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"$ref":"#/components/requestBodies/Ticket"},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/transactions/{transactionId}":{"parameters":[{"$ref":"#/components/parameters/TransactionId"}],"get":{"tags":["Transactions"],"operationId":"getTransaction","summary":"Get a transaction (ticket + result + state)","responses":{"200":{"description":"Transaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/transactions/by-key/{ticketKey}":{"get":{"tags":["Transactions"],"operationId":"getTransactionByKey","summary":"Get a transaction by ticket key","parameters":[{"name":"ticketKey","in":"path","required":true,"schema":{"type":"string"},"description":"`<storeCode>:<posId>:<businessDate>:<ticketNumber>` (URL-encoded)."}],"responses":{"200":{"description":"Transaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/transactions/{transactionId}/commit":{"post":{"tags":["Transactions"],"operationId":"commitTransaction","summary":"Commit a pending transaction","description":"Makes the evaluation final: posts ledger movements (earn, burn confirmation), marks redeemed coupons `used`, creates issued coupons (`available`, render job queued), consumes offer limits, emits `transaction.committed` to RabbitMQ.\nOptionally accepts the final `payments` (the POS knows them only after payment) — payments are **not** re-evaluated except for `payment_method` criteria when `reevaluateOnPayments` is true (workspace setting).\nEquivalent of WsOne `validTransaction`. Idempotent: committing an already committed transaction returns the same result with `Idempotent-Replay: true`.\n","parameters":[{"$ref":"#/components/parameters/TransactionId"},{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Transaction is cancelled or expired, or a coupon reserved by it was consumed elsewhere (problem `type` ends with `/transaction-not-committable`; `result` is omitted).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/v1/transactions/{transactionId}/cancel":{"post":{"tags":["Transactions"],"operationId":"cancelTransaction","summary":"Cancel a transaction","description":"* On a **pending** transaction: releases reserved coupons and soft limits, state → `cancelled`.\n* On a **committed** transaction (within `transactions.cancelWindowSeconds`, default 24h, and only if no refund exists): reverses every effect (ledger compensation movements, coupons issued → `cancelled`, redeemed coupons → `available`, limits given back), state → `cancelled`, emits `transaction.cancelled`. Later than the window → use `refund`.\n","parameters":[{"$ref":"#/components/parameters/TransactionId"},{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/v1/transactions/{transactionId}/refund":{"post":{"tags":["Transactions"],"operationId":"refundTransaction","summary":"Refund a committed transaction, totally or partially","description":"Creates a child transaction (`kind: refund`, `parentTransactionId`) that reverses the effects pro rata of the refunded lines:\ndiscounts are recomputed by removing the refunded units (the POS receives the discount to give back per line), earned counter units are clawed back pro rata (negative `earn` movements, may drive the balance negative if `counterType.allowNegative`, otherwise clamped and a warning is emitted), coupons issued by the original ticket are cancelled when `cancelIssuedCoupons` is true.\nFull refund = omit `lines`. Multiple partial refunds are allowed until the quantities are exhausted. Equivalent of WsOne `refundTransaction`.\n","parameters":[{"$ref":"#/components/parameters/TransactionId"},{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/v1/transactions/cancel-pending":{"post":{"tags":["Transactions"],"operationId":"cancelPendingTransactions","summary":"Cancel all pending transactions of a store / POS","description":"Used at POS close or after a crash. Equivalent of WsOne `cancelTransactionsNotValidated`.","parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["storeCode"],"properties":{"storeCode":{"type":"string"},"posId":{"type":"string","description":"Omit for the whole store."},"olderThanSeconds":{"type":"integer","minimum":0,"default":0}}}}}},"responses":{"200":{"description":"Count of cancelled transactions","content":{"application/json":{"schema":{"type":"object","required":["cancelled"],"properties":{"cancelled":{"type":"integer"},"transactionIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}}}},"/v1/points/reserve":{"post":{"tags":["Points"],"operationId":"reservePoints","summary":"Reserve counter units for a payment (step 1 of burn)","description":"Checks `available >= amount` and creates a **reservation** (ledger movement kind `reserve`, state `reserved`). The amount is deducted from `available` but not from `total` until confirmation.\nReservations expire after `ttlSeconds` (default workspace `points.reservationTtlSeconds`, 900 s) via the `points.reservations.expire` cron. Idempotent on `Idempotency-Key`.\n","parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReserveRequest"}}}},"responses":{"201":{"description":"Reservation created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Insufficient available balance (`type` ends with `/insufficient-balance`, extension `available`).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/v1/points/reservations/{reservationId}":{"parameters":[{"$ref":"#/components/parameters/ReservationId"}],"get":{"tags":["Points"],"operationId":"getReservation","summary":"Get a reservation","responses":{"200":{"description":"Reservation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/points/reservations/{reservationId}/confirm":{"post":{"tags":["Points"],"operationId":"confirmReservation","summary":"Confirm a reservation (step 2 — burn)","description":"Posts a `burn` movement for the reserved amount (or a smaller `amount` if provided; the remainder is released), state → `confirmed`. Idempotent.","parameters":[{"$ref":"#/components/parameters/ReservationId"},{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"description":"Partial confirmation; must be <= reserved amount."},"ticketKey":{"type":"string"},"transactionId":{"type":"string","format":"uuid"},"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Reservation cancelled or expired.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/points/reservations/{reservationId}/cancel":{"post":{"tags":["Points"],"operationId":"cancelReservation","summary":"Cancel a reservation (release)","description":"Posts a `release` movement, state → `cancelled`. Idempotent; cancelling a confirmed reservation is a 409.","parameters":[{"$ref":"#/components/parameters/ReservationId"},{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/v1/coupons/check":{"post":{"tags":["Coupons"],"operationId":"checkCoupon","summary":"Check a coupon code","description":"Returns the coupon state and the offer it unlocks without changing anything.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string"},"customer":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"}}}}}},"responses":{"200":{"description":"Coupon status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponCheck"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/coupons/redeem":{"post":{"tags":["Coupons"],"operationId":"redeemCoupon","summary":"Redeem a coupon outside of a ticket","description":"For coupons whose value is not a ticket discount (e.g. gift, points coupon) or for web flows that do not send a ticket. State → `used`; if the coupon's linked offer has `points_*` actions they are applied to the customer. Idempotent on `Idempotency-Key`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string"},"customer":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"reference":{"type":"string","maxLength":128,"description":"Order id or external reference."},"storeCode":{"type":"string"},"channel":{"$ref":"#/components/schemas/Ticket/$defs/Channel"}}}}}},"responses":{"200":{"description":"Redeemed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponCheck"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Coupon not redeemable (used, expired, reserved, not owned).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/customers/lookup":{"post":{"tags":["Customers"],"operationId":"lookupCustomer","summary":"Resolve a customer from an identifier and return balances and available offers","description":"Used by the POS when a card is scanned before the ticket. Equivalent of WsOne `getAccounts` + `getAvailableOffers`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["identifier"],"properties":{"identifier":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"storeCode":{"type":"string","description":"Filters available offers by store criteria."},"channel":{"$ref":"#/components/schemas/Ticket/$defs/Channel"},"autoCreateCard":{"type":"boolean","default":false},"include":{"type":"array","items":{"type":"string","enum":["balances","offers","coupons","profile"]},"default":["balances","offers"]}}}}}},"responses":{"200":{"description":"Customer context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerContext"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/customers/{customerId}/balances":{"get":{"tags":["Customers"],"operationId":"getCustomerBalances","summary":"Balances of all counters","parameters":[{"$ref":"#/components/parameters/CustomerId"}],"responses":{"200":{"description":"Balances","content":{"application/json":{"schema":{"type":"object","required":["customerId","balances"],"properties":{"customerId":{"type":"string","format":"uuid"},"balances":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/Balance"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/customers/{customerId}/offers":{"get":{"tags":["Customers"],"operationId":"getCustomerAvailableOffers","summary":"Offers the customer can currently benefit from","description":"Published offers whose customer-scoped criteria (segment, status, usage limits, coupon ownership…) are satisfied; ticket-dependent criteria are ignored.","parameters":[{"$ref":"#/components/parameters/CustomerId"},{"name":"storeCode","in":"query","schema":{"type":"string"}},{"name":"channel","in":"query","schema":{"$ref":"#/components/schemas/Ticket/$defs/Channel"}}],"responses":{"200":{"description":"Available offers","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AvailableOffer"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/customers/{customerId}/coupons":{"get":{"tags":["Customers"],"operationId":"getCustomerCoupons","summary":"Coupons owned by the customer","parameters":[{"$ref":"#/components/parameters/CustomerId"},{"name":"state","in":"query","schema":{"type":"array","items":{"$ref":"#/components/schemas/CouponState"}},"style":"form","explode":false,"description":"Default `available,reserved`."}],"responses":{"200":{"description":"Coupons","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CouponCheck"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/events":{"post":{"tags":["Customers"],"operationId":"ingestCustomerEvent","summary":"Ingest a customer event (signup, birthday, custom…) and run event-triggered offers","description":"Stores the event (`customer_events`) and evaluates offers with `trigger.type = event`. Effects (points, coupons, tags) are committed immediately — there is no pending phase for events. The hub sync connectors call this for CRM-originated events; the `customers.birthdays` cron emits `birthday` events itself.\nIdempotent on `Idempotency-Key` (recommended: `<type>:<customerExternalRef>:<date>`).\n","parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerEventRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/v1/actions":{"post":{"tags":["Customers"],"operationId":"rewardCustomerAction","summary":"Reward an off-till customer action (review, referral, profile completed…)","description":"Records the action as a `custom` customer event whose `customType` is the action code, and runs the offers triggered by it (the studio's rewarded actions create them). Effects are committed immediately.\nThe `reference` (review id, referral id…) is unique per customer and action: sending it again replays the first result and never rewards twice. How many times a customer can be rewarded is the offer's per-customer limit (`rejected[].reason = per_customer_limit_reached` once reached).\n","parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewardActionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/rest/transactionWebService/verifyTransaction":{"post":{"tags":["WsOne"],"operationId":"wsoneVerifyTransaction","summary":"WsOne verifyTransaction — simulate","description":"Maps to `simulateTransaction`. Nothing persisted.","requestBody":{"$ref":"#/components/requestBodies/WsOneTransaction"},"responses":{"200":{"$ref":"#/components/responses/WsOneTransaction"}}}},"/rest/transactionWebService/sendTransaction":{"post":{"tags":["WsOne"],"operationId":"wsoneSendTransaction","summary":"WsOne sendTransaction — evaluate, pending","description":"Maps to `evaluateTransaction`. The transaction is identified later by (`EntityRequest.entityIdentifier`, `cashdeskNumber`, `receiptNumber`, `transactionDate`).","requestBody":{"$ref":"#/components/requestBodies/WsOneTransaction"},"responses":{"200":{"$ref":"#/components/responses/WsOneTransaction"}}}},"/rest/transactionWebService/validTransaction":{"post":{"tags":["WsOne"],"operationId":"wsoneValidTransaction","summary":"WsOne validTransaction — commit","description":"Maps to `commitTransaction`. Body carries only `ReceiptRequest` (identification + final amounts); `DetailsRequest` is ignored if present.","requestBody":{"$ref":"#/components/requestBodies/WsOneTransaction"},"responses":{"200":{"$ref":"#/components/responses/WsOneTransaction"}}}},"/rest/transactionWebService/sendAndValidTransaction":{"post":{"tags":["WsOne"],"operationId":"wsoneSendAndValidTransaction","summary":"WsOne sendAndValidTransaction — evaluate + commit","description":"Maps to `evaluateAndCommitTransaction`.","requestBody":{"$ref":"#/components/requestBodies/WsOneTransaction"},"responses":{"200":{"$ref":"#/components/responses/WsOneTransaction"}}}},"/rest/transactionWebService/refundTransaction":{"post":{"tags":["WsOne"],"operationId":"wsoneRefundTransaction","summary":"WsOne refundTransaction — total or partial refund","description":"Maps to `refundTransaction`. `ReceiptRequest.originalReceiptNumber` + `originalTransactionDate` + `cashdeskNumber` identify the original ticket; `DetailsRequest.TransactionItemRequest[]` lists the refunded items (quantities positive). Without `DetailsRequest` the refund is total.\n","requestBody":{"$ref":"#/components/requestBodies/WsOneTransaction"},"responses":{"200":{"$ref":"#/components/responses/WsOneTransaction"}}}},"/rest/transactionWebService/cancelTransactionsNotValidated":{"post":{"tags":["WsOne"],"operationId":"wsoneCancelTransactionsNotValidated","summary":"WsOne cancelTransactionsNotValidated","description":"Maps to `cancelPendingTransactions` for the store (`EntityRequest.entityIdentifier`) and optionally one `cashdeskNumber`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["EntityRequest"],"properties":{"EntityRequest":{"$ref":"#/components/schemas/WsOneEntityRequest"},"cashdeskNumber":{"type":"string"}}}}}},"responses":{"200":{"description":"Result","content":{"application/json":{"schema":{"type":"object","required":["responseCode","infos"],"properties":{"responseCode":{"$ref":"#/components/schemas/WsOneResponseCode"},"infos":{"type":"string"},"nbCancelled":{"type":"integer"}}}}}}}}},"/rest/2.0/personWebService/getAvailableOffers":{"post":{"tags":["WsOne"],"operationId":"wsoneGetAvailableOffers","summary":"WsOne getAvailableOffers","description":"Maps to `getCustomerAvailableOffers` after identifier resolution.","requestBody":{"$ref":"#/components/requestBodies/WsOnePerson"},"responses":{"200":{"description":"Offers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsOneGetAvailableOffersResponse"}}}}}}},"/rest/2.0/personWebService/getAccounts":{"post":{"tags":["WsOne"],"operationId":"wsoneGetAccounts","summary":"WsOne getAccounts","description":"Maps to `getCustomerBalances` after identifier resolution. One `AccountResponse` per counter type.","requestBody":{"$ref":"#/components/requestBodies/WsOnePerson"},"responses":{"200":{"description":"Accounts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsOneGetAccountsResponse"}}}}}}},"/mgmt/v1/info":{"get":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtInfo","summary":"Engine identity and configuration summary","responses":{"200":{"description":"Info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineInfo"}}}}}}},"/mgmt/v1/reload":{"post":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtReload","summary":"Reload offers / API keys / settings from the workspace DB","description":"Also triggered by RabbitMQ control messages; this endpoint is the synchronous fallback used by the hub right after publishing (returns once the new snapshot is live).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","enum":["offers","api_keys","settings","counter_types","segments","product_lists","stores","all"]},"default":["all"]},"offerIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Reload only these offers (publish/unpublish of a single offer)."}}}}}},"responses":{"200":{"description":"Reloaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReloadResult"}}}}}}},"/mgmt/v1/stats":{"get":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtStats","summary":"Runtime statistics (counters since start + last 1/5/15 min windows)","responses":{"200":{"description":"Stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineStats"}}}}}}},"/mgmt/v1/offers":{"get":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtLoadedOffers","summary":"Offers currently compiled in memory","responses":{"200":{"description":"Loaded offers","content":{"application/json":{"schema":{"type":"object","required":["loadedAt","items"],"properties":{"loadedAt":{"type":"string","format":"date-time"},"snapshotId":{"type":"string"},"items":{"type":"array","items":{"type":"object","required":["offerId","code","version","level","mode","validFrom"],"properties":{"offerId":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer"},"level":{"type":"integer"},"mode":{"type":"string"},"priority":{"type":"integer"},"validFrom":{"type":"string","format":"date-time"},"validTo":{"type":["string","null"],"format":"date-time"},"compiledAt":{"type":"string","format":"date-time"},"limitsRemaining":{"type":"object","additionalProperties":{"type":"integer"}}}}}}}}}}}}},"/mgmt/v1/simulate":{"post":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtSimulate","summary":"Simulate with draft offer definitions (studio simulator)","description":"Evaluates the ticket against the given offer documents (any status, compiled on the fly) merged with the published set when `includePublished` is true. Used by the hub `POST /v1/workspaces/{ws}/simulator/evaluate`. Never persists anything and returns full `criteriaTrace`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ticket"],"properties":{"ticket":{"$ref":"#/components/schemas/Ticket"},"offers":{"type":"array","items":{"$ref":"#/components/schemas/Offer"}},"includePublished":{"type":"boolean","default":true},"at":{"type":"string","format":"date-time","description":"Evaluate as if now were this instant."},"customerOverride":{"type":"object","description":"Fake customer context for what-if scenarios (segments, status, balances).","additionalProperties":true}}}}}},"responses":{"200":{"$ref":"#/components/responses/EvaluationResult"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/mgmt/v1/cel/validate":{"post":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtValidateCel","summary":"Type-check a CEL expression against the engine environment","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["expression","scope"],"properties":{"expression":{"type":"string"},"scope":{"type":"string","enum":["ticket","line","mapping"]}}}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","required":["valid"],"properties":{"valid":{"type":"boolean"},"resultType":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"},"line":{"type":"integer"},"column":{"type":"integer"}}}}}}}}}}}},"/mgmt/v1/drain":{"post":{"tags":["Management"],"security":[{"EngineToken":[]}],"operationId":"mgmtDrain","summary":"Stop accepting new requests (readiness → 503) before a restart","responses":{"202":{"description":"Draining"}}}},"/healthz":{"get":{"tags":["Ops"],"security":[],"operationId":"healthz","summary":"Liveness","responses":{"200":{"description":"Alive","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"ok"}}}}}}}}},"/readyz":{"get":{"tags":["Ops"],"security":[],"operationId":"readyz","summary":"Readiness (DB reachable, offers loaded, not draining)","responses":{"200":{"description":"Ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}},"503":{"description":"Not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}}}}},"/metrics":{"get":{"tags":["Ops"],"security":[],"operationId":"metrics","summary":"Prometheus metrics (text exposition format)","responses":{"200":{"description":"Metrics","content":{"text/plain":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Workspace API key `rmz_<env>_<keyId>_<secret>` (`env` = `live` | `test`). Verified locally by the engine against `api_keys.secret_hash` (SHA-256 of the full key string), cached in memory, refreshed by control message `apikeys.changed`.\nScopes: `transactions:write`, `transactions:read`, `points:write`, `points:read`, `coupons:write`, `coupons:read`, `customers:read`, `events:write`, `wsone`. A key may be restricted to store codes (`api_keys.store_codes`).\n"},"EngineToken":{"type":"http","scheme":"bearer","description":"Shared secret `REMIZO_ENGINE_MGMT_TOKEN` (from OpenBao `mo/<ws>/engine`), hub only, management port only."}},"parameters":{"RequestId":{"name":"X-Request-Id","in":"header","required":false,"schema":{"type":"string","maxLength":128},"description":"Caller-supplied correlation id, echoed back; generated when absent."},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Stored 48h with the request hash and response. Same key + same body → replay; same key + different body → 422 `idempotency-key-reused`."},"TransactionId":{"name":"transactionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"ReservationId":{"name":"reservationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"CustomerId":{"name":"customerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}},"requestBodies":{"Ticket":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"},"examples":{"simple":{"summary":"Identified customer, two lines, one coupon","value":{"header":{"ticketNumber":"000123","storeCode":"S042","posId":"03","businessDate":"2026-11-09","timestamp":"2026-11-09T10:42:13+01:00","channel":"pos","currency":"EUR","totals":{"grossCents":5490,"itemCount":3}},"customer":{"identifiers":[{"type":"card","value":"2600000012345"}]},"lines":[{"lineNo":1,"ean":"3017620422003","ref":"NUT750","name":"Pâte à tartiner 750g","category":"EPIC-PAT","rayon":"EPICERIE","univers":"ALIM","qty":2,"unitPriceCents":495,"lineTotalCents":990},{"lineNo":2,"ean":"4006381333931","ref":"STB001","name":"Stylo bille","category":"PAP-STY","rayon":"PAPETERIE","univers":"MAISON","qty":1,"unitPriceCents":4500,"lineTotalCents":4500}],"payments":[],"coupons":[{"code":"2609000001234","source":"scanned"}]}}}}}},"WsOneTransaction":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsOneTransactionRequest"}}}},"WsOnePerson":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["IdentifierRequest"],"properties":{"IdentifierRequest":{"$ref":"#/components/schemas/WsOneIdentifierRequest"},"EntityRequest":{"$ref":"#/components/schemas/WsOneEntityRequest"}}}}}}},"responses":{"EvaluationResult":{"description":"Evaluation result","headers":{"X-Request-Id":{"schema":{"type":"string"}},"Idempotent-Replay":{"schema":{"type":"boolean"},"description":"Present and true when replayed."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluationResult"}}}},"WsOneTransaction":{"description":"WsOne response. HTTP status is always 200; errors are carried by `responseCode` (WsOne convention).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsOneTransactionResponse"}}}},"BadRequest":{"description":"Malformed request (JSON / schema).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Forbidden":{"description":"Key lacks the scope or the store is not allowed for this key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"NotFound":{"description":"Resource not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Conflict":{"description":"State conflict.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Unprocessable":{"description":"Semantically invalid (unknown store, currency mismatch, duplicate lineNo…). `errors[]` lists field-level problems.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"TooManyRequests":{"description":"Rate limit for this API key.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Unavailable":{"description":"Engine draining or DB unreachable.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"schemas":{"Problem":{"type":"object","description":"RFC 9457 problem details. `type` is `https://remizo.dev/problems/<code>`; `code` repeats the last path segment for programmatic use.","required":["type","title","status","code"],"properties":{"type":{"type":"string","format":"uri","example":"https://remizo.dev/problems/validation-failed"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string","description":"Request path."},"code":{"type":"string","enum":["validation-failed","unauthorized","forbidden-scope","forbidden-store","not-found","idempotency-key-reused","ticket-key-committed","transaction-not-committable","transaction-not-cancellable","transaction-not-refundable","insufficient-balance","reservation-not-confirmable","reservation-not-cancellable","coupon-not-redeemable","customer-unknown","customer-blocked","unknown-store","currency-mismatch","rate-limited","engine-unavailable","internal"]},"traceId":{"type":"string"},"errors":{"type":"array","items":{"type":"object","required":["path","message"],"properties":{"path":{"type":"string","example":"/lines/2/qty"},"message":{"type":"string"},"rule":{"type":"string"}}}}},"additionalProperties":true},"CommitRequest":{"type":"object","properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/Ticket/$defs/Payment"}},"totals":{"$ref":"#/components/schemas/Ticket/$defs/Totals","description":"Final POS totals after applying Remizo discounts (used only for the coherence warning and stats)."},"pointsReservationId":{"type":"string","format":"uuid","description":"Reservation to confirm atomically with the commit."},"cashierId":{"type":"string"}}},"RefundRequest":{"type":"object","properties":{"refundTicketNumber":{"type":"string","description":"Receipt number of the refund ticket (becomes the child transaction ticket key with the same store/pos)."},"businessDate":{"type":"string","format":"date"},"timestamp":{"type":"string","format":"date-time"},"lines":{"type":"array","description":"Omit for a full refund.","items":{"type":"object","required":["lineNo","qty"],"properties":{"lineNo":{"type":"integer","minimum":1},"qty":{"type":"number","exclusiveMinimum":0}}}},"clawbackPoints":{"type":"boolean","default":true},"cancelIssuedCoupons":{"type":"boolean","default":true,"description":"Only coupons still `available`."},"reason":{"type":"string","maxLength":200}}},"Transaction":{"type":"object","required":["transactionId","ticketKey","kind","state","createdAt","ticket","result"],"properties":{"transactionId":{"type":"string","format":"uuid"},"ticketKey":{"type":"string"},"kind":{"type":"string","enum":["sale","refund"]},"state":{"type":"string","enum":["pending","committed","cancelled","refunded","partially_refunded","expired"]},"parentTransactionId":{"type":["string","null"],"format":"uuid"},"adapter":{"type":"string","enum":["native","wsone"]},"apiKeyId":{"type":"string"},"customerId":{"type":["string","null"],"format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"committedAt":{"type":["string","null"],"format":"date-time"},"cancelledAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"Pending TTL."},"ticket":{"$ref":"#/components/schemas/Ticket"},"result":{"$ref":"#/components/schemas/EvaluationResult"},"refunds":{"type":"array","items":{"type":"object","properties":{"transactionId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"refundedCents":{"type":"integer"}}}}}},"CustomerEventRequest":{"type":"object","required":["type","customer","occurredAt"],"properties":{"type":{"type":"string","enum":["signup","card_created","birthday","anniversary_signup","status_changed","segment_entered","segment_left","counter_threshold","custom"]},"customType":{"type":"string","maxLength":64,"description":"Required when type = custom."},"customer":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"occurredAt":{"type":"string","format":"date-time"},"storeCode":{"type":"string"},"channel":{"$ref":"#/components/schemas/Ticket/$defs/Channel"},"payload":{"type":"object","additionalProperties":true},"source":{"type":"string","enum":["api","sync","cron","engine"],"default":"api"}}},"RewardActionRequest":{"type":"object","required":["action","reference","customer"],"properties":{"action":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","description":"Action code (e.g. REVIEW)."},"reference":{"type":"string","minLength":1,"maxLength":128,"description":"Unique per customer and action (review id, referral id…)."},"customer":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"occurredAt":{"type":"string","format":"date-time","description":"Defaults to now."},"storeCode":{"type":"string"},"channel":{"$ref":"#/components/schemas/Ticket/$defs/Channel"},"payload":{"type":"object","additionalProperties":true}}},"ReserveRequest":{"type":"object","required":["customer","counterTypeCode","amount"],"properties":{"customer":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"counterTypeCode":{"type":"string"},"amount":{"type":"integer","minimum":1},"ttlSeconds":{"type":"integer","minimum":30,"maximum":86400},"ticketKey":{"type":"string","description":"Ticket this reservation will pay."},"storeCode":{"type":"string"},"reason":{"type":"string","maxLength":200}}},"Reservation":{"type":"object","required":["reservationId","customerId","counterTypeCode","amount","state","createdAt","expiresAt","balance"],"properties":{"reservationId":{"type":"string","format":"uuid"},"customerId":{"type":"string","format":"uuid"},"counterTypeCode":{"type":"string"},"amount":{"type":"integer"},"confirmedAmount":{"type":["integer","null"]},"state":{"type":"string","enum":["reserved","confirmed","cancelled","expired"]},"ticketKey":{"type":["string","null"]},"transactionId":{"type":["string","null"],"format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"valueCents":{"type":["integer","null"],"description":"Monetary value when the counter type has a conversion rate."},"balance":{"$ref":"#/components/schemas/EvaluationResult/$defs/Balance"}}},"CouponState":{"type":"string","enum":["available","reserved","used","expired","cancelled"]},"CouponCheck":{"type":"object","required":["code","found"],"properties":{"code":{"type":"string"},"found":{"type":"boolean"},"couponId":{"type":["string","null"],"format":"uuid"},"state":{"$ref":"#/components/schemas/CouponState"},"redeemable":{"type":"boolean"},"rejectReason":{"type":["string","null"],"enum":["unknown","expired","not_yet_valid","already_used","reserved_elsewhere","cancelled","not_owned","no_matching_offer",null]},"format":{"type":"string","enum":["ean13","qr","short"]},"templateId":{"type":["string","null"],"format":"uuid"},"templateName":{"type":"string"},"campaignId":{"type":["string","null"],"format":"uuid"},"customerId":{"type":["string","null"],"format":"uuid"},"linkedOfferId":{"type":["string","null"],"format":"uuid"},"linkedOfferName":{"type":"string"},"faceValue":{"$ref":"#/components/schemas/EvaluationResult/$defs/CouponIssued/properties/faceValue"},"validFrom":{"type":"string","format":"date-time"},"validTo":{"type":"string","format":"date-time"},"usedAt":{"type":["string","null"],"format":"date-time"},"reusable":{"type":"boolean"},"usesLeft":{"type":["integer","null"]},"render":{"$ref":"#/components/schemas/EvaluationResult/$defs/CouponIssued/properties/render"}}},"AvailableOffer":{"type":"object","required":["offerId","code","name","kind"],"properties":{"offerId":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"kind":{"type":"string","enum":["discount","points","coupon","mixed","message"]},"validTo":{"type":["string","null"],"format":"date-time"},"requiresCoupon":{"type":"boolean"},"couponCode":{"type":["string","null"],"description":"Customer-owned coupon unlocking it, if any."},"summary":{"type":"string","description":"Marketing summary generated from actions, e.g. \"-10% dès 50 €\"."},"usesLeftForCustomer":{"type":["integer","null"]}}},"CustomerContext":{"type":"object","required":["customer"],"properties":{"customer":{"$ref":"#/components/schemas/EvaluationResult/$defs/CustomerResolution"},"profile":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"birthDate":{"type":["string","null"],"format":"date"},"signupAt":{"type":["string","null"],"format":"date-time"},"storeCode":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}}}},"balances":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/Balance"}},"offers":{"type":"array","items":{"$ref":"#/components/schemas/AvailableOffer"}},"coupons":{"type":"array","items":{"$ref":"#/components/schemas/CouponCheck"}}}},"WsOneResponseCode":{"type":"string","description":"`000` OK · `001` unknown identifier (customer/card) · `002` invalid request · `003` duplicate receipt (already validated) · `004` transaction not found · `005` transaction not validatable (cancelled/expired) · `006` unknown entity (store) · `007` insufficient account balance · `100` engine unavailable · `999` internal error.\n","enum":["000","001","002","003","004","005","006","007","100","999"]},"WsOneIdentifierType":{"type":"string","description":"`1` card · `10` web account · `30` wallet · `99` prospect.","enum":["1","10","30","99"]},"WsOneEntityRequest":{"type":"object","required":["entityIdentifier"],"properties":{"entityIdentifier":{"type":"string","description":"Store code (mapped to ticket.header.storeCode through the adapter mapping)."}}},"WsOneEntityResponse":{"type":"object","required":["entityIdentifier"],"properties":{"entityIdentifier":{"type":"string"},"entityName":{"type":"string"}}},"WsOneIdentifierRequest":{"type":"object","required":["identifierType","identifierValue"],"properties":{"identifierType":{"$ref":"#/components/schemas/WsOneIdentifierType"},"identifierValue":{"type":"string"}}},"WsOneAccountAdvantageRequest":{"type":"object","description":"An account (counter) the customer wishes to use on this receipt, e.g. pay with points.","required":["name","value"],"properties":{"name":{"type":"string","description":"Account name = counter type code (e.g. POINTS, CASHBACK)."},"value":{"type":"number","description":"Units to use."},"unit":{"type":"string"}}},"WsOneReceiptRequest":{"type":"object","required":["allTaxesIncludedAmount","currency","cashdeskNumber","receiptNumber","transactionDate","EntityRequest"],"properties":{"allTaxesIncludedAmount":{"type":"number","description":"Decimal amount (e.g. 54.90) → totals.grossCents."},"preTaxAmount":{"type":"number"},"discountAmount":{"type":"number","description":"POS-side discounts already applied → totals.posDiscountCents."},"currency":{"type":"string","example":"EUR"},"cashdeskIpAddress":{"type":"string"},"cashdeskNumber":{"type":"string","description":"→ header.posId"},"receiptNumber":{"type":"string","description":"→ header.ticketNumber"},"transactionDate":{"type":"string","pattern":"^\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}$","description":"Format `DD/MM/YYYY HH24:MI:SS`, store local time → header.timestamp + businessDate.","example":"09/11/2026 10:42:13"},"originalReceiptNumber":{"type":"string","description":"refundTransaction only."},"originalTransactionDate":{"type":"string","description":"refundTransaction only, same format."},"EntityRequest":{"$ref":"#/components/schemas/WsOneEntityRequest"},"IdentifierRequest":{"$ref":"#/components/schemas/WsOneIdentifierRequest"},"AccountAdvantageRequest":{"type":"array","items":{"$ref":"#/components/schemas/WsOneAccountAdvantageRequest"}}}},"WsOneOfferNumberRequest":{"type":"object","required":["offerNumber"],"properties":{"offerNumber":{"type":"string","description":"Coupon code or offer code presented at the till → ticket.coupons[].code."}}},"WsOneTransactionItemRequest":{"type":"object","required":["quantity","allTaxesIncludedUnitPrice"],"properties":{"allTaxesIncludedUnitPrice":{"type":"number","description":"→ line.unitPriceCents"},"discountAmount":{"type":"number","description":"→ line.posDiscountCents"},"ean":{"type":"string"},"mainReference":{"type":"string","description":"→ line.ref"},"name":{"type":"string"},"preTaxUnitPrice":{"type":"number"},"quantity":{"type":"number"},"storeReference":{"type":"string","description":"Store-level reference; kept in line.attributes.storeReference."},"universReference":{"type":"string","description":"→ line.univers (rayon/category derived through adapter mapping when a hierarchy is configured)."}}},"WsOneDetailsRequest":{"type":"object","properties":{"nbItems":{"type":"integer"},"nbOfferNumbers":{"type":"integer"},"OfferNumberRequest":{"type":"array","items":{"$ref":"#/components/schemas/WsOneOfferNumberRequest"}},"TransactionItemRequest":{"type":"array","items":{"$ref":"#/components/schemas/WsOneTransactionItemRequest"}}}},"WsOneTransactionRequest":{"type":"object","required":["ReceiptRequest"],"properties":{"ReceiptRequest":{"$ref":"#/components/schemas/WsOneReceiptRequest"},"DetailsRequest":{"$ref":"#/components/schemas/WsOneDetailsRequest"}},"example":{"ReceiptRequest":{"allTaxesIncludedAmount":54.9,"preTaxAmount":45.75,"discountAmount":0,"currency":"EUR","cashdeskIpAddress":"10.42.0.3","cashdeskNumber":"03","receiptNumber":"000123","transactionDate":"09/11/2026 10:42:13","EntityRequest":{"entityIdentifier":"S042"},"IdentifierRequest":{"identifierType":"1","identifierValue":"2600000012345"},"AccountAdvantageRequest":[]},"DetailsRequest":{"nbItems":2,"nbOfferNumbers":1,"OfferNumberRequest":[{"offerNumber":"2609000001234"}],"TransactionItemRequest":[{"allTaxesIncludedUnitPrice":4.95,"discountAmount":0,"ean":"3017620422003","mainReference":"NUT750","name":"Pâte à tartiner 750g","preTaxUnitPrice":4.69,"quantity":2,"storeReference":"S042-NUT750","universReference":"ALIM"},{"allTaxesIncludedUnitPrice":45,"discountAmount":0,"ean":"4006381333931","mainReference":"STB001","name":"Stylo bille","preTaxUnitPrice":37.5,"quantity":1,"storeReference":"S042-STB001","universReference":"MAISON"}]}}},"WsOneAccountResponse":{"type":"object","required":["name","value","unit"],"properties":{"name":{"type":"string","description":"Counter type code."},"value":{"type":"number"},"unit":{"type":"string","description":"pts, EUR, visits…"},"EntityResponse":{"$ref":"#/components/schemas/WsOneEntityResponse"}}},"WsOneOfferResponse":{"type":"object","required":["used","edited","name","value","type","order","offerNumber"],"properties":{"used":{"type":"boolean","description":"true when the offer was applied on this receipt."},"edited":{"type":"boolean","description":"true when a coupon/voucher was issued (to print)."},"name":{"type":"string"},"value":{"type":"number","description":"Discount value in currency (type 1) or percentage (type 2); points for type 3."},"type":{"type":"integer","description":"`1` € discount · `2` % discount · `3` points · `4` coupon issued · `5` message.","enum":[1,2,3,4,5]},"order":{"type":"integer","description":"Application order (1-based)."},"offerNumber":{"type":"string","description":"Remizo offer code, or coupon code for type 4."},"message":{"type":"string","description":"Ticket message (type 5 or additional text)."},"couponCode":{"type":"string"},"couponUrl":{"type":"string","format":"uri","description":"Signed hub URL of the rendered coupon when render != none."},"reason":{"type":"string","description":"Reject reason when used = false (explain)."}}},"WsOneReceiptResponse":{"type":"object","required":["allTaxesIncludedAmount","discountAmount","currency","cashdeskNumber","receiptNumber","transactionDate","transactionIdentifier"],"properties":{"allTaxesIncludedAmount":{"type":"number","description":"Gross as received."},"preTaxAmount":{"type":"number"},"discountAmount":{"type":"number","description":"Total Remizo discount (decimal)."},"newAllTaxesIncludedAmount":{"type":"number","description":"Gross − Remizo discount."},"currency":{"type":"string"},"cashdeskNumber":{"type":"string"},"receiptNumber":{"type":"string"},"transactionDate":{"type":"string"},"transactionIdentifier":{"type":"string","format":"uuid","description":"Remizo transactionId (empty on verifyTransaction)."},"EntityResponse":{"$ref":"#/components/schemas/WsOneEntityResponse"},"IdentifierResponse":{"$ref":"#/components/schemas/WsOneIdentifierRequest"},"OldAccountResponse":{"type":"array","description":"Balances before the receipt.","items":{"$ref":"#/components/schemas/WsOneAccountResponse"}},"NewAccountResponse":{"type":"array","description":"Balances after the receipt.","items":{"$ref":"#/components/schemas/WsOneAccountResponse"}}}},"WsOneDetailsResponse":{"type":"object","required":["OfferResponse"],"properties":{"nbOffers":{"type":"integer"},"OfferResponse":{"type":"array","items":{"$ref":"#/components/schemas/WsOneOfferResponse"}}}},"WsOneTransactionResponse":{"type":"object","required":["responseCode","infos"],"properties":{"responseCode":{"$ref":"#/components/schemas/WsOneResponseCode"},"infos":{"type":"string","description":"Human-readable status (\"OK\" or error text)."},"ReceiptResponse":{"$ref":"#/components/schemas/WsOneReceiptResponse"},"DetailsResponse":{"$ref":"#/components/schemas/WsOneDetailsResponse"}},"example":{"responseCode":"000","infos":"OK","ReceiptResponse":{"allTaxesIncludedAmount":54.9,"preTaxAmount":45.75,"discountAmount":5.49,"newAllTaxesIncludedAmount":49.41,"currency":"EUR","cashdeskNumber":"03","receiptNumber":"000123","transactionDate":"09/11/2026 10:42:13","transactionIdentifier":"6f1c2a9e-3b7d-4e0a-9c1f-2d3e4f5a6b7c","EntityResponse":{"entityIdentifier":"S042","entityName":"Paris Bastille"},"IdentifierResponse":{"identifierType":"1","identifierValue":"2600000012345"},"OldAccountResponse":[{"name":"POINTS","value":120,"unit":"pts"}],"NewAccountResponse":[{"name":"POINTS","value":169,"unit":"pts"}]},"DetailsResponse":{"nbOffers":2,"OfferResponse":[{"used":true,"edited":false,"name":"-10% dès 50 €","value":10,"type":2,"order":1,"offerNumber":"WELCOME10"},{"used":true,"edited":false,"name":"1 point par euro","value":49,"type":3,"order":2,"offerNumber":"PTS-BASE"}]}}},"WsOneGetAvailableOffersResponse":{"type":"object","required":["responseCode","infos","OfferResponse"],"properties":{"responseCode":{"$ref":"#/components/schemas/WsOneResponseCode"},"infos":{"type":"string"},"IdentifierResponse":{"$ref":"#/components/schemas/WsOneIdentifierRequest"},"OfferResponse":{"type":"array","items":{"$ref":"#/components/schemas/WsOneOfferResponse"}}}},"WsOneGetAccountsResponse":{"type":"object","required":["responseCode","infos","AccountResponse"],"properties":{"responseCode":{"$ref":"#/components/schemas/WsOneResponseCode"},"infos":{"type":"string"},"IdentifierResponse":{"$ref":"#/components/schemas/WsOneIdentifierRequest"},"AccountResponse":{"type":"array","items":{"$ref":"#/components/schemas/WsOneAccountResponse"}}}},"EngineInfo":{"type":"object","required":["instance","workspace","version","startedAt","offers"],"properties":{"instance":{"type":"string","example":"remizo-acme-engine-1"},"workspace":{"type":"string","example":"acme"},"version":{"type":"string"},"gitSha":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"db":{"type":"object","properties":{"connected":{"type":"boolean"},"readOnlyReplica":{"type":"boolean"},"latencyMs":{"type":"number"}}},"amqp":{"type":"object","properties":{"connected":{"type":"boolean"},"queue":{"type":"string"}}},"valkey":{"type":"object","properties":{"connected":{"type":"boolean"}}},"offers":{"type":"object","properties":{"count":{"type":"integer"},"loadedAt":{"type":"string","format":"date-time"},"snapshotId":{"type":"string"}}},"apiKeys":{"type":"object","properties":{"count":{"type":"integer"},"loadedAt":{"type":"string","format":"date-time"}}},"features":{"type":"object","additionalProperties":{"type":"boolean"}},"adapters":{"type":"array","items":{"type":"string","enum":["native","wsone"]}}}},"ReloadResult":{"type":"object","required":["reloadedAt","offers"],"properties":{"reloadedAt":{"type":"string","format":"date-time"},"snapshotId":{"type":"string"},"offers":{"type":"object","properties":{"loaded":{"type":"integer"},"failed":{"type":"integer"},"errors":{"type":"array","items":{"type":"object","properties":{"offerId":{"type":"string"},"message":{"type":"string"}}}}}},"apiKeys":{"type":"integer"},"durationMs":{"type":"number"}}},"EngineStats":{"type":"object","required":["since","requests","evaluations"],"properties":{"since":{"type":"string","format":"date-time"},"requests":{"type":"object","description":"Per endpoint family counters and latency percentiles (ms) over 1/5/15 minute windows.","additionalProperties":{"type":"object","properties":{"count":{"type":"integer"},"errors":{"type":"integer"},"p50":{"type":"number"},"p95":{"type":"number"},"p99":{"type":"number"}}}},"evaluations":{"type":"object","properties":{"simulated":{"type":"integer"},"pending":{"type":"integer"},"committed":{"type":"integer"},"cancelled":{"type":"integer"},"refunded":{"type":"integer"},"offersApplied":{"type":"integer"},"discountCents":{"type":"integer"},"pointsEarned":{"type":"integer"},"couponsIssued":{"type":"integer"},"computeP99Ms":{"type":"number"}}},"queue":{"type":"object","properties":{"outboxPending":{"type":"integer"},"amqpPublished":{"type":"integer"},"amqpFailed":{"type":"integer"}}}}},"Readiness":{"type":"object","required":["status","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded","not_ready","draining"]},"checks":{"type":"object","additionalProperties":{"type":"string","enum":["ok","fail","skipped"]},"example":{"db":"ok","offers":"ok","amqp":"fail","valkey":"ok"}}}},"Ticket":{"title":"Ticket","description":"Canonical Remizo ticket. Every input adapter (native API, WsOne, future POS vendors) maps its payload to this document before evaluation. All amounts are integer cents (int64) in the ticket currency; the POS computes totals, the engine never recomputes them (coherence check = warning only).","type":"object","additionalProperties":false,"required":["header","lines"],"properties":{"schemaVersion":{"type":"string","const":"1","description":"Ticket document version. Defaults to \"1\" when absent."},"header":{"$ref":"#/components/schemas/Ticket/$defs/Header"},"customer":{"$ref":"#/components/schemas/Ticket/$defs/Customer"},"lines":{"type":"array","minItems":0,"maxItems":2000,"items":{"$ref":"#/components/schemas/Ticket/$defs/Line"}},"payments":{"type":"array","items":{"$ref":"#/components/schemas/Ticket/$defs/Payment"},"description":"Payments announced by the POS. May be empty at evaluation time (before the customer pays) and completed at commit."},"coupons":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/Ticket/$defs/PresentedCoupon"},"description":"Coupon / promo codes presented at checkout."},"options":{"$ref":"#/components/schemas/Ticket/$defs/Options"},"ext":{"type":"object","description":"Free-form extension bag preserved verbatim; reachable from CEL custom criteria as ticket.ext.*. Adapters put vendor-specific fields here.","additionalProperties":true}},"$defs":{"Cents":{"type":"integer","minimum":-9223372036854776000,"maximum":9223372036854776000,"description":"Integer amount in minor currency units (cents)."},"NonNegativeCents":{"type":"integer","minimum":0,"maximum":9223372036854776000},"Header":{"type":"object","additionalProperties":false,"required":["ticketNumber","storeCode","posId","businessDate","timestamp","currency","totals"],"properties":{"ticketNumber":{"type":"string","minLength":1,"maxLength":64,"description":"Receipt number as printed by the POS. Unique per (storeCode, posId, businessDate)."},"storeCode":{"type":"string","minLength":1,"maxLength":32,"description":"Store identifier known to Remizo (stores table)."},"posId":{"type":"string","minLength":1,"maxLength":32,"description":"Cash register identifier (number or IP)."},"cashierId":{"type":"string","maxLength":64},"businessDate":{"type":"string","format":"date","description":"Business day of the ticket (YYYY-MM-DD) in the store timezone."},"timestamp":{"type":"string","format":"date-time","description":"Ticket date/time, RFC 3339 with offset."},"channel":{"$ref":"#/components/schemas/Ticket/$defs/Channel"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217 code. Must equal the workspace currency."},"locale":{"type":"string","pattern":"^[a-z]{2}(-[A-Z]{2})?$","description":"Used to pick message translations (default: workspace locale)."},"totals":{"$ref":"#/components/schemas/Ticket/$defs/Totals"},"externalRef":{"type":"string","maxLength":128,"description":"Opaque reference from the calling system (order id, basket id)."},"originalTicketKey":{"type":"string","description":"For refund tickets: the ticketKey of the original transaction (see architecture.md, ticket key)."},"kind":{"type":"string","enum":["sale","refund"],"default":"sale"}}},"Totals":{"type":"object","additionalProperties":false,"required":["grossCents"],"description":"Totals as computed by the POS BEFORE Remizo offers. gross = sum of line totals after POS-side discounts.","properties":{"grossCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents","description":"Amount all taxes included, before Remizo offers."},"preTaxCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents"},"taxCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents"},"posDiscountCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents","description":"Discounts already granted by the POS itself (not Remizo)."},"itemCount":{"type":"integer","minimum":0,"description":"Number of units announced by the POS (used by the coherence check)."}}},"Channel":{"type":"string","enum":["pos","web","app","kiosk","callcenter","marketplace","other"],"default":"pos"},"Customer":{"type":"object","additionalProperties":false,"required":["identifiers"],"properties":{"identifiers":{"type":"array","minItems":1,"maxItems":10,"items":{"$ref":"#/components/schemas/Ticket/$defs/Identifier"},"description":"Resolved in order; the first identifier that matches a known customer wins."},"autoCreateCard":{"type":"boolean","default":true,"description":"When an identifier of type card is unknown, create the card and a minimal customer (workspace setting customers.autoCreateCard can forbid it)."},"prospect":{"type":"object","description":"Minimal data supplied at checkout when a new customer/prospect is created.","additionalProperties":false,"properties":{"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":32},"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","maxLength":100},"optinMarketing":{"type":"boolean"}}}}},"Identifier":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/Ticket/$defs/IdentifierType"},"value":{"type":"string","minLength":1,"maxLength":128}}},"IdentifierType":{"type":"string","description":"card = loyalty card number (WsOne type 1); web_account = e-commerce account (WsOne 10); wallet = mobile wallet id (WsOne 30); prospect = prospect id (WsOne 99); customer = Remizo customer UUID; account = Remizo account id; external = CRM external reference.","enum":["card","web_account","wallet","prospect","customer","account","email","phone","external"]},"Line":{"type":"object","additionalProperties":false,"required":["lineNo","qty","unitPriceCents","lineTotalCents"],"properties":{"lineNo":{"type":"integer","minimum":1,"description":"Position in the ticket, unique within the ticket. Used to address discounts in the result."},"ean":{"type":"string","pattern":"^[0-9]{8,14}$","description":"EAN-8/13/14 or GTIN."},"ref":{"type":"string","maxLength":64,"description":"Retailer main reference (SKU)."},"name":{"type":"string","maxLength":200},"category":{"type":"string","maxLength":64,"description":"Finest classification level (famille / sous-famille)."},"rayon":{"type":"string","maxLength":64,"description":"Department (rayon)."},"univers":{"type":"string","maxLength":64,"description":"Top classification level (univers)."},"brand":{"type":"string","maxLength":100},"qty":{"type":"number","exclusiveMinimum":0,"multipleOf":0.001,"description":"Quantity; integer for unit items, up to 3 decimals for weight/volume items. Refund tickets use positive quantities with header.kind = refund."},"qtyUnit":{"type":"string","enum":["unit","kg","g","l","ml","m"],"default":"unit"},"unitPriceCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents","description":"Unit price all taxes included, before POS discounts."},"unitPricePreTaxCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents"},"posDiscountCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents","default":0,"description":"Discount already applied by the POS on this line (total for the line)."},"lineTotalCents":{"$ref":"#/components/schemas/Ticket/$defs/NonNegativeCents","description":"qty x unitPriceCents - posDiscountCents, as computed by the POS. Remizo offers apply on this amount."},"taxRateBp":{"type":"integer","minimum":0,"maximum":10000,"description":"VAT rate in basis points (2000 = 20%)."},"flags":{"type":"array","uniqueItems":true,"items":{"type":"string","enum":["no_promo","no_points","gift_card","service","deposit","fee","already_discounted","remizo_free_product"]},"description":"no_promo: excluded from discounts; no_points: excluded from points base; remizo_free_product: line added by the POS following a free_product action (ignored on re-evaluation)."},"attributes":{"type":"object","description":"Retailer-specific attributes (size, color, supplier...). Reachable by article_attribute criteria.","additionalProperties":{"type":["string","number","boolean"]}}}},"Payment":{"type":"object","additionalProperties":false,"required":["method","amountCents"],"properties":{"method":{"$ref":"#/components/schemas/Ticket/$defs/PaymentMethod"},"amountCents":{"$ref":"#/components/schemas/Ticket/$defs/Cents"},"reference":{"type":"string","maxLength":128,"description":"Authorization code, voucher number, or Remizo reservation id when method = points."},"brand":{"type":"string","maxLength":32,"description":"Card scheme or wallet brand (visa, cb, applepay...)."}}},"PaymentMethod":{"type":"string","enum":["cash","card","check","voucher","gift_card","points","mobile","transfer","credit","other"]},"PresentedCoupon":{"type":"object","additionalProperties":false,"required":["code"],"properties":{"code":{"type":"string","minLength":1,"maxLength":64,"description":"Coupon code as scanned/typed (EAN-13, QR payload or short code)."},"source":{"type":"string","enum":["scanned","typed","app","auto"],"default":"scanned"}}},"Options":{"type":"object","additionalProperties":false,"description":"Per-call behaviour switches. Defaults come from workspace settings.","properties":{"coherenceCheck":{"type":"boolean","description":"Emit a warning when sum(lines) != totals.grossCents. Never blocks."},"explain":{"type":"boolean","default":true,"description":"Include rejected offers with reasons in the result."},"offerIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Simulator only: restrict evaluation to these offers (draft versions allowed when called through the hub simulator)."},"pointsBurn":{"type":"object","additionalProperties":false,"description":"Ask the engine to burn counter units as part of this ticket (alternative to a prior /points/reserve).","required":["counterTypeCode","amount"],"properties":{"counterTypeCode":{"type":"string"},"amount":{"type":"integer","minimum":1},"reservationId":{"type":"string","format":"uuid","description":"Existing reservation to confirm at commit."}}}}}}},"EvaluationResult":{"title":"EvaluationResult","description":"Result returned by the engine for simulate / evaluate / commit / refund calls. Everything the POS needs to adjust the ticket (discounts per line), print (messages, coupons) and display (balances), plus the explanation of every offer considered. Amounts are integer cents.","type":"object","additionalProperties":false,"required":["evaluationId","mode","status","ticketKey","totals","applied","rejected","pointsMovements","balances","couponsIssued","couponsRedeemed","messages","warnings","engine"],"properties":{"schemaVersion":{"type":"string","const":"1"},"evaluationId":{"type":"string","format":"uuid","description":"Unique id of this computation (also the trace id suffix)."},"transactionId":{"type":["string","null"],"format":"uuid","description":"Persisted transaction id (null in simulate mode)."},"ticketKey":{"type":"string","description":"Canonical key <storeCode>:<posId>:<businessDate>:<ticketNumber>."},"mode":{"type":"string","enum":["simulate","evaluate","commit","refund"]},"status":{"type":"string","enum":["ok","ok_with_warnings","customer_unknown","rejected"],"description":"rejected is only used by commit when the pending transaction cannot be committed (see problem details); evaluation errors are HTTP errors, not results."},"transactionState":{"type":["string","null"],"enum":["pending","committed","cancelled","refunded","partially_refunded","expired",null]},"idempotentReplay":{"type":"boolean","default":false,"description":"true when this response was served from the idempotency store."},"customer":{"$ref":"#/components/schemas/EvaluationResult/$defs/CustomerResolution"},"totals":{"$ref":"#/components/schemas/EvaluationResult/$defs/Totals"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/LineAdjustment"},"description":"Per-line net effect (all offers merged). Lines with no adjustment are omitted."},"addedLines":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/AddedLine"},"description":"Lines the POS must add (free_product mode add_line)."},"applied":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/AppliedOffer"}},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/RejectedOffer"},"description":"Present when options.explain is true (default)."},"pointsMovements":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/PointsMovement"}},"balances":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/Balance"},"description":"Balances after this ticket (projected in simulate/evaluate, actual in commit)."},"couponsIssued":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/CouponIssued"}},"couponsRedeemed":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/CouponRedeemed"}},"couponsRejected":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/CouponRejected"},"description":"Presented codes that could not be used."},"messages":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/Message"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/Warning"}},"engine":{"$ref":"#/components/schemas/EvaluationResult/$defs/EngineInfo"}},"$defs":{"Cents":{"type":"integer"},"CustomerResolution":{"type":"object","additionalProperties":false,"required":["resolved"],"properties":{"resolved":{"type":"boolean"},"customerId":{"type":"string","format":"uuid"},"accountId":{"type":"string","format":"uuid"},"cardNumber":{"type":"string"},"externalRef":{"type":"string"},"status":{"type":"string","enum":["active","blocked","prospect","anonymized"]},"loyaltyStatus":{"type":"string"},"firstName":{"type":"string"},"created":{"type":"boolean","description":"true when the card/customer was auto-created by this call."},"matchedIdentifier":{"type":"object","additionalProperties":false,"properties":{"type":{"type":"string"},"value":{"type":"string"}}},"segments":{"type":"array","items":{"type":"string","format":"uuid"}}}},"Totals":{"type":"object","additionalProperties":false,"required":["grossCents","discountCents","netCents"],"properties":{"grossCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Ticket gross as received."},"discountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Total Remizo discount (ticket-level + line-level)."},"ticketDiscountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Part granted as ticket-level discount (not attributable to one line by the POS)."},"lineDiscountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Part granted on lines (sum of lines[].discountCents)."},"netCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"grossCents - discountCents. Informational; the POS remains the source of truth."},"pointsBurnCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Value of counter units burnt as payment (payments method points / options.pointsBurn)."},"currency":{"type":"string","pattern":"^[A-Z]{3}$"}}},"LineAdjustment":{"type":"object","additionalProperties":false,"required":["lineNo","discountCents"],"properties":{"lineNo":{"type":"integer","minimum":1},"discountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Total Remizo discount on the line."},"qtyAffected":{"type":"number","description":"Units concerned (for N bought M free)."},"newLineTotalCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents"},"pointsEarned":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["counterTypeCode","amount"],"properties":{"counterTypeCode":{"type":"string"},"amount":{"type":"integer"}}}},"offers":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["offerId","discountCents"],"properties":{"offerId":{"type":"string","format":"uuid"},"actionId":{"type":"string"},"discountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents"},"qtyAffected":{"type":"number"}}}}}},"AddedLine":{"type":"object","additionalProperties":false,"required":["offerId","qty","unitPriceCents"],"properties":{"offerId":{"type":"string","format":"uuid"},"actionId":{"type":"string"},"ean":{"type":"string"},"ref":{"type":"string"},"name":{"type":"string"},"qty":{"type":"integer","minimum":1},"unitPriceCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","const":0},"flags":{"type":"array","items":{"type":"string"},"default":["remizo_free_product"]}}},"AppliedOffer":{"type":"object","additionalProperties":false,"required":["offerId","offerCode","offerVersion","name","level","mode","applications","valueCents","actions"],"properties":{"offerId":{"type":"string","format":"uuid"},"offerCode":{"type":"string"},"offerVersion":{"type":"integer"},"name":{"type":"string"},"level":{"type":"integer"},"mode":{"type":"string","enum":["cumulative","best_offer","best_line"]},"priority":{"type":"integer"},"applications":{"type":"integer","minimum":1,"description":"Times the offer repeated in the ticket (limits.perTicket)."},"valueCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents","description":"Total discount value of this offer (0 for pure points/coupon offers)."},"triggeredBy":{"type":"object","additionalProperties":false,"properties":{"couponCode":{"type":"string"},"eventId":{"type":"string","format":"uuid"},"abTestId":{"type":"string","format":"uuid"},"abVariant":{"type":"string"}}},"actions":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult/$defs/ActionResult"}},"explanation":{"type":"string","description":"Human-readable (workspace locale) summary, e.g. \"Ticket >= 50,00 EUR and store in group IDF: -10%\"."},"criteriaTrace":{"type":"array","description":"Simulator / explain mode only: evaluation of each criterion node.","items":{"type":"object","additionalProperties":false,"required":["nodeId","type","result"],"properties":{"nodeId":{"type":"string"},"type":{"type":"string"},"result":{"type":"boolean"},"matchedLines":{"type":"array","items":{"type":"integer"}},"detail":{"type":"string"}}}}}},"ActionResult":{"type":"object","additionalProperties":false,"required":["type","status"],"properties":{"actionId":{"type":"string"},"type":{"type":"string","enum":["discount_amount","discount_percent","discount_fixed_price","cheapest_free","free_product","lot_price","points_fixed","points_per_amount","points_multiplier","counter_update","coupon_issue","message","customer_tag","customer_status","custom_event","stamp_card"]},"status":{"type":"string","enum":["applied","skipped","deferred"],"description":"deferred = will happen at commit (coupon render, customer status...)."},"skipReason":{"type":"string","enum":["free_product_absent","no_eligible_lines","insufficient_counter","cap_reached","no_customer","zero_value"]},"discountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents"},"lines":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["lineNo","discountCents"],"properties":{"lineNo":{"type":"integer"},"discountCents":{"$ref":"#/components/schemas/EvaluationResult/$defs/Cents"},"qtyAffected":{"type":"number"}}}},"counterTypeCode":{"type":"string"},"amount":{"type":"integer","description":"Counter units credited/debited by this action."},"couponIds":{"type":"array","items":{"type":"string","format":"uuid"}},"messageIndex":{"type":"integer","description":"Index into messages[]."},"detail":{"type":"string"},"stampCard":{"type":"object","additionalProperties":false,"required":["stampsRequired","stampsBefore","stampsAdded","stampsAfter","completed"],"description":"Stamp card progress (type stamp_card): stamps on the current card before and after the ticket, completed cards.","properties":{"stampsRequired":{"type":"integer"},"stampsBefore":{"type":"integer"},"stampsAdded":{"type":"integer"},"stampsAfter":{"type":"integer"},"completed":{"type":"integer"}}}}},"RejectedOffer":{"type":"object","additionalProperties":false,"required":["offerId","offerCode","name","reason"],"properties":{"offerId":{"type":"string","format":"uuid"},"offerCode":{"type":"string"},"offerVersion":{"type":"integer"},"name":{"type":"string"},"level":{"type":"integer"},"reason":{"$ref":"#/components/schemas/EvaluationResult/$defs/RejectReason"},"criterionId":{"type":"string","description":"First failing criterion node (reason criteria_not_met)."},"criterionType":{"type":"string"},"byOfferId":{"type":"string","format":"uuid","description":"Winning/excluding offer (reasons lost_best_offer, lost_best_line, excluded_by, exclusive_applied)."},"detail":{"type":"string"},"criteriaTrace":{"$ref":"#/components/schemas/EvaluationResult/$defs/AppliedOffer/properties/criteriaTrace"}}},"RejectReason":{"type":"string","enum":["criteria_not_met","not_in_validity","trigger_mismatch","customer_required","customer_blocked","excluded_by","exclusive_applied","lost_best_offer","lost_best_line","global_limit_reached","customer_limit_reached","store_limit_reached","ticket_limit_reached","coupon_required","coupon_invalid","coupon_expired","coupon_already_used","coupon_not_owned","ab_not_in_variant","suspended","no_eligible_lines","zero_value","action_error"]},"PointsMovement":{"type":"object","additionalProperties":false,"required":["counterTypeCode","kind","amount","state"],"properties":{"movementId":{"type":["string","null"],"format":"uuid","description":"Ledger id (null until commit)."},"counterTypeCode":{"type":"string"},"kind":{"type":"string","enum":["earn","burn","reserve","release","adjust","expire","reset"]},"amount":{"type":"integer","description":"Signed: positive credit, negative debit."},"state":{"type":"string","enum":["projected","pending_activation","posted","reserved","confirmed","cancelled"]},"offerId":{"type":["string","null"],"format":"uuid"},"reservationId":{"type":["string","null"],"format":"uuid"},"activatesAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"reason":{"type":"string"}}},"Balance":{"type":"object","additionalProperties":false,"required":["counterTypeCode","name","unit","total","available"],"properties":{"counterTypeCode":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","enum":["points","cashback","visits","technical"]},"unit":{"type":"string","description":"pts, EUR cents, visits..."},"total":{"type":"integer"},"available":{"type":"integer","description":"total - reserved - pending_activation."},"reserved":{"type":"integer"},"pending":{"type":"integer","description":"Credited but not yet active."},"previousTotal":{"type":"integer","description":"Before this ticket."},"nextExpiry":{"type":["object","null"],"additionalProperties":false,"properties":{"amount":{"type":"integer"},"at":{"type":"string","format":"date-time"}}}}},"CouponIssued":{"type":"object","additionalProperties":false,"required":["couponId","code","format","templateId","validFrom","validTo","render"],"properties":{"couponId":{"type":"string","format":"uuid"},"code":{"type":"string"},"format":{"type":"string","enum":["ean13","qr","short"]},"templateId":{"type":"string","format":"uuid"},"templateName":{"type":"string"},"offerId":{"type":"string","format":"uuid"},"linkedOfferId":{"type":["string","null"],"format":"uuid"},"faceValue":{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string"},"value":{"type":["integer","string"]},"label":{"type":"string"}}},"validFrom":{"type":"string","format":"date-time"},"validTo":{"type":"string","format":"date-time"},"state":{"type":"string","enum":["projected","available"],"description":"projected until commit."},"render":{"type":"object","additionalProperties":false,"required":["mode","status"],"properties":{"mode":{"type":"string","enum":["none","png","pdf"]},"status":{"type":"string","enum":["none","pending","ready","failed"]},"url":{"type":"string","format":"uri","description":"Signed hub URL (/v1/public/coupons/{token}); valid even while pending (returns 202 until ready)."}}},"printText":{"type":"string","description":"Short text to print under the barcode."}}},"CouponRedeemed":{"type":"object","additionalProperties":false,"required":["code","couponId","offerId","state"],"properties":{"code":{"type":"string"},"couponId":{"type":"string","format":"uuid"},"offerId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["reserved","used"],"description":"reserved after evaluate, used after commit."}}},"CouponRejected":{"type":"object","additionalProperties":false,"required":["code","reason"],"properties":{"code":{"type":"string"},"reason":{"type":"string","enum":["unknown","expired","not_yet_valid","already_used","reserved_elsewhere","cancelled","not_owned","no_matching_offer","offer_criteria_not_met"]},"offerId":{"type":["string","null"],"format":"uuid"}}},"Message":{"type":"object","additionalProperties":false,"required":["text","placement"],"properties":{"text":{"type":"string"},"placement":{"type":"string","enum":["ticket","screen","both"]},"offerId":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","enum":["offer","balance","coupon","system"],"default":"offer"}}},"Warning":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","enum":["coherence_lines_vs_total","coherence_item_count","unknown_store","currency_mismatch","customer_auto_created","customer_blocked","payments_incomplete","offer_load_stale","limit_near_exhaustion","coupon_render_disabled","unknown_counter_type","line_without_identifier","duplicate_line_no","points_reservation_mismatch"]},"message":{"type":"string"},"detail":{"type":"object","additionalProperties":true}}},"EngineInfo":{"type":"object","additionalProperties":false,"required":["instance","offersLoadedAt","computeMs"],"properties":{"instance":{"type":"string","description":"Container name, e.g. remizo-acme-engine-1."},"version":{"type":"string"},"offersLoadedAt":{"type":"string","format":"date-time"},"offersCount":{"type":"integer"},"computeMs":{"type":"number"},"totalMs":{"type":"number"}}}}},"Offer":{"title":"Offer","description":"Remizo offer definition: trigger -> condition tree (AND/OR/NOT groups of typed criteria) -> actions, plus stacking, limits and validity. This exact document is what the studio edits, what offer_versions.definition stores, and what the engine compiles in memory once published. Amounts are integer cents; percentages are basis points (bp, 10000 = 100%).","type":"object","additionalProperties":false,"required":["id","code","name","status","version","trigger","criteria","actions","stacking","validity"],"properties":{"schemaVersion":{"type":"string","const":"1"},"id":{"type":"string","format":"uuid","description":"Stable offer id across versions."},"code":{"type":"string","pattern":"^[A-Z0-9][A-Z0-9_-]{1,31}$","description":"Human code, unique per workspace, immutable (used in POS messages and WsOne offerNumber)."},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":2000},"folderId":{"type":["string","null"],"format":"uuid"},"tags":{"type":"array","uniqueItems":true,"items":{"type":"string","maxLength":40}},"status":{"$ref":"#/components/schemas/Offer/$defs/OfferStatus"},"version":{"type":"integer","minimum":1,"description":"Version number of this definition (offer_versions.version_no)."},"publishedAt":{"type":["string","null"],"format":"date-time"},"trigger":{"$ref":"#/components/schemas/Offer/$defs/Trigger"},"criteria":{"$ref":"#/components/schemas/Offer/$defs/CriterionNode","description":"Root of the condition tree. An empty AND group means \"always\"."},"actions":{"type":"array","minItems":1,"maxItems":20,"items":{"$ref":"#/components/schemas/Offer/$defs/Action"},"description":"Executed in order when the criteria hold. Discount actions of the same offer are summed."},"stacking":{"$ref":"#/components/schemas/Offer/$defs/Stacking"},"limits":{"$ref":"#/components/schemas/Offer/$defs/Limits"},"validity":{"$ref":"#/components/schemas/Offer/$defs/Validity"},"coupon":{"$ref":"#/components/schemas/Offer/$defs/CouponRenderDefaults"},"ui":{"type":"object","description":"Studio-only data (React Flow node positions, notes). Ignored by the engine.","additionalProperties":true},"meta":{"type":"object","additionalProperties":false,"properties":{"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"},"templateId":{"type":["string","null"],"format":"uuid","description":"Ready-made template this offer was created from."}}}},"$defs":{"Cents":{"type":"integer","minimum":0,"maximum":9223372036854776000},"Bp":{"type":"integer","minimum":0,"maximum":1000000,"description":"Basis points: 10000 = 100%. Multipliers may exceed 10000."},"I18nText":{"type":"object","description":"Map of BCP-47 language -> text. The workspace default locale key is required.","minProperties":1,"propertyNames":{"pattern":"^[a-z]{2}(-[A-Z]{2})?$"},"additionalProperties":{"type":"string","maxLength":500}},"Weekday":{"type":"string","enum":["mon","tue","wed","thu","fri","sat","sun"]},"Period":{"type":"string","enum":["ever","day","week","month","quarter","year","rolling_days","validity"],"description":"Calendar periods are computed in the offer/workspace timezone. rolling_days requires periodDays. validity = the offer validity window."},"StringOp":{"type":"string","enum":["eq","neq","in","not_in","contains","starts_with","regex","exists","not_exists"]},"NumberOp":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","between","exists","not_exists"]},"Rounding":{"type":"string","enum":["down","half_up","up"],"default":"half_up"},"Expiry":{"type":"object","additionalProperties":false,"required":["mode"],"description":"Expiry rule for credited counter units. none = counter type default.","properties":{"mode":{"type":"string","enum":["counter_default","never","days","end_of_month","end_of_year","fixed_date"]},"days":{"type":"integer","minimum":1},"date":{"type":"string","format":"date"}}},"OfferStatus":{"type":"string","enum":["draft","pending_approval","approved","published","suspended","expired","archived"],"description":"draft -> pending_approval -> approved -> published; published <-> suspended (emergency stop); published -> expired (validity.to passed); any -> archived. Only published versions are loaded by the engine."},"Trigger":{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string","enum":["transaction","event","coupon"],"description":"transaction: evaluated on every ticket. event: evaluated when a customer event occurs (no ticket). coupon: evaluated on tickets only when one of the trigger coupons is presented; the coupon is redeemed when the offer applies."},"event":{"type":"object","additionalProperties":false,"required":["types"],"properties":{"types":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","enum":["signup","card_created","birthday","anniversary_signup","status_changed","segment_entered","segment_left","counter_threshold","custom"]}},"customTypes":{"type":"array","items":{"type":"string","maxLength":64}},"counterThreshold":{"type":"object","additionalProperties":false,"required":["counterTypeCode","threshold"],"properties":{"counterTypeCode":{"type":"string"},"threshold":{"type":"integer"},"direction":{"type":"string","enum":["crosses_up","crosses_down"],"default":"crosses_up"}}}}},"coupon":{"type":"object","additionalProperties":false,"properties":{"templateIds":{"type":"array","items":{"type":"string","format":"uuid"}},"campaignIds":{"type":"array","items":{"type":"string","format":"uuid"}},"codes":{"type":"array","items":{"type":"string"},"description":"Static promo codes (reusable, not stored as coupons)."},"redeem":{"type":"boolean","default":true,"description":"Mark the coupon used at commit."}}}},"allOf":[{"if":{"properties":{"type":{"const":"event"}}},"then":{"required":["event"]}},{"if":{"properties":{"type":{"const":"coupon"}}},"then":{"required":["coupon"]}}]},"CriterionNode":{"oneOf":[{"$ref":"#/components/schemas/Offer/$defs/Group"},{"$ref":"#/components/schemas/Offer/$defs/Criterion"}]},"Group":{"type":"object","additionalProperties":false,"required":["kind","op","children"],"properties":{"kind":{"const":"group"},"id":{"$ref":"#/components/schemas/Offer/$defs/NodeId"},"op":{"type":"string","enum":["and","or","not"],"description":"not takes exactly one child."},"label":{"type":"string","maxLength":120},"children":{"type":"array","items":{"$ref":"#/components/schemas/Offer/$defs/CriterionNode"}}},"if":{"properties":{"op":{"const":"not"}}},"then":{"properties":{"children":{"minItems":1,"maxItems":1}}}},"NodeId":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,40}$","description":"Unique within the offer. Needed when an action targets this criterion's matching lines."},"CriterionBase":{"type":"object","additionalProperties":false,"required":["kind","type","params"],"properties":{"kind":{"const":"criterion"},"id":{"$ref":"#/components/schemas/Offer/$defs/NodeId"},"type":{"$ref":"#/components/schemas/Offer/$defs/CriterionType"},"label":{"type":"string","maxLength":120},"negate":{"type":"boolean","default":false},"params":{"type":"object"}}},"CriterionType":{"type":"string","enum":["ticket_amount","ticket_item_count","date_range","time_range","weekdays","store","channel","customer_identified","customer_attribute","loyalty_status","segment","customer_anniversary","first_purchase","coupon_presented","payment_method","counter_balance","usage_count","ab_variant","event","custom_cel","article_ean","article_ref","article_list","article_category","article_rayon","article_univers","article_brand","article_attribute","article_price","article_lot"]},"Criterion":{"allOf":[{"$ref":"#/components/schemas/Offer/$defs/CriterionBase"},{"oneOf":[{"$ref":"#/components/schemas/Offer/$defs/criteria/TicketAmount"},{"$ref":"#/components/schemas/Offer/$defs/criteria/TicketItemCount"},{"$ref":"#/components/schemas/Offer/$defs/criteria/DateRange"},{"$ref":"#/components/schemas/Offer/$defs/criteria/TimeRange"},{"$ref":"#/components/schemas/Offer/$defs/criteria/Weekdays"},{"$ref":"#/components/schemas/Offer/$defs/criteria/Store"},{"$ref":"#/components/schemas/Offer/$defs/criteria/Channel"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CustomerIdentified"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CustomerAttribute"},{"$ref":"#/components/schemas/Offer/$defs/criteria/LoyaltyStatus"},{"$ref":"#/components/schemas/Offer/$defs/criteria/Segment"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CustomerAnniversary"},{"$ref":"#/components/schemas/Offer/$defs/criteria/FirstPurchase"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CouponPresented"},{"$ref":"#/components/schemas/Offer/$defs/criteria/PaymentMethod"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CounterBalance"},{"$ref":"#/components/schemas/Offer/$defs/criteria/UsageCount"},{"$ref":"#/components/schemas/Offer/$defs/criteria/AbVariant"},{"$ref":"#/components/schemas/Offer/$defs/criteria/Event"},{"$ref":"#/components/schemas/Offer/$defs/criteria/CustomCel"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleEan"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleRef"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleList"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleCategory"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleRayon"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleUnivers"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleBrand"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleAttribute"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticlePrice"},{"$ref":"#/components/schemas/Offer/$defs/criteria/ArticleLot"}]}]},"ArticleMatch":{"type":"object","additionalProperties":false,"description":"Common thresholds for article criteria. The criterion holds when the matching lines (excluding flags no_promo for discounts) reach these thresholds. Matching lines become the criterion's eligible set, addressable by actions through target.criterionId.","properties":{"minQty":{"type":"number","minimum":0,"default":1,"description":"Minimum total quantity of matching lines."},"maxQty":{"type":"number","minimum":0},"minAmountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents","description":"Minimum total of matching lineTotalCents."},"minLines":{"type":"integer","minimum":0}}},"criteria":{"TicketAmount":{"properties":{"type":{"const":"ticket_amount"},"params":{"type":"object","additionalProperties":false,"properties":{"base":{"type":"string","enum":["gross","pre_tax","eligible"],"default":"gross","description":"eligible = sum of lines without flags no_promo/gift_card/deposit/fee."},"minCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"maxCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"}},"anyOf":[{"required":["minCents"]},{"required":["maxCents"]}]}}},"TicketItemCount":{"properties":{"type":{"const":"ticket_item_count"},"params":{"type":"object","additionalProperties":false,"properties":{"countMode":{"type":"string","enum":["units","lines"],"default":"units"},"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0}},"anyOf":[{"required":["min"]},{"required":["max"]}]}}},"DateRange":{"properties":{"type":{"const":"date_range"},"params":{"type":"object","additionalProperties":false,"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date","description":"Inclusive."},"recurringYearly":{"type":"boolean","default":false,"description":"Ignore the year (e.g. every 24/12-26/12)."}},"anyOf":[{"required":["from"]},{"required":["to"]}]}}},"TimeRange":{"properties":{"type":{"const":"time_range"},"params":{"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"from":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},"to":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Exclusive. If to < from the range crosses midnight."},"timezone":{"type":"string","description":"IANA tz; default store timezone, then workspace timezone."}}}}},"Weekdays":{"properties":{"type":{"const":"weekdays"},"params":{"type":"object","additionalProperties":false,"required":["days"],"properties":{"days":{"type":"array","minItems":1,"uniqueItems":true,"items":{"$ref":"#/components/schemas/Offer/$defs/Weekday"}}}}}},"Store":{"properties":{"type":{"const":"store"},"params":{"type":"object","additionalProperties":false,"properties":{"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"storeCodes":{"type":"array","items":{"type":"string"}},"storeGroups":{"type":"array","items":{"type":"string"},"description":"Store group codes (store_groups table)."}},"anyOf":[{"required":["storeCodes"]},{"required":["storeGroups"]}]}}},"Channel":{"properties":{"type":{"const":"channel"},"params":{"type":"object","additionalProperties":false,"required":["channels"],"properties":{"channels":{"type":"array","minItems":1,"items":{"type":"string","enum":["pos","web","app","kiosk","callcenter","marketplace","other"]}}}}}},"CustomerIdentified":{"properties":{"type":{"const":"customer_identified"},"params":{"type":"object","additionalProperties":false,"properties":{"identifierTypes":{"type":"array","items":{"type":"string","enum":["card","web_account","wallet","prospect","customer","account","email","phone","external"]},"description":"Empty = any identifier."},"allowAutoCreated":{"type":"boolean","default":true,"description":"false = the card must have existed before this ticket."},"customerStatuses":{"type":"array","items":{"type":"string","enum":["active","blocked","prospect"]}}}}}},"CustomerAttribute":{"properties":{"type":{"const":"customer_attribute"},"params":{"type":"object","additionalProperties":false,"required":["attribute","op"],"properties":{"attribute":{"type":"string","description":"Built-in: gender, birthDate, signupAt, storeCode, optinMarketing, email, phone, city, postalCode, tags; or attributes.<key>."},"op":{"$ref":"#/components/schemas/Offer/$defs/StringOp"},"value":{"type":["string","number","boolean","null"]},"values":{"type":"array","items":{"type":["string","number","boolean"]}}}}}},"LoyaltyStatus":{"properties":{"type":{"const":"loyalty_status"},"params":{"type":"object","additionalProperties":false,"required":["statuses"],"properties":{"statuses":{"type":"array","minItems":1,"items":{"type":"string","maxLength":32},"description":"Loyalty status codes defined in workspace settings (e.g. bronze, silver, gold)."}}}}},"Segment":{"properties":{"type":{"const":"segment"},"params":{"type":"object","additionalProperties":false,"required":["segmentIds"],"properties":{"mode":{"type":"string","enum":["in_any","in_all","not_in"],"default":"in_any"},"segmentIds":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"}}}}}},"CustomerAnniversary":{"properties":{"type":{"const":"customer_anniversary"},"params":{"type":"object","additionalProperties":false,"required":["event"],"properties":{"event":{"type":"string","enum":["birthday","signup"]},"daysBefore":{"type":"integer","minimum":0,"default":0},"daysAfter":{"type":"integer","minimum":0,"default":0},"wholeMonth":{"type":"boolean","default":false}}}}},"FirstPurchase":{"properties":{"type":{"const":"first_purchase"},"params":{"type":"object","additionalProperties":false,"properties":{"scope":{"type":"string","enum":["ever","store","channel"],"default":"ever"}}}}},"CouponPresented":{"properties":{"type":{"const":"coupon_presented"},"params":{"type":"object","additionalProperties":false,"description":"Holds when a valid (available, not expired) coupon of the given kind is presented. Does NOT redeem it (use trigger.type = coupon for that).","properties":{"templateIds":{"type":"array","items":{"type":"string","format":"uuid"}},"campaignIds":{"type":"array","items":{"type":"string","format":"uuid"}},"codes":{"type":"array","items":{"type":"string"},"description":"Static promo codes."},"ownedByCustomer":{"type":"boolean","default":false,"description":"The coupon must have been issued to the identified customer."}},"anyOf":[{"required":["templateIds"]},{"required":["campaignIds"]},{"required":["codes"]}]}}},"PaymentMethod":{"properties":{"type":{"const":"payment_method"},"params":{"type":"object","additionalProperties":false,"required":["methods"],"properties":{"methods":{"type":"array","minItems":1,"items":{"type":"string","enum":["cash","card","check","voucher","gift_card","points","mobile","transfer","credit","other"]}},"brands":{"type":"array","items":{"type":"string"}},"minAmountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"mode":{"type":"string","enum":["any","all_payments","majority"],"default":"any"}}}}},"CounterBalance":{"properties":{"type":{"const":"counter_balance"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode"],"properties":{"counterTypeCode":{"type":"string"},"balance":{"type":"string","enum":["available","total","pending"],"default":"available"},"min":{"type":"integer"},"max":{"type":"integer"}},"anyOf":[{"required":["min"]},{"required":["max"]}]}}},"UsageCount":{"properties":{"type":{"const":"usage_count"},"params":{"type":"object","additionalProperties":false,"required":["scope","max"],"description":"Number of previous applications of THIS offer (or another offer via offerId).","properties":{"scope":{"type":"string","enum":["customer","card","store","global"]},"offerId":{"type":"string","format":"uuid"},"period":{"$ref":"#/components/schemas/Offer/$defs/Period","default":"ever"},"periodDays":{"type":"integer","minimum":1},"min":{"type":"integer","minimum":0},"max":{"type":"integer","minimum":0}}}}},"AbVariant":{"properties":{"type":{"const":"ab_variant"},"params":{"type":"object","additionalProperties":false,"required":["testId","variants"],"properties":{"testId":{"type":"string","format":"uuid"},"variants":{"type":"array","minItems":1,"items":{"type":"string","maxLength":20}}}}}},"Event":{"properties":{"type":{"const":"event"},"params":{"type":"object","additionalProperties":false,"description":"Only for trigger.type = event: filter on event payload.","properties":{"path":{"type":"string","description":"JSON path in event.payload (dot notation)."},"op":{"$ref":"#/components/schemas/Offer/$defs/StringOp"},"value":{"type":["string","number","boolean","null"]}},"required":["path","op"]}}},"CustomCel":{"properties":{"type":{"const":"custom_cel"},"params":{"type":"object","additionalProperties":false,"required":["expression"],"description":"CEL boolean expression over variables: ticket, customer, balances (map counterTypeCode -> {available,total}), store, now, ctx. Validated at save time by the hub (engine /mgmt/v1/cel/validate).","properties":{"expression":{"type":"string","maxLength":4000},"lineExpression":{"type":"string","maxLength":4000,"description":"Optional CEL over variable line; when set the criterion is article-level and its eligible set is the lines where lineExpression is true."},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleEan":{"properties":{"type":{"const":"article_ean"},"params":{"type":"object","additionalProperties":false,"required":["eans"],"properties":{"eans":{"type":"array","minItems":1,"maxItems":5000,"items":{"type":"string","pattern":"^[0-9]{8,14}$"}},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleRef":{"properties":{"type":{"const":"article_ref"},"params":{"type":"object","additionalProperties":false,"required":["refs"],"properties":{"refs":{"type":"array","minItems":1,"maxItems":5000,"items":{"type":"string","maxLength":64}},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleList":{"properties":{"type":{"const":"article_list"},"params":{"type":"object","additionalProperties":false,"required":["listIds"],"description":"Product lists managed in the studio (product_lists table); matched on ean or ref.","properties":{"listIds":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"}},"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleCategory":{"properties":{"type":{"const":"article_category"},"params":{"type":"object","additionalProperties":false,"required":["codes"],"properties":{"codes":{"type":"array","minItems":1,"items":{"type":"string"}},"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleRayon":{"properties":{"type":{"const":"article_rayon"},"params":{"type":"object","additionalProperties":false,"required":["codes"],"properties":{"codes":{"type":"array","minItems":1,"items":{"type":"string"}},"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleUnivers":{"properties":{"type":{"const":"article_univers"},"params":{"type":"object","additionalProperties":false,"required":["codes"],"properties":{"codes":{"type":"array","minItems":1,"items":{"type":"string"}},"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleBrand":{"properties":{"type":{"const":"article_brand"},"params":{"type":"object","additionalProperties":false,"required":["brands"],"properties":{"brands":{"type":"array","minItems":1,"items":{"type":"string"}},"mode":{"type":"string","enum":["in","not_in"],"default":"in"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticleAttribute":{"properties":{"type":{"const":"article_attribute"},"params":{"type":"object","additionalProperties":false,"required":["attribute","op"],"properties":{"attribute":{"type":"string","description":"Line field (name, taxRateBp, qtyUnit) or attributes.<key>."},"op":{"$ref":"#/components/schemas/Offer/$defs/StringOp"},"value":{"type":["string","number","boolean","null"]},"values":{"type":"array","items":{"type":["string","number","boolean"]}},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}}}}},"ArticlePrice":{"properties":{"type":{"const":"article_price"},"params":{"type":"object","additionalProperties":false,"properties":{"field":{"type":"string","enum":["unitPriceCents","lineTotalCents"],"default":"unitPriceCents"},"minCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"maxCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"match":{"$ref":"#/components/schemas/Offer/$defs/ArticleMatch"}},"anyOf":[{"required":["minCents"]},{"required":["maxCents"]}]}}},"ArticleLot":{"properties":{"type":{"const":"article_lot"},"params":{"type":"object","additionalProperties":false,"required":["components"],"description":"A lot (bundle) is complete when every component is satisfied by distinct units. The criterion holds when at least minLots complete lots exist; its eligible set is the units consumed by the lots (cheapest-first allocation).","properties":{"components":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","additionalProperties":false,"required":["qty"],"properties":{"criterionId":{"$ref":"#/components/schemas/Offer/$defs/NodeId","description":"Article criterion elsewhere in the tree whose eligible set feeds this component."},"selector":{"$ref":"#/components/schemas/Offer/$defs/Criterion","description":"Inline article criterion (alternative to criterionId)."},"qty":{"type":"number","exclusiveMinimum":0}},"oneOf":[{"required":["criterionId"]},{"required":["selector"]}]}},"minLots":{"type":"integer","minimum":1,"default":1},"maxLots":{"type":"integer","minimum":1,"description":"Cap on lots per ticket (default unlimited)."},"sameArticle":{"type":"boolean","default":false,"description":"All units of a lot must share the same ean/ref."}}}}}},"Target":{"type":"object","additionalProperties":false,"description":"Which ticket lines an article-level action applies to. Exactly one of all / criterionId / selector.","properties":{"all":{"const":true,"description":"All lines without flag no_promo."},"criterionId":{"$ref":"#/components/schemas/Offer/$defs/NodeId","description":"Eligible set of an article criterion of this offer."},"selector":{"$ref":"#/components/schemas/Offer/$defs/Criterion","description":"Inline article criterion."},"sort":{"type":"string","enum":["cheapest_first","most_expensive_first","ticket_order"],"default":"cheapest_first"},"maxQty":{"type":"number","exclusiveMinimum":0,"description":"Limit the number of units affected."}},"oneOf":[{"required":["all"]},{"required":["criterionId"]},{"required":["selector"]}]},"ActionType":{"type":"string","enum":["discount_amount","discount_percent","discount_fixed_price","cheapest_free","free_product","lot_price","points_fixed","points_per_amount","points_multiplier","counter_update","coupon_issue","message","customer_tag","customer_status","custom_event","stamp_card"]},"ActionBase":{"type":"object","additionalProperties":false,"required":["type","params"],"properties":{"id":{"$ref":"#/components/schemas/Offer/$defs/NodeId"},"type":{"$ref":"#/components/schemas/Offer/$defs/ActionType"},"label":{"type":"string","maxLength":120},"params":{"type":"object"}}},"Action":{"allOf":[{"$ref":"#/components/schemas/Offer/$defs/ActionBase"},{"oneOf":[{"$ref":"#/components/schemas/Offer/$defs/actions/DiscountAmount"},{"$ref":"#/components/schemas/Offer/$defs/actions/DiscountPercent"},{"$ref":"#/components/schemas/Offer/$defs/actions/DiscountFixedPrice"},{"$ref":"#/components/schemas/Offer/$defs/actions/CheapestFree"},{"$ref":"#/components/schemas/Offer/$defs/actions/FreeProduct"},{"$ref":"#/components/schemas/Offer/$defs/actions/LotPrice"},{"$ref":"#/components/schemas/Offer/$defs/actions/PointsFixed"},{"$ref":"#/components/schemas/Offer/$defs/actions/PointsPerAmount"},{"$ref":"#/components/schemas/Offer/$defs/actions/PointsMultiplier"},{"$ref":"#/components/schemas/Offer/$defs/actions/CounterUpdate"},{"$ref":"#/components/schemas/Offer/$defs/actions/CouponIssue"},{"$ref":"#/components/schemas/Offer/$defs/actions/Message"},{"$ref":"#/components/schemas/Offer/$defs/actions/CustomerTag"},{"$ref":"#/components/schemas/Offer/$defs/actions/CustomerStatus"},{"$ref":"#/components/schemas/Offer/$defs/actions/CustomEvent"},{"$ref":"#/components/schemas/Offer/$defs/actions/StampCard"}]}]},"actions":{"DiscountAmount":{"properties":{"type":{"const":"discount_amount"},"params":{"type":"object","additionalProperties":false,"required":["amountCents"],"description":"Fixed discount. scope ticket: one amount on the ticket (capped at the eligible total). per_line / per_unit: on each targeted line / unit. eligible_total: one amount spread pro rata on the targeted lines.","properties":{"amountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"scope":{"type":"string","enum":["ticket","eligible_total","per_line","per_unit"],"default":"ticket"},"target":{"$ref":"#/components/schemas/Offer/$defs/Target","description":"Required when scope != ticket."},"maxTotalCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"}}}}},"DiscountPercent":{"properties":{"type":{"const":"discount_percent"},"params":{"type":"object","additionalProperties":false,"required":["percentBp"],"description":"Percentage discount. scope ticket: on the eligible ticket amount (spread pro rata over eligible lines in the breakdown). lines: on the targeted lines.","properties":{"percentBp":{"type":"integer","minimum":1,"maximum":10000},"scope":{"type":"string","enum":["ticket","lines"],"default":"ticket"},"target":{"$ref":"#/components/schemas/Offer/$defs/Target"},"maxTotalCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"rounding":{"$ref":"#/components/schemas/Offer/$defs/Rounding"}}}}},"DiscountFixedPrice":{"properties":{"type":{"const":"discount_fixed_price"},"params":{"type":"object","additionalProperties":false,"required":["priceCents","target"],"description":"Bring the targeted units to a fixed price (discount = unitPrice - price when positive).","properties":{"priceCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"target":{"$ref":"#/components/schemas/Offer/$defs/Target"}}}}},"CheapestFree":{"properties":{"type":{"const":"cheapest_free"},"params":{"type":"object","additionalProperties":false,"required":["target","groupSize","freeCount"],"description":"\"N bought, M cheapest free\" (or discounted by percentBp). Units of the target are sorted by unit price, grouped by groupSize; in each group the freeCount cheapest units get the discount.","properties":{"target":{"$ref":"#/components/schemas/Offer/$defs/Target"},"groupSize":{"type":"integer","minimum":2},"freeCount":{"type":"integer","minimum":1},"percentBp":{"type":"integer","minimum":1,"maximum":10000,"default":10000},"maxGroups":{"type":"integer","minimum":1}}}}},"FreeProduct":{"properties":{"type":{"const":"free_product"},"params":{"type":"object","additionalProperties":false,"required":["product","qty","mode"],"description":"Offer a product. add_line: the result asks the POS to add the line at price 0 (flag remizo_free_product). discount_if_present: 100% discount on the product if already in the ticket, otherwise the action is skipped (reason free_product_absent).","properties":{"product":{"type":"object","additionalProperties":false,"properties":{"ean":{"type":"string","pattern":"^[0-9]{8,14}$"},"ref":{"type":"string","maxLength":64},"name":{"type":"string","maxLength":200}},"anyOf":[{"required":["ean"]},{"required":["ref"]}]},"qty":{"type":"integer","minimum":1,"default":1},"mode":{"type":"string","enum":["add_line","discount_if_present"]},"perLotOf":{"$ref":"#/components/schemas/Offer/$defs/NodeId","description":"Repeat per complete lot of this article_lot criterion."}}}}},"LotPrice":{"properties":{"type":{"const":"lot_price"},"params":{"type":"object","additionalProperties":false,"required":["lotCriterionId","lotPriceCents"],"description":"Set the price of each complete lot (discount = lot units total - lotPriceCents, spread pro rata).","properties":{"lotCriterionId":{"$ref":"#/components/schemas/Offer/$defs/NodeId"},"lotPriceCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"maxLots":{"type":"integer","minimum":1}}}}},"PointsFixed":{"properties":{"type":{"const":"points_fixed"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode","amount"],"description":"Credit a fixed number of units on a counter (points, cashback cents, visits...).","properties":{"counterTypeCode":{"type":"string"},"amount":{"type":"integer","minimum":1},"activationDelayDays":{"type":"integer","minimum":0,"default":0},"expiry":{"$ref":"#/components/schemas/Offer/$defs/Expiry"}}}}},"PointsPerAmount":{"properties":{"type":{"const":"points_per_amount"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode","points","perCents"],"description":"Credit `points` units for every `perCents` of the base amount (e.g. 1 point per 100 cents).","properties":{"counterTypeCode":{"type":"string"},"points":{"type":"integer","minimum":1},"perCents":{"type":"integer","minimum":1},"base":{"type":"string","enum":["gross","pre_tax","eligible","after_discounts","target_lines"],"default":"after_discounts","description":"after_discounts = eligible amount minus Remizo discounts of lower or equal levels."},"target":{"$ref":"#/components/schemas/Offer/$defs/Target","description":"Required when base = target_lines."},"rounding":{"$ref":"#/components/schemas/Offer/$defs/Rounding"},"minBaseCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"maxPoints":{"type":"integer","minimum":1},"activationDelayDays":{"type":"integer","minimum":0,"default":0},"expiry":{"$ref":"#/components/schemas/Offer/$defs/Expiry"}}}}},"PointsMultiplier":{"properties":{"type":{"const":"points_multiplier"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode","multiplierBp"],"description":"Multiply the units credited on this ticket by other offers (lower levels only). 20000 = x2.","properties":{"counterTypeCode":{"type":"string"},"multiplierBp":{"type":"integer","minimum":1,"maximum":1000000},"appliesTo":{"type":"string","enum":["all_earned","offer_ids"],"default":"all_earned"},"offerIds":{"type":"array","items":{"type":"string","format":"uuid"}},"rounding":{"$ref":"#/components/schemas/Offer/$defs/Rounding"}}}}},"CounterUpdate":{"properties":{"type":{"const":"counter_update"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode","op"],"description":"Technical counter operation (visits, stamps...). decrement fails the action (not the offer) when insufficient unless allowNegative on the counter type.","properties":{"counterTypeCode":{"type":"string"},"op":{"type":"string","enum":["increment","decrement","set","reset"]},"amount":{"type":"integer","minimum":0},"expiry":{"$ref":"#/components/schemas/Offer/$defs/Expiry"}}}}},"CouponIssue":{"properties":{"type":{"const":"coupon_issue"},"params":{"type":"object","additionalProperties":false,"required":["templateId"],"description":"Issue a coupon to the customer (or anonymous ticket coupon when no customer). Rendering (png/pdf) happens asynchronously on the hub.","properties":{"templateId":{"type":"string","format":"uuid"},"quantity":{"type":"integer","minimum":1,"maximum":10,"default":1},"validity":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["template_default","fixed","rolling"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"startDelayDays":{"type":"integer","minimum":0,"default":0},"days":{"type":"integer","minimum":1}}},"render":{"type":"string","enum":["inherit","none","png","pdf"],"default":"inherit","description":"inherit = offer.coupon.render."},"faceValue":{"type":"object","additionalProperties":false,"required":["kind","value"],"description":"Value printed on the coupon (informational; the discount itself is carried by the offer triggered by this coupon).","properties":{"kind":{"type":"string","enum":["amount_cents","percent_bp","points","text"]},"value":{"type":["integer","string"]}}},"linkedOfferId":{"type":"string","format":"uuid","description":"Offer (trigger.type = coupon) that this coupon unlocks; printed on the coupon."}}}}},"Message":{"properties":{"type":{"const":"message"},"params":{"type":"object","additionalProperties":false,"required":["text"],"description":"Text returned to the POS. Placeholders: {{customer.firstName}}, {{balance.<counterTypeCode>.available}}, {{offer.name}}, {{discount.total}}, {{points.earned}}.","properties":{"placement":{"type":"string","enum":["ticket","screen","both"],"default":"ticket"},"text":{"$ref":"#/components/schemas/Offer/$defs/I18nText"},"onlyWhenApplied":{"type":"boolean","default":true}}}}},"CustomerTag":{"properties":{"type":{"const":"customer_tag"},"params":{"type":"object","additionalProperties":false,"required":["tag","op"],"properties":{"tag":{"type":"string","maxLength":40},"op":{"type":"string","enum":["add","remove"]}}}}},"CustomerStatus":{"properties":{"type":{"const":"customer_status"},"params":{"type":"object","additionalProperties":false,"required":["status"],"description":"Set the loyalty status (tier). Applied at commit.","properties":{"status":{"type":"string","maxLength":32},"onlyUpgrade":{"type":"boolean","default":true}}}}},"CustomEvent":{"properties":{"type":{"const":"custom_event"},"params":{"type":"object","additionalProperties":false,"required":["eventType"],"description":"Emit a customer event (customer_events table + RabbitMQ custom.event) usable by event-triggered offers and webhooks.","properties":{"eventType":{"type":"string","maxLength":64},"payload":{"type":"object","additionalProperties":true}}}}},"StampCard":{"properties":{"type":{"const":"stamp_card"},"params":{"type":"object","additionalProperties":false,"required":["counterTypeCode","stampsRequired","reward"],"description":"Stamp card: adds stampsPerVisit stamps (once per ticket) to the counter; each time the card is full (stampsRequired), the reward is granted and the card restarts with the extra stamps.","properties":{"counterTypeCode":{"type":"string","description":"Counter holding the stamps of the current card (kind visits)."},"stampsRequired":{"type":"integer","minimum":2,"maximum":50},"stampsPerVisit":{"type":"integer","minimum":1,"maximum":10,"default":1},"reward":{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string","enum":["coupon","points"]},"templateId":{"type":"string","format":"uuid","description":"Coupon template (type coupon)."},"quantity":{"type":"integer","minimum":1,"maximum":10,"default":1},"counterTypeCode":{"type":"string","description":"Points counter credited (type points)."},"amount":{"type":"integer","minimum":1,"description":"Points credited per full card (type points)."},"label":{"type":"string","maxLength":120,"description":"Reward shown on the card (e.g. a free coffee)."}}},"design":{"type":"object","additionalProperties":false,"description":"Card look (studio, POS, customer apps).","properties":{"title":{"type":"string","maxLength":60},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"accent":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"icon":{"type":"string","maxLength":32}}}}}}}},"Stacking":{"type":"object","additionalProperties":false,"required":["level","mode"],"description":"Levels run in ascending order; level N sees the ticket after discounts of levels < N. Within one level: offers in mode cumulative all apply; among offers in mode best_offer only the one with the highest total value (discount cents, then points) applies, the others are rejected with reason lost_best_offer; offers in mode best_line compete per line: on each line only the best discount wins (lost_best_line). Priority breaks ties (higher first) and orders cumulative execution.","properties":{"level":{"type":"integer","minimum":0,"maximum":99},"mode":{"type":"string","enum":["cumulative","best_offer","best_line"]},"priority":{"type":"integer","minimum":0,"maximum":1000,"default":100},"exclusions":{"type":"array","uniqueItems":true,"items":{"type":"string","format":"uuid"},"description":"Offer ids that cannot apply on the same ticket as this one. Symmetric: the engine treats A excludes B as B excludes A; the lower-priority (or later-level) one is rejected with reason excluded_by."},"exclusiveTags":{"type":"array","uniqueItems":true,"items":{"type":"string","maxLength":40},"description":"Offers sharing one of these tags are mutually exclusive on a ticket."},"exclusive":{"type":"boolean","default":false,"description":"When applied, no other offer of a higher level applies on this ticket."}}},"Limits":{"type":"object","additionalProperties":false,"description":"Budgets. Counters are checked at evaluation and consumed at commit (see architecture.md, limits).","properties":{"global":{"type":"object","additionalProperties":false,"properties":{"maxUses":{"type":"integer","minimum":1,"description":"Total number of tickets the offer may apply to."},"maxAmountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents","description":"Total discount budget."},"maxPoints":{"type":"integer","minimum":1},"maxCoupons":{"type":"integer","minimum":1},"onExhausted":{"type":"string","enum":["reject","suspend"],"default":"reject","description":"suspend = also flip the offer status to suspended (event offer.budget_exhausted)."}}},"perCustomer":{"type":"object","additionalProperties":false,"properties":{"maxUses":{"type":"integer","minimum":1},"maxAmountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"},"period":{"$ref":"#/components/schemas/Offer/$defs/Period","default":"ever"},"periodDays":{"type":"integer","minimum":1},"identifyBy":{"type":"string","enum":["customer","card"],"default":"customer"},"whenAnonymous":{"type":"string","enum":["allow","reject"],"default":"allow","description":"Behaviour when the ticket has no identified customer."}}},"perTicket":{"type":"object","additionalProperties":false,"properties":{"maxApplications":{"type":"integer","minimum":1,"default":1,"description":"How many times the offer may repeat in one ticket (e.g. once per lot)."},"maxDiscountCents":{"$ref":"#/components/schemas/Offer/$defs/Cents"}}},"perStore":{"type":"object","additionalProperties":false,"properties":{"maxUses":{"type":"integer","minimum":1},"period":{"$ref":"#/components/schemas/Offer/$defs/Period","default":"ever"},"periodDays":{"type":"integer","minimum":1}}}}},"Validity":{"type":"object","additionalProperties":false,"required":["from"],"description":"Hard activation window; outside it the engine does not even load the offer (reason not_in_validity when evaluated through the simulator). Finer schedules use date_range / time_range / weekdays criteria.","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":["string","null"],"format":"date-time","description":"null = open-ended."},"timezone":{"type":"string","description":"IANA timezone for calendar computations; default workspace timezone."}}},"CouponRenderDefaults":{"type":"object","additionalProperties":false,"required":["render"],"description":"Default rendering for coupons issued by this offer. none = code only (default).","properties":{"render":{"type":"string","enum":["none","png","pdf"],"default":"none"},"templateId":{"type":["string","null"],"format":"uuid","description":"Coupon template whose design is rendered. Required when render != none."}},"if":{"properties":{"render":{"enum":["png","pdf"]}}},"then":{"required":["templateId"]}}}}}}}