{
  "openapi": "3.1.0",
  "info": {
    "title": "Dee Dog See Cat Loyalty Integration API",
    "version": "1.3.0",
    "description": "ERP HQ is the only external authority. PetPOS -> Branch Server -> ERP HQ -> Website Loyalty API. Enroll one HQ in Profile and Settings; do not distribute its API key to POS or branches. Pre-2.6.13 keys are not automatically trusted. HQ publishes earning rules; website administration is read-only for those rules. The website holds the single Loyalty ledger. Any new sale or payment spending stamps/PTS must reserve, then commit or release. quote is not a reservation. Reservations expire after 30-900 seconds (default 300), capped by reserved wallet expiry. Commit uses the exact transaction, allocations and approved rule snapshot captured at reservation time. Stamps are whole units and points are decimal PTS strings. Outbound synchronization remains cursor-based pull; no real-money payment processing. Point wallets use earliest effective expiry first (FEFO), with purchase time as tie breaker. Promotional points expire at the end of expiresOn in Asia/Bangkok. Premium points expire 365 days after first use. Refunds preserve each original wallet expiry. Membership pet suffixes have at least three digits (-001, -002); row_key and member_number remain stable. API dates remain ISO Gregorian; user interfaces display Buddhist calendar dates."
  },
  "servers": [
    {
      "url": "/api/integrations/loyalty/v1",
      "description": "Prefix with the site base path when installed under a subdirectory."
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/status": {
      "get": {
        "operationId": "status",
        "summary": "Connection, permissions and branches",
        "description": "Required scope: any valid key.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/customers": {
      "get": {
        "operationId": "exportCustomers",
        "summary": "Export updated customers and merge redirects",
        "description": "Required scope: customers:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC3339 updated timestamp from next.since."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Last cursor; for customer export this is the ID paired with since."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "1–200, default 100."
          }
        ]
      },
      "post": {
        "operationId": "upsertCustomer",
        "summary": "Create/link customer or update name with expected revision",
        "description": "Required scope: customers:write.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerInput"
              }
            }
          }
        }
      }
    },
    "/customers/lookup": {
      "get": {
        "operationId": "lookupCustomer",
        "summary": "Find member and current stamps / PTS balance",
        "description": "Required scope: customers:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "customerId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "customerId"
          },
          {
            "name": "externalId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "externalId"
          },
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "phone"
          }
        ]
      }
    },
    "/rules": {
      "get": {
        "operationId": "getRules",
        "summary": "Read source-specific earning rules",
        "description": "Required scope: rules:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "upsertRule",
        "summary": "Create/update a per-amount or fixed earning rule",
        "description": "Required scope: rules:write.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuleInput"
              }
            }
          }
        }
      }
    },
    "/quote": {
      "post": {
        "operationId": "quote",
        "summary": "Validate an uncommitted sale, grant or payment",
        "description": "Enrolled ERP HQ only. Calculates available balances and earnings without reserving. Use reservations before any stamp/PTS payment.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteInput"
              }
            }
          }
        }
      }
    },
    "/transactions": {
      "post": {
        "operationId": "recordTransaction",
        "summary": "Record unreserved earning-only sale, HQ grant, proportional refund or void; new payments must reserve and commit.",
        "description": "Required scope: transactions:write (plus grants:write for grant).",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransactionInput"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "exportTransactions",
        "summary": "Read one receipt by externalId, or export this connection in ascending sequence",
        "description": "Required scope: transactions:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "externalId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "externalId"
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Last cursor; for customer export this is the ID paired with since."
          },
          {
            "name": "snapshot",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Preserve while reading pages of events/transactions."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "1–200, default 100."
          },
          {
            "name": "branch",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "branch"
          }
        ]
      }
    },
    "/events": {
      "get": {
        "operationId": "exportEvents",
        "summary": "Export global member ledger events in permitted branches",
        "description": "Required scope: events:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Last cursor; for customer export this is the ID paired with since."
          },
          {
            "name": "snapshot",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Preserve while reading pages of events/transactions."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "1–200, default 100."
          },
          {
            "name": "branch",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "branch"
          },
          {
            "name": "customerId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "customerId"
          }
        ]
      }
    },
    "/catalog": {
      "get": {
        "operationId": "catalog",
        "summary": "Read published available rewards and point packages",
        "description": "Required scope: catalog:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/reservations": {
      "post": {
        "operationId": "reservePayment",
        "summary": "Reserve both currencies before closing a POS bill",
        "description": "Enrolled ERP HQ only. Required scope: transactions:write.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReservationInput"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "readReservations",
        "summary": "Read a reservation by id, or the latest 100 reservations",
        "description": "Enrolled ERP HQ only. Required scope: transactions:read.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "branch",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Branch"
            }
          }
        ]
      }
    },
    "/reservations/commit": {
      "post": {
        "operationId": "commitPayment",
        "summary": "Atomically post the frozen approved transaction and consume its reservation",
        "description": "Enrolled ERP HQ only. Required scope: transactions:write.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReservationCommit"
              }
            }
          }
        }
      }
    },
    "/reservations/release": {
      "post": {
        "operationId": "releasePayment",
        "summary": "Release an uncommitted hold with an idempotent command reference",
        "description": "Enrolled ERP HQ only. Required scope: transactions:write.",
        "responses": {
          "200": {
            "description": "Success. Mutations with a repeated identical reference return the same result plus duplicate: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fields or units",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired, revoked or paused key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or branch access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record or route not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate reference conflicts, stale revision, insufficient balance or consumed award",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "JSON exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "600 requests per minute per key. Retry-After: 60",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error; retry with the identical external reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReservationRelease"
              }
            }
          }
        }
      }
    },
    "/members": {
      "get": {
        "summary": "Approved membership event feed for ERP HQ; requires members:read",
        "operationId": "membershipEvents",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "required": false
          },
          {
            "name": "snapshot",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "required": false
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "required": false
          },
          {
            "name": "branch",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "schemaVersion 1, events (seq, id, kind, member, rows), hasMore, nextCursor, snapshot. Stable row_key matches Excel and Google Sheets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MembershipFeed"
                }
              }
            }
          },
          "403": {
            "description": "HQ key lacks members:read or branch permission"
          },
          "409": {
            "description": "Conflict; retry with a valid cursor and snapshot"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ddsc_<64 lowercase hex digits>"
      }
    },
    "schemas": {
      "ExternalId": {
        "type": "string",
        "minLength": 1,
        "maxLength": 120
      },
      "Decimal": {
        "type": "string",
        "pattern": "^(?:0|[1-9]\\d{0,7})(?:\\.\\d{1,2})?$",
        "description": "THB or PTS decimal text, at most 2 fraction digits. Maximum positive value 10000000.00."
      },
      "Branch": {
        "enum": ["chaengwattana", "westgate", "villagehub"]
      },
      "Units": {
        "type": "object",
        "properties": {
          "stamps": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000
          },
          "points": {
            "$ref": "#/components/schemas/Decimal"
          }
        },
        "required": [],
        "additionalProperties": false
      },
      "CustomerSelector": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              }
            },
            "required": ["id"],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "externalId": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              }
            },
            "required": ["externalId"],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "phone": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              }
            },
            "required": ["phone"],
            "additionalProperties": false
          }
        ]
      },
      "CustomerInput": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "phone": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 160
          },
          "commandId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "revision": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": ["externalId", "phone", "name"],
        "additionalProperties": false
      },
      "RuleInput": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "commandId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "revision": {
            "type": "integer",
            "minimum": 0
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 160
          },
          "active": {
            "type": "boolean"
          },
          "currency": {
            "enum": ["stamps", "points"]
          },
          "mode": {
            "enum": ["per_amount", "fixed"]
          },
          "basis": {
            "enum": ["eligibleAmount", "cashAmount"]
          },
          "spendEvery": {
            "$ref": "#/components/schemas/Decimal"
          },
          "reward": {
            "oneOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000000
              },
              {
                "$ref": "#/components/schemas/Decimal"
              }
            ]
          },
          "minimumAmount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "maximumReward": {
            "oneOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000000
              },
              {
                "$ref": "#/components/schemas/Decimal"
              }
            ]
          },
          "branches": {
            "type": "array",
            "minItems": 1,
            "maxItems": 3,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Branch"
            }
          },
          "startDate": {
            "type": "string",
            "format": "date"
          },
          "endDate": {
            "type": "string",
            "format": "date"
          }
        },
        "required": [
          "externalId",
          "commandId",
          "revision",
          "name",
          "active",
          "currency",
          "mode",
          "basis",
          "reward",
          "branches"
        ],
        "additionalProperties": false
      },
      "SelectedRule": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "revision": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": ["externalId", "revision"],
        "additionalProperties": false
      },
      "Sale": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "customer": {
            "$ref": "#/components/schemas/CustomerSelector"
          },
          "branch": {
            "$ref": "#/components/schemas/Branch"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "sale"
          },
          "amount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "eligibleAmount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "cashAmount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "spend": {
            "$ref": "#/components/schemas/Units"
          },
          "rules": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/SelectedRule"
            }
          },
          "pointPolicy": {
            "$ref": "#/components/schemas/PointPolicy"
          }
        },
        "required": ["externalId", "type", "customer", "branch", "amount", "rules"],
        "additionalProperties": false
      },
      "Grant": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "customer": {
            "$ref": "#/components/schemas/CustomerSelector"
          },
          "branch": {
            "$ref": "#/components/schemas/Branch"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "grant"
          },
          "earn": {
            "$ref": "#/components/schemas/Units"
          },
          "pointPolicy": {
            "$ref": "#/components/schemas/PointPolicy"
          }
        },
        "required": ["externalId", "type", "customer", "branch", "earn"],
        "additionalProperties": false
      },
      "Payment": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "customer": {
            "$ref": "#/components/schemas/CustomerSelector"
          },
          "branch": {
            "$ref": "#/components/schemas/Branch"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "payment"
          },
          "spend": {
            "$ref": "#/components/schemas/Units"
          }
        },
        "required": ["externalId", "type", "customer", "branch", "spend"],
        "additionalProperties": false
      },
      "Refund": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "refund"
          },
          "originalExternalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "amount": {
            "$ref": "#/components/schemas/Decimal"
          }
        },
        "required": ["externalId", "type", "originalExternalId", "amount"],
        "additionalProperties": false
      },
      "Void": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "void"
          },
          "originalExternalId": {
            "$ref": "#/components/schemas/ExternalId"
          }
        },
        "required": ["externalId", "type", "originalExternalId"],
        "additionalProperties": false
      },
      "TransactionInput": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/SaleWithoutTender"
          },
          {
            "$ref": "#/components/schemas/Grant"
          },
          {
            "$ref": "#/components/schemas/Refund"
          },
          {
            "$ref": "#/components/schemas/Void"
          }
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "requestId": {
            "type": "string"
          }
        },
        "required": ["error", "code", "requestId"],
        "additionalProperties": false
      },
      "Object": {
        "type": "object",
        "additionalProperties": true
      },
      "QuoteInput": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/Sale"
          },
          {
            "$ref": "#/components/schemas/Grant"
          },
          {
            "$ref": "#/components/schemas/Payment"
          }
        ]
      },
      "ReservationInput": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "transaction": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Sale"
              },
              {
                "$ref": "#/components/schemas/Payment"
              }
            ]
          },
          "expiresInSeconds": {
            "type": "integer",
            "minimum": 30,
            "maximum": 900,
            "default": 300
          }
        },
        "required": ["externalId", "transaction"],
        "additionalProperties": false
      },
      "ReservationCommit": {
        "type": "object",
        "properties": {
          "reservationId": {
            "$ref": "#/components/schemas/ExternalId"
          }
        },
        "required": ["reservationId"],
        "additionalProperties": false
      },
      "ReservationRelease": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "reservationId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": ["externalId", "reservationId"],
        "additionalProperties": false
      },
      "SaleWithoutTender": {
        "type": "object",
        "properties": {
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "customer": {
            "$ref": "#/components/schemas/CustomerSelector"
          },
          "branch": {
            "$ref": "#/components/schemas/Branch"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "staffName": {
            "type": "string",
            "maxLength": 120
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          },
          "type": {
            "const": "sale"
          },
          "amount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "eligibleAmount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "cashAmount": {
            "$ref": "#/components/schemas/Decimal"
          },
          "rules": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/SelectedRule"
            }
          },
          "pointPolicy": {
            "$ref": "#/components/schemas/PointPolicy"
          }
        },
        "required": ["externalId", "type", "customer", "branch", "amount", "rules"],
        "additionalProperties": false
      },
      "Reservation": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "externalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "transactionExternalId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "customerId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "branch": {
            "$ref": "#/components/schemas/Branch"
          },
          "stamps": {
            "type": "integer",
            "minimum": 0
          },
          "points": {
            "$ref": "#/components/schemas/Decimal"
          },
          "status": {
            "enum": ["held", "committed", "released", "expired"]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": ["id", "status", "expiresAt", "stamps", "points"]
      },
      "ReservationResult": {
        "type": "object",
        "properties": {
          "success": {
            "const": true
          },
          "reservationId": {
            "$ref": "#/components/schemas/ExternalId"
          },
          "reservation": {
            "$ref": "#/components/schemas/Reservation"
          },
          "committed": {
            "type": "boolean"
          },
          "duplicate": {
            "type": "boolean"
          }
        }
      },
      "MembershipRow": {
        "type": "object",
        "description": "Stable flat record; keys match Members worksheet. All values are strings.",
        "properties": {
          "pet_number": {
            "type": "string",
            "description": "Member number followed by a hyphen and a minimum three-digit sequence, e.g. DD001QC25690001-001. Appended pets use the next number after the largest existing suffix; stable row_key is unchanged."
          }
        },
        "additionalProperties": {
          "type": "string"
        }
      },
      "MembershipFeed": {
        "type": "object",
        "required": ["schemaVersion", "events", "nextCursor", "snapshot", "hasMore"],
        "properties": {
          "schemaVersion": {
            "const": 1
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "seq": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "kind": {
                  "enum": ["approved", "customer_updated"]
                },
                "member": {
                  "type": "object"
                },
                "rows": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MembershipRow"
                  }
                }
              }
            }
          },
          "nextCursor": {
            "type": "integer"
          },
          "snapshot": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "PointPolicy": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "pointType": {
            "type": "string",
            "enum": ["premium", "promotion"],
            "default": "premium"
          },
          "expiresOn": {
            "type": "string",
            "format": "date",
            "description": "Required for promotion. ISO Gregorian date; valid through 23:59:59.999 Asia/Bangkok on this date."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "pointType": {
                  "const": "promotion"
                }
              },
              "required": ["pointType"]
            },
            "then": {
              "required": ["expiresOn"]
            }
          }
        ]
      }
    }
  },
  "x-governance": {
    "externalAuthority": "enrolled_erp_hq",
    "masterOwner": "ERP HQ",
    "ledgerOwner": "Website Loyalty",
    "reservationFlow": ["reserve", "commit", "release"],
    "quoteReserves": false
  }
}
