{
  "openapi": "3.0.1",
  "info": {
    "title": "GRUBBRR Partner Ordering API",
    "description": "Partner-facing Ordering API for voice-AI and drive-through integrations. **Base path** `partner/v1`.\n\n### Authentication\n\nCallers authenticate as a **Partner Ordering device** (JWT claim `deviceType=ExternalDevice`). Your GRUBBRR administrator provisions the device once and gives you a single-use **enrollment code**. Then:\n\n1. **Register** — redeem the enrollment code at `POST /auth/register` to get the device's first **refresh token**.\n2. **Refresh** — exchange the refresh token at `POST /auth/refresh` for a short-lived **access token** (this also rotates the refresh token, so persist the new one each time).\n3. **Authorize** — send the access token as `Authorization: Bearer <token>` on every menu and ordering call.\n\n### Endpoints at a glance\n\n<table>\n<thead><tr><th>Method &amp; path</th><th>Purpose</th></tr></thead>\n<tbody>\n<tr><td><code>POST /auth/register</code></td><td>Redeem enrollment code &rarr; first refresh token</td></tr>\n<tr><td><code>POST /auth/refresh</code></td><td>Rotate refresh token &rarr; short-lived access token</td></tr>\n<tr><td><code>GET /menu</code></td><td>Normalized menu</td></tr>\n<tr><td><code>GET /menu/availability</code></td><td>Authoritative sold-out / 86 list</td></tr>\n<tr><td><code>POST /orders/validate</code></td><td>POS-calculated totals (no order created)</td></tr>\n<tr><td><code>POST /orders/submit</code></td><td>Submit order &rarr; <code>202 Accepted</code>, <code>status: PENDING</code></td></tr>\n<tr><td><code>GET /orders/status/{sessionId}</code></td><td>Poll until terminal <code>SUBMITTED</code> / <code>FAILED</code></td></tr>\n</tbody>\n</table>\n\n### Order lifecycle\n\n**validate → submit → poll status**\n\n- **`POST /orders/validate`** returns POS-calculated totals **without** creating an order.\n- **`POST /orders/submit`** accepts the order and returns `202 Accepted` with `status: PENDING` before the POS confirms. It is **idempotent on `sessionId`**, so a safe retry returns the same order.\n- **`GET /orders/status/{sessionId}`** — poll until the order reaches a terminal `SUBMITTED` or `FAILED` state.\n\n> Company and location always come from the access token — **never** the request body.\n\n### Errors\n\nOrdering and menu endpoints return errors as an RFC 7807 `application/problem+json` envelope with a stable, machine-readable `errorCode`. **Branch on `errorCode`** — not the HTTP status or the human-readable `detail` (which may change).\n\n### Rate limits\n\nCalls are rate-limited **per location** at the gateway. Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header — wait that many seconds before retrying.",
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://api.nge.grubbrr.com/api/partner/v1"
    }
  ],
  "paths": {
    "/auth/refresh": {
      "post": {
        "tags": [
          "PartnerAuth"
        ],
        "summary": "Exchanges a refresh token for a fresh access token (valid 3 days) plus a rotated, single-use\r\nrefresh token (valid 14 days). Call this on demand before the access token expires. Anonymous\r\nbecause the refresh token is itself the credential. Returns 401 when the token is invalid,\r\nexpired, not a Partner Ordering token, or its refresh code has already been rotated.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRefreshRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRefreshRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRefreshRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRefreshRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/auth/register": {
      "post": {
        "tags": [
          "PartnerAuth"
        ],
        "summary": "Redeems a single-use enrollment code (issued by GRUBBRR) to activate your device and obtain its\r\ndevice id plus initial refresh token. Anonymous because the code is itself the credential. Returns\r\n401 for an unknown or expired code, or one that has already been redeemed; 403 when the location is\r\nnot licensed or the device has been disabled.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/discounts": {
      "get": {
        "tags": [
          "PartnerDiscounts"
        ],
        "summary": "Returns the discounts the partner can apply at the token's location: each entry's `id` is the\r\nvalue to apply on an order (code-based entries additionally expose their `codes`). Only\r\npartner-applicable discounts are returned — code-based and user-selectable; auto-applied discounts are\r\nomitted because the order pipeline applies those itself. Each entry includes its value (type),\r\nflat eligibility rules and scope. An empty list means the location has no partner-applicable\r\ndiscounts configured. The location is derived from the token — never from request input.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountsDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/login": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Signs a loyalty customer in for the session. The body carries the client-generated\r\n`sessionId` plus exactly one identifier (`phoneNumber`, `email`, or `loginCode`).\r\nThe look-up method is gated against the location provider's capabilities (`login_method_not_supported`,\r\n422) before any provider call. A matched account returns the loyalty-user context the client carries\r\non subsequent calls; no match returns `200` with `loyaltyAttached: false`.",
        "requestBody": {
          "description": "The sign-in request (session id + single identifier).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/rewards": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Returns the rewards available to the signed-in loyalty customer. Stateless: the body\r\ncarries the `sessionId` and the `loyaltyUser` the client received from `/loyalty/login`.\r\nCart-scoped rewards remain available via `POST /loyalty/rewards/applicable`.",
        "requestBody": {
          "description": "The rewards request (session id + client-carried loyalty user).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/rewards/applicable": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Returns the rewards applicable to a specific cart. Stateless: the body carries the\r\n`sessionId`, the `loyaltyUser` from `/loyalty/login`, the menu `order`, and its\r\n`orderDetails` totals. Only cart-driven providers support this; otherwise responds\r\n`applicable_rewards_not_supported` (422) directing the client to `POST /loyalty/rewards`.",
        "requestBody": {
          "description": "The applicable-rewards request (session id + loyalty user + order + order details).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplicableRewardsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/rewards/apply": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Validates (applies) the cart's reward(s) and returns the dollar amount they deduct from the subtotal.\r\nThe reward is <b>not</b> burned here — redemption happens at `/loyalty/redeem` after\r\nsubmit. Stateless: the body carries the `sessionId`, the `loyaltyUser`, and the menu\r\n`order` (with its applied `rewards`) and its `orderDetails`. A rejected reward responds\r\n`reward_not_applicable` (422, echoing the reward id).",
        "requestBody": {
          "description": "The apply request (session id + loyalty user + order [with rewards] + order details).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/redeem": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Redeems (burns) the cart's reward(s) after the order has been submitted and returns the\r\nprovider `loyaltyTransactionId`. Stateless: the body carries the `sessionId`, the\r\n`loyaltyUser`, and the submitted menu `order` (with its applied `rewards`) and its\r\n`orderDetails`. GRUBBRR does not persist the transaction id — the client retains it to call\r\n`/loyalty/void` later.",
        "requestBody": {
          "description": "The redeem request (session id + loyalty user + order [with rewards] + order details).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/guest/submit": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Submits an order to the loyalty provider for a <b>guest</b> — no signed-in loyalty user —\r\nand returns the provider `loyaltyTransactionId`. The guest counterpart of `/loyalty/redeem`:\r\nthe body carries the `sessionId`, the submitted menu `order`, and its `orderDetails`,\r\nbut no `loyaltyUser`. GRUBBRR does not persist the transaction id — the client retains it to call\r\n`/loyalty/redeem/void` later.",
        "requestBody": {
          "description": "The guest submit request (session id + order + order details).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/loyalty/redeem/void": {
      "post": {
        "tags": [
          "PartnerLoyalty"
        ],
        "summary": "Voids a previously completed redemption. It carries the same stateless context as redeem so\r\nthe provider can reconcile the void against the original order: the `sessionId`, the\r\n`loyaltyUser`, the `loyaltyTransactionId` from `/loyalty/redeem`, and the redeemed menu\r\n`order` + its `orderDetails`. Callable in-session or later from the partner back-office. A\r\nprovider rejection / already-voided redemption responds `void_failed` (422, echoing the transaction id).",
        "requestBody": {
          "description": "The void request (session id + loyalty user + transaction id + order + order details).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/menu": {
      "get": {
        "tags": [
          "PartnerMenu"
        ],
        "summary": "Returns the normalized menu for the token's location: a category tree plus top-level lists of items,\r\ncombos, components, modifier groups, modifiers and codes (each emitted once, linked by id). Sold-out / 86\r\nstate is returned separately as GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerMenuDto.Unavailable (same source as\r\n`GET /menu/availability`) so the menu body can be cached independently. Location is from the token.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerMenuDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/menu/availability": {
      "get": {
        "tags": [
          "PartnerMenu"
        ],
        "summary": "Returns the authoritative availability snapshot for the token's location: the current, complete\r\nlist of sold-out / 86'd entities (id, name, type, status), normalized across POS systems. This\r\nendpoint is the source of truth for availability. The location is derived from the token — never\r\nfrom request input.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.MenuAvailabilityDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/menu/tender-types": {
      "get": {
        "tags": [
          "PartnerMenu"
        ],
        "summary": "Returns the payment tender types configured for the token's location: each tender's\r\nPOS-native `tenderId`, a human-readable `name`, and the owning `paymentIntegrationId`.\r\nCalled at startup alongside\r\n`GET /menu` and `GET /menu/availability`. The client composes each `POST /orders/submit`\r\npayment (and each order-based loyalty payment) from a returned `tenderId` / `paymentIntegrationId`\r\npair. All configured tender types are returned (no filtering); an empty list is a valid response when\r\nnone are configured. The location is derived from the token — never from request input.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerTenderTypesDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/ping": {
      "get": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Authenticated readiness probe. Requires a valid device access token;\r\nreturns 401 otherwise. Confirms the partner authentication path is configured correctly.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApiStatus"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/health": {
      "get": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Unauthenticated infrastructure health probe used to verify gateway-to-service connectivity.\r\nExposes no business data.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApiStatus"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/orders/validate": {
      "post": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Validates a partner order against the POS without creating a POS order.",
        "description": "Use this endpoint before order submission to retrieve POS-calculated totals, taxes, and fees\r\nfor an order payload. The call uses the device identity from the access\r\ntoken for company/location scoping and does not create a POS order.\r\n<br>\r\nA `sessionId` is the order idempotency key: once it has produced a completed (submitted)\r\norder it cannot be reused to validate another, and the call responds `session_already_completed`\r\n(409). The partner must start a new order with a new `sessionId`.\r\n",
        "requestBody": {
          "description": "Partner order payload to validate.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/orders/submit": {
      "post": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Asynchronously submits a partner order to the POS and returns immediately with a PENDING ticket.",
        "description": "The order is recorded by GRUBBRR and submitted to the POS in the background, so the\r\nresponse returns `202 Accepted` with `status: \"PENDING\"` before the POS confirms.\r\nPoll `GET /orders/status/{sessionId}` for the terminal outcome (SUBMITTED with a posOrderId,\r\nor FAILED with an error code). The submit is idempotent on `sessionId`: repeating a submit\r\nreturns the existing order's current status without creating a duplicate or re-submitting.\r\nCompany/location are taken from the access token. No payment-transaction data is accepted or stored.",
        "requestBody": {
          "description": "Partner order payload to submit.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/orders/apply-coupon": {
      "post": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Resolves a coupon code against a partner cart and returns the resolved coupon when accepted.",
        "description": "Reuses the existing POS apply-coupon flow: the cart is sent to the POS, which resolves the coupon\r\ncode. On success the response carries `accepted: true` and the resolved `coupon` object,\r\nwhich the partner then sends on `POST /orders/validate` and `POST /orders/submit` via\r\ntheir `coupons[]` field. A code the POS does not recognize returns `200` with\r\n`accepted: false` and a `reason` rather than an error. Company/location are taken from\r\nthe access token.",
        "requestBody": {
          "description": "The cart and coupon code to resolve.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ValidationProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Client Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "504": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/orders/status/{sessionId}": {
      "get": {
        "tags": [
          "PartnerOrdering"
        ],
        "summary": "Returns the current lifecycle status of a previously submitted partner order, keyed by session id.",
        "description": "Lightweight polling endpoint: partners poll this after `POST /orders/submit` until the order\r\nreaches a terminal state. The response is projected by state — `PENDING` (no terminal fields),\r\n`SUBMITTED` (with `posOrderId`), or\r\n`FAILED` (with `errorCode` and `failureReason`); `SUBMITTED`/`FAILED` also\r\ncarry `resolvedAt`. The lookup is scoped to the access token's location, so a sessionId that\r\nis unknown OR belongs to another location returns `404` (`order_not_found`) identically —\r\nno cross-location data is leaked. Sessions are independent: polling one never affects another.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "description": "The partner session identifier used when the order was submitted.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/order-types": {
      "get": {
        "tags": [
          "PartnerOrderTypes"
        ],
        "summary": "Returns the GRUBBRR order types configured at the token's location: each entry's `id` is the value\r\nto send as `orderTypeId` on `POST /orders/validate` and `POST /orders/submit` (the order\r\npipeline matches that id against the location's configured order types). Each entry also carries an\r\n`identity` block describing the order- and customer-identity the location expects (table tent /\r\ntoken, plus the required/optional customer fields) so partners know what to collect and send on the\r\norder's `customer` object. An empty list means the location has no enabled order types configured.\r\nThe location is derived from the token — never from request input.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTypesDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Microsoft.AspNetCore.Mvc.ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.MenuAvailabilityDto": {
        "type": "object",
        "properties": {
          "unavailable": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.UnavailableEntityDto"
            },
            "description": "Entities currently unavailable (sold out / 86'd) at the location. Ids are GRUBBRR master ids,\r\nstable across POS systems and consistent with the ids returned by `GET /menu`; an empty\r\nlist means everything is currently available.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Authoritative availability snapshot for the token's location: the current, complete list of\r\nsold-out / 86'd entities. This endpoint - not the availability webhook - is the source of truth; the\r\nwebhook only signals that this list may have changed and is never the data source."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityDto": {
        "type": "object",
        "properties": {
          "availableFrom": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "availableUntil": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "times": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityWindowDto"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityWindowDto": {
        "type": "object",
        "properties": {
          "startTime": {
            "type": "string",
            "nullable": true
          },
          "endTime": {
            "type": "string",
            "nullable": true
          },
          "startDayOfWeek": {
            "type": "string",
            "nullable": true
          },
          "endDayOfWeek": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerCategoryNodeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "isAlcoholic": {
            "type": "boolean"
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "availability": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityDto"
          },
          "isSubCategory": {
            "type": "boolean",
            "description": "True when this is a subcategory; top-level categories are false."
          },
          "subCategoryIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "itemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "comboIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A category node; references items/combos/subcategories by id."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboChoiceDto": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "string",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean"
          },
          "price": {
            "type": "integer",
            "description": "Absolute selection price (minor units): price-list per-choice, else combo per-choice, else item base.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "A selectable item within a combo component; render its modifier tree from GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerMenuDto.Items."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboComponentDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "type": {
            "type": "string",
            "description": "Component role, e.g. Main / Side / Unknown.",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "minSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "displayOrder": {
            "type": "integer",
            "format": "int32"
          },
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboChoiceDto"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A combo component definition (emitted once); its choices reference items by id."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "integer",
            "description": "Combo price in minor units (price-list combo price when present, else base price).",
            "format": "int32"
          },
          "caloriesText": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          },
          "availability": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityDto"
          },
          "componentIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A combo definition (emitted once); references its components by id."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerDietaryTagDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerGroupModifierEdgeDto": {
        "type": "object",
        "properties": {
          "modifierId": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "integer",
            "description": "Group-level default price (minor units). Per-item prices live on the item's GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerItemDefDto.ModifierOverrides.",
            "format": "int32"
          },
          "minQuantity": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxQuantity": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "defaultQuantity": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean"
          },
          "isLocked": {
            "type": "boolean"
          },
          "modifierCodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierCodeRefDto"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A group-to-modifier link with per-group values."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerGroupRefDto": {
        "type": "object",
        "properties": {
          "groupId": {
            "type": "string",
            "nullable": true
          },
          "minSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "includedQuantity": {
            "type": "integer",
            "format": "int32"
          },
          "minAggregateCount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxAggregateCount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "isInvisible": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "description": "A per-context override of a modifier group's selection counts (item- or modifier-scoped). Emitted only when\r\nthe context actually overrides the group definition; a null count field means that value is not overridden\r\n(fall back to the group's GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierGroupDefDto value)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerItemDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "shortDescription": {
            "type": "string",
            "nullable": true
          },
          "longDescription": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "integer",
            "description": "Base price in minor units (price-list item price when present, else the base menu price).",
            "format": "int32"
          },
          "caloriesText": {
            "type": "string",
            "nullable": true
          },
          "isAlcoholic": {
            "type": "boolean"
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          },
          "allergenInfo": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "dietaryTagIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "availability": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerAvailabilityDto"
          },
          "modifierGroupIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ids of the item's top-level modifier groups, display-ordered.",
            "nullable": true
          },
          "modifierGroupOverrides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerGroupRefDto"
            },
            "description": "Per-item group setting deltas. Sparse: only groups that carry an actual override are listed — a group\r\npresent in GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerItemDefDto.ModifierGroupIds with no entry here uses its definition (GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierGroupDefDto) values.",
            "nullable": true
          },
          "modifierOverrides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierOverrideDto"
            },
            "description": "Per-item modifier overrides (quantities, default, per-item price), keyed by (group, modifier).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An item definition (emitted once) with its top-level groups and per-item overrides."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerMenuDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Resolved menu master id for the location.",
            "nullable": true
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerCategoryNodeDto"
            },
            "description": "All categories (top-level and sub), flat. A parent links children via GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerCategoryNodeDto.SubCategoryIds.",
            "nullable": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerItemDefDto"
            },
            "nullable": true
          },
          "combos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboDefDto"
            },
            "nullable": true
          },
          "comboComponents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerComboComponentDefDto"
            },
            "nullable": true
          },
          "modifierGroups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierGroupDefDto"
            },
            "nullable": true
          },
          "modifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierDefDto"
            },
            "nullable": true
          },
          "modifierCodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierCodeDefDto"
            },
            "nullable": true
          },
          "dietaryTags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerDietaryTagDto"
            },
            "nullable": true
          },
          "unavailable": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.UnavailableEntityDto"
            },
            "description": "Current sold-out / 86'd entities; same shape and source as `GET /menu/availability`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Partner-facing menu contract. Normalized: every entity is emitted once in a flat top-level list\r\nand linked by id; references carry per-context overrides. Prices are integer minor units. Availability is\r\ndecoupled - the sold-out / 86'd set is returned in GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerMenuDto.Unavailable (same source as\r\n`GET /menu/availability`) so the menu body can be cached independently."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierCodeDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "behavior": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A prep/instruction code definition (e.g. \"Extra\", \"No\"). GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierCodeDefDto.Behavior: None/Negative/Extra/Add."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierCodeRefDto": {
        "type": "object",
        "properties": {
          "codeId": {
            "type": "string",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "description": "A reference to a modifier code from a group-to-modifier link."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "caloriesText": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          },
          "allergenInfo": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "dietaryTagIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "nestedModifierGroupIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ids of the modifier's nested modifier groups, display-ordered.",
            "nullable": true
          },
          "modifierGroupOverrides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerGroupRefDto"
            },
            "description": "Per-modifier nested-group setting deltas. Sparse: only nested groups that carry an actual override are\r\nlisted — a group present in GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierDefDto.NestedModifierGroupIds with no entry here uses its definition values.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A modifier definition (emitted once) with its nested group references."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierGroupDefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "nullable": true
          },
          "minSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxSelection": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "includedQuantity": {
            "type": "integer",
            "format": "int32"
          },
          "minAggregateCount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxAggregateCount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "modifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerGroupModifierEdgeDto"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A modifier-group definition (emitted once) with its group-level modifier links."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerModifierOverrideDto": {
        "type": "object",
        "properties": {
          "groupId": {
            "type": "string",
            "nullable": true
          },
          "modifierId": {
            "type": "string",
            "nullable": true
          },
          "minQuantity": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "maxQuantity": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean"
          },
          "isInvisible": {
            "type": "boolean"
          },
          "isLocked": {
            "type": "boolean"
          },
          "price": {
            "type": "integer",
            "description": "Per-item price-list price (minor units), when one exists; else null (use the group-level default price).",
            "format": "int32",
            "nullable": true
          },
          "displayOrder": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A per-item override for a modifier. Null fields mean \"use the definition / group-default value\"."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerTenderTypeDto": {
        "type": "object",
        "properties": {
          "tenderId": {
            "type": "string",
            "description": "The POS-native tender id; send this as the `tenderId` on a `POST /orders/submit` payment.\r\nEmpty when the owning payment integration has no POS tender mapped — settle against the\r\n`paymentIntegrationId` in that case.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Human-readable label for the tender type (e.g. \"Cash\", \"Card\", \"Gift Card\").",
            "nullable": true
          },
          "paymentIntegrationId": {
            "type": "string",
            "description": "The GRUBBRR payment integration that owns this tender (e.g. `payment-integration-cash`). The order\r\nis settled against this integration when the tender is submitted.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single payment tender type — the `tenderId` / `paymentIntegrationId` pair a partner puts on a\r\n`POST /orders/submit` payment (and on order-based loyalty payments)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerTenderTypesDto": {
        "type": "object",
        "properties": {
          "tenderTypes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.Menu.PartnerTenderTypeDto"
            },
            "description": "All tender types configured for the location (no filtering). An empty list is a valid response when\r\nno tender types are configured.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The payment tender types configured for the token's location. Called at startup alongside\r\n`GET /menu` and `GET /menu/availability`. Partners read this to discover the\r\n`tenderId` / `paymentIntegrationId` pairs to put on each `POST /orders/submit` payment\r\n(and each order-based loyalty payment). The location is resolved from the access token — never from\r\nrequest input."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.Menu.UnavailableEntityDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "GRUBBRR master id, stable across POS systems. Matches the id used in `GET /menu`.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Display name of the entity.",
            "nullable": true
          },
          "type": {
            "type": "string",
            "description": "Entity kind: one of `Item`, `Combo` or `Modifier`.",
            "nullable": true
          },
          "externalPosId": {
            "type": "string",
            "description": "POS-specific external id, when known; null otherwise.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Availability status; entries in this list are always GRUBBRR.Web.UI.Areas.Partner.Models.Menu.AvailabilityStatus.SoldOut.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single sold-out / 86'd entity at the location."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRefreshRequest": {
        "required": [
          "refreshToken"
        ],
        "type": "object",
        "properties": {
          "refreshToken": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false,
        "description": "Request to exchange a refresh token for a fresh access + rotated refresh token pair. The refresh\r\ntoken itself is the credential, so this endpoint is anonymous."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterRequest": {
        "required": [
          "enrollmentCode"
        ],
        "type": "object",
        "properties": {
          "enrollmentCode": {
            "minLength": 1,
            "type": "string"
          },
          "clientVersion": {
            "maxLength": 200,
            "type": "string",
            "description": "Optional partner-reported integration/client version (e.g. `voice-ai 2.3.1`).",
            "nullable": true
          },
          "integrationName": {
            "maxLength": 200,
            "type": "string",
            "description": "Optional partner-reported integration name / system (e.g. `OpenAI Voice`).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Redeem a single-use enrollment code to activate the pre-created device and obtain its device id +\r\ninitial refresh token. Anonymous — the code is itself the credential. The optional self-reported\r\nfields are stored on the device record for support/diagnostics; they are unverified hints, never used\r\nfor authorization."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiRegisterResponse": {
        "type": "object",
        "properties": {
          "deviceId": {
            "type": "string",
            "nullable": true
          },
          "refreshToken": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Result of redeeming an enrollment code: the activated device's id plus its initial refresh token."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.OrderingApiTokenResponse": {
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string",
            "nullable": true
          },
          "refreshToken": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Issued token pair (access + refresh) for a device."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApiStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Always `\"ok\"` when the endpoint is reachable.",
            "nullable": true
          },
          "apiVersion": {
            "type": "string",
            "description": "The public API version segment (e.g. `\"v1\"`).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Lightweight status payload for the Partner Ordering API probe endpoints.\r\nCarries no business data."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest": {
        "required": [
          "externalOrderTypeId",
          "orderTypeId",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Partner cart or checkout session identifier. Use the same value sent to\r\n`POST /orders/validate` so the coupon is evaluated against the active cart."
          },
          "couponCode": {
            "type": "string",
            "description": "Optional coupon code, as entered by the customer, to resolve against the POS. The endpoint\r\nvalidates the cart's coupon and/or discounts in a single POS round-trip, so this may be omitted\r\nwhen only the supplied GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest.Discounts need to be validated. At least one of\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest.CouponCode or GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest.Discounts must be supplied.",
            "nullable": true
          },
          "orderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR order type identifier to evaluate the coupon under (e.g. the configured pickup or dine-in\r\norder type)."
          },
          "externalOrderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "Required partner-facing order type code or delivery-mode identifier."
          },
          "externalOrderTypeLabel": {
            "type": "string",
            "description": "Optional partner-facing display label for the order type.",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "description": "Optional partner order identifier. When omitted, the coupon is still resolved against the cart.",
            "nullable": true
          },
          "customer": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem"
            },
            "description": "Menu items in the cart the coupon is evaluated against. A request must include at least one item\r\nor one combo.",
            "nullable": true
          },
          "combos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo"
            },
            "description": "Combo selections in the cart the coupon is evaluated against. A request must include at least one\r\nitem or one combo.",
            "nullable": true
          },
          "discounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount"
            },
            "description": "Discounts to validate against the cart, using the shared GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount model.\r\nUsed both to validate the discounts themselves and as cart context so the POS evaluates a coupon\r\nagainst the already-discounted cart. Optional when a GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponRequest.CouponCode is supplied.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request body for applying a coupon code to a partner cart. Reuses the validate/submit cart shape\r\n(GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem / GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo / GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity)\r\nso the POS evaluates the coupon against the same cart the partner is building. The coupon is resolved\r\nby the POS through the existing apply-coupon flow; a resolved coupon is returned as a\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon the partner can then send on `POST /orders/validate` and\r\n`POST /orders/submit`."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The partner session identifier this result is for.",
            "nullable": true
          },
          "couponCode": {
            "type": "string",
            "description": "The coupon code that was evaluated.",
            "nullable": true
          },
          "accepted": {
            "type": "boolean",
            "description": "Whether the POS resolved and accepted the coupon for the cart."
          },
          "reason": {
            "type": "string",
            "description": "Human-readable reason the coupon was not applied. `null` when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse.Accepted is\r\n`true`.",
            "nullable": true
          },
          "coupon": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon"
          }
        },
        "additionalProperties": false,
        "description": "Result of applying a coupon code to a partner cart. GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse.Accepted reports whether the POS\r\nresolved the coupon; on success GRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse.Coupon carries the resolved coupon (the same\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon shape accepted by validate/submit), and on failure\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerApplyCouponResponse.Reason explains why the coupon was not applied."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerCustomerIdentityFieldDto": {
        "type": "object",
        "properties": {
          "field": {
            "type": "string",
            "description": "The identity field: one of `Name`, `Phone`, `Email`, or `StateId`.",
            "nullable": true
          },
          "required": {
            "type": "boolean",
            "description": "`true` when the location marks the field mandatory for this order type; `false` when it is\r\n            requested but optional."
          }
        },
        "additionalProperties": false,
        "description": "A single customer-identity field the location requests for an order type."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "GRUBBRR discount id; send this value to apply the discount on an order.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Internal discount name.",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "description": "Customer-facing display name; falls back to GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountDto.Name when not configured.",
            "nullable": true
          },
          "shortDescription": {
            "type": "string",
            "description": "Short marketing description; empty when not configured.",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "description": "Discount image URL; empty when not configured.",
            "nullable": true
          },
          "applicationMode": {
            "type": "string",
            "description": "How the discount is applied: `Code` (requires one of GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountDto.Codes) or\r\n`UserSelectable` (the partner chooses to apply it).",
            "nullable": true
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The codes that redeem this discount when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountDto.ApplicationMode is `Code`; empty otherwise.",
            "nullable": true
          },
          "type": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountTypeDto"
          },
          "active": {
            "type": "boolean",
            "description": "Whether the discount is currently enabled."
          },
          "startDate": {
            "type": "string",
            "description": "Inclusive UTC start of the discount's active window; `null` when always-on.",
            "format": "date-time",
            "nullable": true
          },
          "endDate": {
            "type": "string",
            "description": "Inclusive UTC end of the discount's active window; `null` when always-on.",
            "format": "date-time",
            "nullable": true
          },
          "maxApplicationsPerOrder": {
            "type": "integer",
            "description": "Maximum number of times the discount may be applied to a single order (0 = unlimited).",
            "format": "int32"
          },
          "maxDiscountedUnits": {
            "type": "integer",
            "description": "Maximum number of units the discount may apply to (0 = unlimited).",
            "format": "int32"
          },
          "maxDiscountAmount": {
            "type": "number",
            "description": "Maximum total amount the discount may take off a single application; `null` = unlimited.",
            "format": "double",
            "nullable": true
          },
          "minSpendDiscountBasis": {
            "type": "string",
            "description": "Whether the minimum-spend condition is evaluated `BeforeDiscount` or `AfterDiscount`.",
            "nullable": true
          },
          "applicableForFrequentCustomers": {
            "type": "boolean",
            "description": "Whether the discount is applicable for frequent (loyalty) customers."
          },
          "externalPosId": {
            "type": "string",
            "description": "The POS-facing discount id for reconciliation; `null` when not mapped to a POS connector.",
            "nullable": true
          },
          "scope": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountScopeDto"
          },
          "eligibility": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountEligibilityDto"
          }
        },
        "additionalProperties": false,
        "description": "A single discount the partner can apply to an order (by `id` or, when present, a code)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountEligibilityDto": {
        "type": "object",
        "properties": {
          "minimumSubtotal": {
            "type": "number",
            "description": "Minimum cart subtotal required; `null` when there is no minimum spend.",
            "format": "double",
            "nullable": true
          },
          "schedules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountScheduleDto"
            },
            "description": "Day/time windows when the discount is available; empty when not schedule-restricted.",
            "nullable": true
          },
          "orderTypeIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Order type ids the order must match; empty when all order types qualify.",
            "nullable": true
          },
          "stateCodes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "US state codes the location must be in; empty when not state-restricted.",
            "nullable": true
          },
          "qualifyingItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerQualifyingItemGroupDto"
            },
            "description": "Cart must include items from each group. Within a group, any listed item, category, or combo satisfies\r\nthe quantity rule.",
            "nullable": true
          },
          "qualifyingModifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerQualifyingModifierDto"
            },
            "description": "Items that must include specific modifiers or modifier groups.",
            "nullable": true
          },
          "requiredDiscounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerRequiredDiscountDto"
            },
            "description": "Other discounts that must already be on the cart.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Partner-friendly eligibility summary. Integrators read flat lists instead of walking a nested condition tree.\r\nAll populated top-level fields must hold; each qualifying-items group is satisfied when any of its items,\r\ncategories, or combos are present in the cart at the required quantity."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountOffer": {
        "required": [
          "itemId"
        ],
        "type": "object",
        "properties": {
          "itemId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR item identifier (proto `itemId`)."
          }
        },
        "additionalProperties": false,
        "description": "An item granted or affected by a discount (mirrors the POS `DiscountOffer` message)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountQualifying": {
        "required": [
          "itemId"
        ],
        "type": "object",
        "properties": {
          "itemId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR item identifier (proto `itemId`)."
          }
        },
        "additionalProperties": false,
        "description": "An item that qualifies an order for a discount (mirrors the POS `DiscountQualifying` message)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountScheduleDto": {
        "type": "object",
        "properties": {
          "dayOfWeek": {
            "type": "string",
            "description": "Day name: `Sunday`, `Monday`, … `Saturday`.",
            "nullable": true
          },
          "startHour": {
            "type": "integer",
            "description": "Window start hour (24h).",
            "format": "int32",
            "nullable": true
          },
          "startMinutes": {
            "type": "integer",
            "description": "Window start minute.",
            "format": "int32",
            "nullable": true
          },
          "endHour": {
            "type": "integer",
            "description": "Window end hour (24h).",
            "format": "int32",
            "nullable": true
          },
          "endMinutes": {
            "type": "integer",
            "description": "Window end minute.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A day/time window when a discount is available."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountScopeDto": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "One of: `Order`, `SingleItem`, `MultipleItem`, `OrderItem`.",
            "nullable": true
          },
          "menuItemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Menu item ids in scope (item/multiple/order-item scopes).",
            "nullable": true
          },
          "comboIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Combo ids in scope.",
            "nullable": true
          },
          "categoryIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Category ids in scope.",
            "nullable": true
          },
          "maxDiscountedItems": {
            "type": "integer",
            "description": "Maximum number of items discounted, for `MultipleItem` scope; `null` otherwise.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "What a discount applies to, discriminated by GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountScopeDto.Kind."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountTypeDto": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "One of: `FreeItem`, `FixedAmount`, `Percentage`, `PriceOverride`, `ApplyItem`, `Unknown`.",
            "nullable": true
          },
          "amount": {
            "type": "number",
            "description": "Flat amount off, for `FixedAmount`.",
            "format": "double",
            "nullable": true
          },
          "percent": {
            "type": "number",
            "description": "Percentage off, for `Percentage`.",
            "format": "double",
            "nullable": true
          },
          "priceOverride": {
            "type": "number",
            "description": "Overridden price, for `PriceOverride`.",
            "format": "double",
            "nullable": true
          },
          "itemId": {
            "type": "string",
            "description": "Target item id, for `FreeItem` / `ApplyItem`.",
            "nullable": true
          },
          "shouldSubtractPrice": {
            "type": "boolean",
            "description": "Whether the applied item's price is subtracted, for `ApplyItem`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The value a discount confers, discriminated by GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountTypeDto.Kind."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountsDto": {
        "type": "object",
        "properties": {
          "discounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountDto"
            },
            "description": "The partner-applicable discounts at the location. An empty list means the location has no\r\ncode-based or user-selectable discounts configured.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The discounts a partner can apply at the token's location. Only partner-applicable discounts are\r\nreturned — code-based and user-selectable. Auto-applied discounts are intentionally omitted because the\r\norder pipeline applies those itself; a partner never sends them. The location is resolved from the access\r\ntoken — never from request input."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplicableRewardsRequest": {
        "required": [
          "loyaltyUser",
          "order",
          "orderDetails",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          },
          "order": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder"
          },
          "orderDetails": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          }
        },
        "additionalProperties": false,
        "description": "Request for the rewards applicable to a specific cart. Stateless: the client sends the\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplicableRewardsRequest.LoyaltyUser it received from `/loyalty/login` plus the GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplicableRewardsRequest.Order (menu order)\r\nand its GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplicableRewardsRequest.OrderDetails totals to scope against — mirroring the kiosk loyalty\r\n`GetRewardsForOrder` request. Only valid for providers whose reward flow is cart-driven\r\n(GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardFlow.RewardsBasedOnCart)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardRequest": {
        "required": [
          "loyaltyUser",
          "order",
          "orderDetails",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          },
          "order": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder"
          },
          "orderDetails": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          }
        },
        "additionalProperties": false,
        "description": "Request to validate (apply) the cart's reward(s) and return the dollar amount they would deduct from the\r\nsubtotal. This <b>does not burn</b> the reward — redemption happens at `/loyalty/redeem`\r\nafter submit. Stateless: the client sends back the GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardRequest.LoyaltyUser plus the current\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardRequest.Order (menu order) and its GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardRequest.OrderDetails. The reward(s) to validate are carried\r\non the order's `rewards`."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyApplyRewardResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The correlation key this result is for.",
            "nullable": true
          },
          "rewardId": {
            "type": "string",
            "description": "The reward id that was validated.",
            "nullable": true
          },
          "rewardName": {
            "type": "string",
            "description": "The reward name (echoed from the supplied reward).",
            "nullable": true
          },
          "appliedAmount": {
            "type": "number",
            "description": "The dollar amount the reward deducts from the subtotal.",
            "format": "double"
          },
          "accepted": {
            "type": "boolean",
            "description": "Always `true` on a success response; a rejected reward returns `reward_not_applicable` (422)."
          }
        },
        "additionalProperties": false,
        "description": "Result of validating (applying) a reward against the cart. The reward is not yet redeemed/burned."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitRequest": {
        "required": [
          "order",
          "orderDetails",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "order": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder"
          },
          "orderDetails": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          }
        },
        "additionalProperties": false,
        "description": "Request to submit an order to the loyalty provider for a <b>guest</b> — no signed-in loyalty user.\r\nThe guest counterpart of `/loyalty/redeem`: the client sends the correlation\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitRequest.SessionId plus the submitted GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitRequest.Order (menu order) and its\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitRequest.OrderDetails, but <b>no</b>`loyaltyUser`. Returns the provider\r\n`loyaltyTransactionId` the client retains to void later. Stateless — GRUBBRR persists nothing."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyGuestSubmitResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The correlation key this submission is for.",
            "nullable": true
          },
          "loyaltyTransactionId": {
            "type": "string",
            "description": "The provider transaction id for the submission. <b>Retain this</b> to void later.",
            "nullable": true
          },
          "submittedAt": {
            "type": "string",
            "description": "Server timestamp (UTC) the order was submitted to the loyalty provider.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Result of a guest order submission: the provider transaction id to retain for a later void."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginRequest": {
        "required": [
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key for the ordering interaction. Required."
          },
          "phoneNumber": {
            "type": "string",
            "description": "Look up the customer by phone number. Provide exactly one of phone / email / login code.",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Look up the customer by email. Provide exactly one of phone / email / login code.",
            "nullable": true
          },
          "loginCode": {
            "type": "string",
            "description": "Look up the customer by a manually entered loyalty code/id. Provide exactly one of phone / email /\r\nlogin code.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to sign a loyalty customer in. Exactly one of GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginRequest.PhoneNumber,\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginRequest.Email, or GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginRequest.LoginCode identifies the customer; the look-up method must be\r\none the location's loyalty provider supports."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The correlation key this result is for.",
            "nullable": true
          },
          "loyaltyAttached": {
            "type": "boolean",
            "description": "Whether a loyalty account was resolved and is now associated with the session."
          },
          "firstName": {
            "type": "string",
            "description": "Customer first name, when the provider supplies it.",
            "nullable": true
          },
          "pointBalance": {
            "type": "number",
            "description": "Current primary point balance, when available.",
            "format": "double",
            "nullable": true
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          }
        },
        "additionalProperties": false,
        "description": "Result of a loyalty sign-in. GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginResponse.LoyaltyAttached reports whether a loyalty account was\r\nresolved; on success GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyLoginResponse.LoyaltyUser carries the context the client must echo on subsequent\r\nloyalty calls, plus the available profile fields. A look-up that matched no account returns\r\n`200` with `loyaltyAttached: false` (a normal outcome, not an error)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder": {
        "required": [
          "externalOrderTypeId",
          "orderTypeId"
        ],
        "type": "object",
        "properties": {
          "orderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR order type id (e.g. dine-in / takeout). Required."
          },
          "externalOrderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "The integrator's order type id, echoed onto the loyalty order. Required."
          },
          "externalOrderTypeLabel": {
            "type": "string",
            "description": "The integrator's order type label, echoed onto the loyalty order. Optional.",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "description": "The GRUBBRR order id (from validate/submit) used to correlate the loyalty order. Optional.",
            "nullable": true
          },
          "customer": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem"
            },
            "description": "The order line items. Optional when the order is combo-only (see GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder.Combos).",
            "nullable": true
          },
          "combos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo"
            },
            "description": "The combo line items. Optional when the order carries plain GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder.Items.",
            "nullable": true
          },
          "rewards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward"
            },
            "description": "Loyalty rewards already applied to this cart, carried so the provider evaluates the order in its\r\ncurrent state (e.g. applicable rewards is scoped against the rewards already on the cart, and\r\n`/loyalty/apply` and `/loyalty/redeem` act on these rewards). Reuses the\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward the loyalty endpoints return; the client echoes them back unchanged.",
            "nullable": true
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment"
            },
            "description": "How the order was (or will be) paid. Optional. Uses the <b>same</b>GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment\r\nshape as `POST /orders/submit`, so a client can send the identical `payments` array to both\r\nthe order submit and the loyalty calls. Forwarded to the loyalty provider on every order-based loyalty\r\nendpoint (applicable rewards, apply, redeem, guest submit, void) so providers that award points by\r\namount / tender can compute them. The provider's tender method is resolved server-side from each\r\npayment's `paymentIntegrationId` (cash / card / stored-value), so no separate payment-type field is\r\nneeded. Send this only when the provider's point rules depend on how the order was paid.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The order (cart) the loyalty provider evaluates rewards against — the partner-facing equivalent of the\r\nkiosk loyalty `MenuOrder`. It reuses the same item/combo/identity models as order validate/submit so\r\na validated cart can be sent unchanged. Built into the internal kiosk `MenuOrder` via\r\n`PartnerOrderProtoMapper.BuildMenuOrder` and then run through the same loyalty conversion pipeline\r\nthe kiosk uses (POS-id resolution, totals, items)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemRequest": {
        "required": [
          "loyaltyUser",
          "order",
          "orderDetails",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          },
          "order": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder"
          },
          "orderDetails": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          }
        },
        "additionalProperties": false,
        "description": "Request to <b>redeem (burn)</b> the cart's reward(s) after the order has been submitted. Returns\r\nthe provider `loyaltyTransactionId` used to void the redemption. Stateless: the client sends back the\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemRequest.LoyaltyUser plus the submitted GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemRequest.Order (menu order) and its\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemRequest.OrderDetails — the reward(s) to burn ride on the order's `rewards`. GRUBBRR does not\r\npersist the transaction id — the client carries it to `/loyalty/void`."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRedeemResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The correlation key this redemption is for.",
            "nullable": true
          },
          "rewardId": {
            "type": "string",
            "description": "The reward id that was redeemed.",
            "nullable": true
          },
          "loyaltyTransactionId": {
            "type": "string",
            "description": "The provider transaction id for the redemption. <b>Retain this</b> to void later.",
            "nullable": true
          },
          "redeemedAt": {
            "type": "string",
            "description": "Server timestamp (UTC) the reward was redeemed.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Result of burning a reward: the provider transaction id to retain for a later void."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward": {
        "type": "object",
        "properties": {
          "rewardId": {
            "type": "string",
            "description": "Stable provider reward id (the loyalty reward id).",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Reward description (full description when available, otherwise the short description).",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "description": "Reward image URL, when the provider supplies one.",
            "nullable": true
          },
          "value": {
            "type": "string",
            "description": "Provider-supplied display value (e.g. `\"$5 off\"`), when available.",
            "nullable": true
          },
          "externalId": {
            "type": "string",
            "description": "Provider-native external id for the reward.",
            "nullable": true
          },
          "posDiscountId": {
            "type": "string",
            "description": "POS discount id the reward maps to, when the provider supplies one.",
            "nullable": true
          },
          "integrationMetadata": {
            "type": "string",
            "description": "Opaque provider metadata required to apply/redeem this reward later. Treated as a pass-through\r\ntoken: the client echoes it back unchanged.",
            "nullable": true
          },
          "redeemedAmount": {
            "type": "integer",
            "description": "The discount this reward deducts, in cents. On responses, cart-driven applicable rewards (and any\r\nlisting where the provider computes a per-reward amount) populate this from the reward's redeemable\r\npoint/balance deduction — the same value the kiosk surfaces as `redeemablePoint.discountApplied`\r\n— so the client can carry the reward straight onto an order without a separate\r\n`POST /loyalty/apply` round-trip. On requests, set it when placing the reward in an order's\r\n`rewards` collection (`/orders/validate`, `/orders/submit`, and the loyalty\r\n`order.rewards`) so the POS applies the discount. `null` when the provider reward carries no\r\nparseable point/balance amount.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single loyalty reward available to the signed-in customer. Display fields (GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.Name,\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.Description, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.ImageUrl, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.Value) plus the pass-through context\r\n(GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.RewardId, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.ExternalId, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.PosDiscountId,\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.IntegrationMetadata) the client echoes back when applying/redeeming the reward."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsRequest": {
        "required": [
          "loyaltyUser",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          }
        },
        "additionalProperties": false,
        "description": "Request for the rewards available to the signed-in loyalty customer. Stateless: the\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsRequest.LoyaltyUser the client received from `/loyalty/login` is sent back here so the\r\nprovider can resolve the account — GRUBBRR holds no session state of its own."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyRewardsResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The correlation key this result is for.",
            "nullable": true
          },
          "rewards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward"
            },
            "description": "The rewards currently available to the customer (possibly empty).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Rewards available to the signed-in loyalty customer."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Provider customer id (frequent-customer id), when supplied.",
            "nullable": true
          },
          "externalId": {
            "type": "string",
            "description": "Provider-native external id for the loyalty account.",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "primaryPointBalance": {
            "type": "number",
            "description": "Current primary point balance reported by the provider, when available.",
            "format": "double",
            "nullable": true
          },
          "integrationMetadata": {
            "type": "string",
            "description": "Opaque provider metadata required to rehydrate later provider calls (rewards / apply / redeem /\r\nvoid). Treated as a pass-through token: the client must echo it back unchanged.",
            "nullable": true
          },
          "posOrderId": {
            "type": "string",
            "description": "POS-native order id correlated with the loyalty account, when the provider supplies one.",
            "nullable": true
          },
          "posOrderMetadataJson": {
            "type": "string",
            "description": "Opaque POS order metadata supplied by the provider, echoed back on later calls.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The resolved loyalty-user context returned by `/loyalty/login`. The loyalty flow is stateless,\r\nso the <b>client</b> holds this object (notably GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser.IntegrationMetadata, which the provider\r\nneeds to rehydrate later calls) and sends it back on subsequent loyalty endpoints — GRUBBRR stores\r\nnothing of its own."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest": {
        "required": [
          "loyaltyTransactionId",
          "loyaltyUser",
          "order",
          "orderDetails",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Client-generated correlation key. Required."
          },
          "loyaltyUser": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyUser"
          },
          "loyaltyTransactionId": {
            "minLength": 1,
            "type": "string",
            "description": "The provider transaction id returned by `/loyalty/redeem`. Required."
          },
          "order": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyOrder"
          },
          "orderDetails": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          }
        },
        "additionalProperties": false,
        "description": "Request to <b>void</b> a previously completed redemption. Identified by the provider\r\n`loyaltyTransactionId` from `/loyalty/redeem`, sent back with the same stateless context as\r\nredeem: the correlation GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest.SessionId, the GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest.LoyaltyUser, and the redeemed\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest.Order + GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidRequest.OrderDetails, so the provider can reconcile the void against the\r\noriginal order. The call is authenticated and token/location-scoped like every partner endpoint."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyVoidResponse": {
        "type": "object",
        "properties": {
          "loyaltyTransactionId": {
            "type": "string",
            "description": "The transaction id that was voided.",
            "nullable": true
          },
          "voided": {
            "type": "boolean",
            "description": "Always `true` on a success response; a failed void returns `void_failed` (422)."
          },
          "voidedAt": {
            "type": "string",
            "description": "Server timestamp (UTC) the redemption was voided.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Result of voiding a redemption."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerModifierCode": {
        "required": [
          "modifierCodeId",
          "modifierCodePosId"
        ],
        "type": "object",
        "properties": {
          "modifierCodeId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR modifier code identifier."
          },
          "name": {
            "type": "string",
            "description": "Optional modifier code display name.",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this modifier code.",
            "nullable": true
          },
          "modifierCodePosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS modifier code identifier. The partner must send the resolved POS id so validation never\r\nfalls back to a menu-service lookup."
          }
        },
        "additionalProperties": false,
        "description": "Modifier code selection used by POS systems that support modifier-code behavior."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo": {
        "required": [
          "categoryId",
          "categoryPosId",
          "comboId",
          "comboPosId"
        ],
        "type": "object",
        "properties": {
          "comboId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR combo identifier."
          },
          "quantity": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Quantity of this combo. Must be greater than zero.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Optional combo display name.",
            "nullable": true
          },
          "categoryId": {
            "minLength": 1,
            "type": "string",
            "description": "Required GRUBBRR category identifier."
          },
          "categoryName": {
            "type": "string",
            "description": "Optional category display name.",
            "nullable": true
          },
          "categoryPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS category identifier. The partner must send the resolved POS id so validation never falls\r\nback to a menu-service lookup."
          },
          "comboPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS combo identifier. The partner must send the resolved POS id so validation never falls\r\nback to a menu-service lookup."
          },
          "itemSessionId": {
            "type": "string",
            "description": "Optional partner-generated identifier for this specific combo instance in the cart.",
            "nullable": true
          },
          "unitPrice": {
            "type": "integer",
            "description": "Optional unit price in cents.",
            "format": "int32",
            "nullable": true
          },
          "totalPrice": {
            "type": "integer",
            "description": "Optional total price in cents for this combo selection.",
            "format": "int32",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this combo.",
            "nullable": true
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderComboComponent"
            },
            "description": "Components selected for this combo.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Combo selection to validate."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderComboComponent": {
        "required": [
          "comboComponentId",
          "comboComponentPosId"
        ],
        "type": "object",
        "properties": {
          "comboComponentId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR combo component identifier."
          },
          "name": {
            "type": "string",
            "description": "Optional combo component display name.",
            "nullable": true
          },
          "comboComponentPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS combo component identifier. The partner must send the resolved POS id so validation never\r\nfalls back to a menu-service lookup."
          },
          "itemSelections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem"
            },
            "description": "Item selections for this component. A component must carry at least one entry; a component that\r\nselects a single item sends a one-element list (see M:GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderComboComponent.Validate(System.ComponentModel.DataAnnotations.ValidationContext)).",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this combo component.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Component selection inside a combo."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon": {
        "required": [
          "couponCode",
          "id"
        ],
        "type": "object",
        "properties": {
          "id": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR coupon/discount identifier (proto `id`)."
          },
          "name": {
            "type": "string",
            "description": "Optional coupon display name (proto `name`).",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this coupon (proto `posData`).",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "description": "Optional coupon image URL (proto `imageUrl`).",
            "nullable": true
          },
          "discountAmount": {
            "type": "integer",
            "description": "Applied coupon discount amount in cents (proto `discountAmount`).",
            "format": "int64"
          },
          "couponCode": {
            "minLength": 1,
            "type": "string",
            "description": "The coupon code as entered by the customer (proto `couponCode`)."
          },
          "isItemLevel": {
            "type": "boolean",
            "description": "Indicates whether the coupon applies at the item level rather than the order level\r\n(proto `isItemLevel`)."
          },
          "discountItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCouponItem"
            },
            "description": "Items the coupon applies to when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon.IsItemLevel is set (proto `discountItems`).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Coupon discount to apply during order validation and submission. This is the single shared coupon\r\nmodel used by both the validate and submit requests, and it mirrors the POS `CouponDiscount`\r\ncontract one-to-one so a coupon that validates can be submitted unchanged. It is carried on the\r\norder's POS coupon list — the same channel the kiosk uses for `ValidateOrderWithStatus` and\r\nclose-order."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCouponItem": {
        "required": [
          "itemId"
        ],
        "type": "object",
        "properties": {
          "itemId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR item identifier (proto `itemId`)."
          }
        },
        "additionalProperties": false,
        "description": "An item a coupon applies to (mirrors the POS `CouponDiscountItem` message)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount": {
        "required": [
          "discountId"
        ],
        "type": "object",
        "properties": {
          "discountId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR discount identifier (proto `discountId`)."
          },
          "flat": {
            "type": "integer",
            "description": "Flat discount amount in cents (proto `value.flat`). Set at most one of\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Flat, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Points, or GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Percent.",
            "format": "int32",
            "nullable": true
          },
          "points": {
            "type": "integer",
            "description": "Loyalty points value backing the discount (proto `value.points`). Set at most one of\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Flat, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Points, or GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Percent.",
            "format": "int32",
            "nullable": true
          },
          "percent": {
            "type": "integer",
            "description": "Percentage discount value (proto `value.percent`), passed through to the POS as supplied.\r\nSet at most one of GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Flat, GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Points, or GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount.Percent.",
            "format": "int32",
            "nullable": true
          },
          "discountAmount": {
            "type": "number",
            "description": "Partner-calculated applied discount amount (proto `discountAmount`).",
            "format": "double"
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this discount (proto `posData`).",
            "nullable": true
          },
          "discountName": {
            "type": "string",
            "description": "Optional discount display name (proto `discountName`).",
            "nullable": true
          },
          "discountQualifyings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountQualifying"
            },
            "description": "Items that qualify the order for this discount (proto `discountQualifyings`).",
            "nullable": true
          },
          "discountOffers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerDiscountOffer"
            },
            "description": "Items granted or affected by this discount (proto `discountOffers`).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Discount to apply during order validation and submission. This is the single shared discount model\r\nused by both the validate and submit requests, and it mirrors the POS `OrderDiscount` contract\r\none-to-one so a discount that validates can be submitted unchanged."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderFee": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Fee display label.",
            "nullable": true
          },
          "amount": {
            "type": "integer",
            "description": "Fee amount in cents.",
            "format": "int32"
          },
          "feeId": {
            "type": "string",
            "description": "POS or GRUBBRR fee identifier.",
            "nullable": true
          },
          "type": {
            "type": "string",
            "description": "POS fee type or category.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Fee returned by POS validation."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Customer display name.",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Customer email address.",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "description": "Customer phone number.",
            "nullable": true
          },
          "orderToken": {
            "type": "string",
            "description": "Optional partner or POS order token used by integrations that require a customer-facing token.",
            "nullable": true
          },
          "tableTent": {
            "type": "string",
            "description": "Optional table tent or pickup identifier.",
            "nullable": true
          },
          "tableNumber": {
            "type": "string",
            "description": "Optional table number. Forwarded to the POS when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity.TableTent is not supplied.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Customer identity details associated with the order being validated."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentityRequirementsDto": {
        "type": "object",
        "properties": {
          "orderIdentityMode": {
            "type": "string",
            "description": "How the order itself is identified: `None`, `TableTent`, or `Token`. When\r\n`TableTent`, send the table identifier on the order `customer.tableTent` (or\r\n`customer.tableNumber`). When `Token`, send the order token on `customer.orderToken`.",
            "nullable": true
          },
          "requiresTableTent": {
            "type": "boolean",
            "description": "Convenience flag: `true` when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentityRequirementsDto.OrderIdentityMode is `TableTent`."
          },
          "requiresOrderToken": {
            "type": "boolean",
            "description": "Convenience flag: `true` when GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentityRequirementsDto.OrderIdentityMode is `Token`."
          },
          "orderIdPrefix": {
            "type": "string",
            "description": "Order-id prefix the location applies to this order type, when configured; `null` otherwise.",
            "nullable": true
          },
          "customerFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerCustomerIdentityFieldDto"
            },
            "description": "The customer-identity fields the location asks for on this order type, each flagged required or\r\noptional. Empty when the order type collects no customer identity. Limited to the fields the\r\nlocation settings can configure: `Name`, `Phone`, `Email`, and `StateId`.\r\nOf these, `Name`, `Phone`, and `Email` map to the `customer` object\r\n(`PartnerOrderIdentity`); `StateId` is surfaced for awareness but has no dedicated slot yet.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The order- and customer-identity requirements configured for an order type. A partner uses this to\r\ndecide what to collect from the guest and populate on the order `customer` object\r\n(`PartnerOrderIdentity`). All requirements are advisory discovery — GRUBBRR does not reject an\r\norder for an unfilled field here; the POS/location owns any hard enforcement."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem": {
        "required": [
          "categoryId",
          "categoryPosId",
          "menuItemId",
          "menuItemPosId"
        ],
        "type": "object",
        "properties": {
          "menuItemId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR menu item identifier."
          },
          "quantity": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Quantity of this item. Must be greater than zero.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Optional menu item display name.",
            "nullable": true
          },
          "categoryId": {
            "minLength": 1,
            "type": "string",
            "description": "Required GRUBBRR category identifier."
          },
          "categoryName": {
            "type": "string",
            "description": "Optional category display name.",
            "nullable": true
          },
          "categoryPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS category identifier. The partner must send the resolved POS id so validation never falls\r\nback to a menu-service lookup."
          },
          "menuItemPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS menu item identifier. The partner must send the resolved POS id so validation never falls\r\nback to a menu-service lookup."
          },
          "specialRequest": {
            "type": "string",
            "description": "Optional kitchen or customer special request text for this item.",
            "nullable": true
          },
          "itemSessionId": {
            "type": "string",
            "description": "Optional partner-generated identifier for this specific item instance in the cart.",
            "nullable": true
          },
          "itemPrice": {
            "type": "integer",
            "description": "Optional base item price in cents.",
            "format": "int32",
            "nullable": true
          },
          "unitPrice": {
            "type": "integer",
            "description": "Optional unit price in cents, including any partner-calculated price adjustments that should be sent to POS validation.",
            "format": "int32",
            "nullable": true
          },
          "totalPrice": {
            "type": "integer",
            "description": "Optional total price in cents for this line item.",
            "format": "int32",
            "nullable": true
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this item.",
            "nullable": true
          },
          "discounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount"
            },
            "description": "Optional discounts applied to this specific line item, using the shared\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount model that mirrors the POS `OrderDiscount` contract.\r\nOrder-level discounts are sent via GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateRequest.Discounts instead.",
            "nullable": true
          },
          "modifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderModifier"
            },
            "description": "Modifiers selected for this item.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Menu item selection to validate."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderModifier": {
        "required": [
          "modifierGroupId",
          "modifierGroupPosId",
          "modifierId",
          "modifierPosId"
        ],
        "type": "object",
        "properties": {
          "modifierId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR modifier identifier."
          },
          "modifierGroupId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR modifier group identifier."
          },
          "quantity": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Quantity of this modifier. Must be greater than zero.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Optional modifier display name.",
            "nullable": true
          },
          "modifierPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS modifier identifier. The partner must send the resolved POS id so validation never falls\r\nback to a menu-service lookup."
          },
          "modifierGroupPosId": {
            "minLength": 1,
            "type": "string",
            "description": "Required POS modifier group identifier. The partner must send the resolved POS id so validation never\r\nfalls back to a menu-service lookup."
          },
          "freeQuantity": {
            "type": "integer",
            "description": "Number of units covered by a free quantity rule.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "integer",
            "description": "Optional unit price in cents.",
            "format": "int32",
            "nullable": true
          },
          "totalPrice": {
            "type": "integer",
            "description": "Optional total price in cents for this modifier selection.",
            "format": "int32",
            "nullable": true
          },
          "isDefault": {
            "type": "boolean",
            "description": "Indicates whether this modifier is selected by default on the menu item."
          },
          "posData": {
            "type": "string",
            "description": "Optional opaque POS metadata for this modifier.",
            "nullable": true
          },
          "modifierCode": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerModifierCode"
          },
          "modifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderModifier"
            },
            "description": "Nested modifiers selected under this modifier.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Modifier selection attached to an item or nested modifier."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment": {
        "required": [
          "paymentIntegrationId",
          "paymentIntegrationLabel",
          "tenderId",
          "totalPaid"
        ],
        "type": "object",
        "properties": {
          "paymentIntegrationId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR payment integration that owns the tender — the `paymentIntegrationId` from\r\n`GET /menu/tender-types` (e.g. `payment-integration-cash`). Required."
          },
          "tenderId": {
            "minLength": 1,
            "type": "string",
            "description": "POS-native tender id — the `tenderId` from `GET /menu/tender-types`. Required."
          },
          "paymentIntegrationLabel": {
            "minLength": 1,
            "type": "string",
            "description": "Human-readable payment label forwarded to the POS — the `name` from `GET /menu/tender-types`\r\n(e.g. \"Cash\", \"Card\"). Required."
          },
          "totalPaid": {
            "maximum": 2147483647,
            "minimum": 0,
            "type": "integer",
            "description": "Amount paid in cents, inclusive of GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment.TipAmount. Required; declared nullable so a missing\r\nvalue is rejected by validation rather than silently defaulting to 0.",
            "format": "int32"
          },
          "tipAmount": {
            "maximum": 2147483647,
            "minimum": 0,
            "type": "integer",
            "description": "Tip portion of GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment.TotalPaid in cents. Optional — defaults to 0 (no tip).",
            "format": "int32"
          },
          "transactionId": {
            "type": "string",
            "description": "Optional partner/POS transaction reference for this payment.",
            "nullable": true
          },
          "paymentCardName": {
            "type": "string",
            "description": "Optional cardholder name to display on the receipt; omit when not card-based.",
            "nullable": true
          },
          "paymentCardNumber": {
            "type": "string",
            "description": "Optional masked card number to display on the receipt; omit when not card-based. Send only a masked\r\nvalue — full PANs must never be transmitted.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single settled payment on the submitted order, forwarded to the POS on the close-order. The client\r\ncomposes it from a `GET /menu/tender-types` entry (its `tenderId` / `paymentIntegrationId`)\r\nplus the partner-calculated amounts (in cents). These are settled-payment references — no card/processor\r\nsecrets are accepted or stored."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderStatusResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The partner session identifier this status is for.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Current lifecycle state: `PENDING`, `SUBMITTED`, or `FAILED`.",
            "nullable": true
          },
          "posOrderId": {
            "type": "string",
            "description": "GRUBBRR-minted canonical order id (the partner-facing `posOrderId`). Present only on\r\n`SUBMITTED`; omitted otherwise.",
            "nullable": true
          },
          "thirdPartyOrderId": {
            "type": "string",
            "description": "The POS-native order id returned by the point of sale (without the GRUBBRR `ord-` prefix),\r\nfor reconciliation against the POS system. Present only on `SUBMITTED` when the POS supplied\r\nan id; omitted otherwise.",
            "nullable": true
          },
          "errorCode": {
            "type": "string",
            "description": "Canonical machine-readable failure code (see the Partner Ordering error taxonomy). Present only\r\non `FAILED`; omitted otherwise.",
            "nullable": true
          },
          "failureReason": {
            "type": "string",
            "description": "Client-safe failure detail describing why submission failed. Present only on `FAILED`;\r\nomitted otherwise.",
            "nullable": true
          },
          "resolvedAt": {
            "type": "string",
            "description": "UTC time the order reached its terminal state. Present on `SUBMITTED` / `FAILED`;\r\nomitted while `PENDING`.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Status projection returned by `GET /orders/status/{sessionId}`. It is the terminal-outcome\r\ncounterpart to the GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitResponse acknowledgement: partners poll this until\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderStatusResponse.Status reaches a terminal value."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails": {
        "type": "object",
        "properties": {
          "subtotal": {
            "type": "integer",
            "description": "Subtotal in cents before tax, fees, tip, rewards, and discounts.",
            "format": "int32",
            "nullable": true
          },
          "tax": {
            "type": "integer",
            "description": "Tax amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "description": "Final total in cents.",
            "format": "int32",
            "nullable": true
          },
          "tip": {
            "type": "integer",
            "description": "Tip amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "discount": {
            "type": "integer",
            "description": "Discount amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "rewards": {
            "type": "integer",
            "description": "Reward amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "serviceCharge": {
            "type": "integer",
            "description": "Service charge amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "charityAmount": {
            "type": "integer",
            "description": "Charity or donation amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "taxes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTaxLineItem"
            },
            "description": "Per-tax breakdown applied to the order.",
            "nullable": true
          },
          "fees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderFee"
            },
            "description": "Per-fee breakdown applied to the order.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Partner-calculated order totals forwarded to the POS on the submitted order's close-order details.\r\nMirrors the internal close-order `OrderDetails` shape (all monetary values in cents), minus the\r\nGRUBBRR-minted order id which the partner must not supply. Every field is optional; an omitted field\r\nleaves the corresponding POS value at its default rather than overwriting it."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitRequest": {
        "required": [
          "details",
          "externalOrderTypeId",
          "orderTypeId",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Partner checkout session identifier. Required: it is the idempotency key — re-submitting the\r\nsame GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitRequest.SessionId returns the existing order's status instead of creating a second\r\norder. Use the same value sent to `POST /orders/validate`."
          },
          "orderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR order type identifier to submit under (e.g. the configured pickup or dine-in order type)."
          },
          "externalOrderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "Required partner-facing order type code or delivery-mode identifier."
          },
          "externalOrderTypeLabel": {
            "type": "string",
            "description": "Optional partner-facing display label for the order type.",
            "nullable": true
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderPayment"
            },
            "description": "Optional payments the order is settled with. Each entry is built by the client from the\r\ntender types returned by `GET /menu/tender-types` (its `tenderId` / `paymentIntegrationId`)\r\nplus the partner-calculated amounts, and is forwarded to the POS as-is. When omitted, the order falls\r\nback to pay-at-counter. These are settled-payment references, not captured card/processor data.",
            "nullable": true
          },
          "thirdPartyOrderId": {
            "type": "string",
            "description": "Optional partner-owned order identifier, retained on the order record for reconciliation.",
            "nullable": true
          },
          "details": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitDetails"
          },
          "customer": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem"
            },
            "description": "Menu items to submit. A request must include at least one item or one combo.",
            "nullable": true
          },
          "combos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo"
            },
            "description": "Combo selections to submit. A request must include at least one item or one combo.",
            "nullable": true
          },
          "discounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount"
            },
            "description": "Optional order-level discounts to apply, using the same GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount model\r\naccepted by `POST /orders/validate`. Item-level discounts are set on the item via\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem.Discounts. Send the same discounts that were validated so submitted\r\ntotals match the validated totals.",
            "nullable": true
          },
          "coupons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon"
            },
            "description": "Optional coupon discounts to apply, using the same GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon model accepted\r\nby `POST /orders/validate`. Send the same coupons that were validated so submitted totals\r\nmatch the validated totals.",
            "nullable": true
          },
          "rewards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward"
            },
            "description": "Optional loyalty rewards already validated against this cart (via `POST /loyalty/apply`) to\r\ninclude in pricing, using the same GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward model accepted by\r\n`POST /orders/validate`. Set each reward's GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.RedeemedAmount\r\n(cents) so the POS deducts the validated discount. Send the same rewards that were validated so\r\nsubmitted totals match the validated totals.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request body for asynchronously submitting a partner order to the POS. Reuses the validate payload\r\nshape (GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem / GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo /\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity) so a cart that validated can be submitted unchanged."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitResponse": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The partner session identifier this acknowledgement is for.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Current lifecycle state: `PENDING`, `SUBMITTED`, or `FAILED`.",
            "nullable": true
          },
          "timestamp": {
            "type": "string",
            "description": "UTC time the acknowledgement was produced.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Acknowledgement returned by `POST /orders/submit`. The submit is asynchronous: the response\r\nconfirms acceptance and the current lifecycle state. Poll `GET /orders/status/{sessionId}`\r\nfor terminal details (posOrderId / failure reason). A duplicate submit for the same\r\nGRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitResponse.SessionId returns the existing order's current GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderSubmitResponse.Status with no re-submit."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTaxLineItem": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Tax display label.",
            "nullable": true
          },
          "amount": {
            "type": "integer",
            "description": "Tax amount in cents.",
            "format": "int32"
          },
          "taxId": {
            "type": "string",
            "description": "POS or GRUBBRR tax identifier.",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "description": "Total taxable amount in cents this tax line was calculated on.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "A single tax line forwarded on the submitted order's close-order details. Mirrors the internal\r\nclose-order `OrderTaxLineItem` (monetary values in cents)."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTotals": {
        "type": "object",
        "properties": {
          "subtotal": {
            "type": "integer",
            "description": "Subtotal in cents before tax, fees, tip, rewards, and discounts.",
            "format": "int32"
          },
          "tax": {
            "type": "integer",
            "description": "Tax amount in cents.",
            "format": "int32"
          },
          "tip": {
            "type": "integer",
            "description": "Tip amount in cents.",
            "format": "int32"
          },
          "total": {
            "type": "integer",
            "description": "Final total in cents.",
            "format": "int32"
          },
          "discount": {
            "type": "integer",
            "description": "Discount amount in cents.",
            "format": "int32"
          },
          "rewards": {
            "type": "integer",
            "description": "Reward amount in cents.",
            "format": "int32"
          },
          "serviceCharge": {
            "type": "integer",
            "description": "Service charge amount in cents.",
            "format": "int32"
          },
          "charityAmount": {
            "type": "integer",
            "description": "Charity or donation amount in cents.",
            "format": "int32"
          },
          "fees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderFee"
            },
            "description": "POS-calculated fees included in the order total.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "POS-calculated order totals. All monetary values are in cents."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTypeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "GRUBBRR order-type id; send this value as `orderTypeId` on validate / submit.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Human-readable order-type name (e.g. \"Dine In\", \"Pickup\").",
            "nullable": true
          },
          "supportsTips": {
            "type": "boolean",
            "description": "Whether this order type accepts tips, derived from the location's tip configuration (tips enabled and\r\nthe order type included in the tippable set)."
          },
          "externalPosId": {
            "type": "string",
            "description": "The POS-facing external id for this order type (the configured external delivery mode), when the\r\nlocation's order type is mapped to a POS connector; `null` when no mapping/external id is set.",
            "nullable": true
          },
          "identity": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentityRequirementsDto"
          }
        },
        "additionalProperties": false,
        "description": "A single order type the partner can send as `orderTypeId` on validate / submit."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTypesDto": {
        "type": "object",
        "properties": {
          "orderTypes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTypeDto"
            },
            "description": "The enabled order types available at the location. An empty list means the location has no enabled\r\norder types configured.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The GRUBBRR order types configured for the token's location. Partners read this to discover the valid\r\n`orderTypeId` values to send on `POST /orders/validate` and `POST /orders/submit`. The\r\nlocation is resolved from the access token — never from request input."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateRequest": {
        "required": [
          "externalOrderTypeId",
          "orderTypeId",
          "sessionId"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "minLength": 1,
            "type": "string",
            "description": "Partner-generated cart or checkout session identifier used for traceability and validation logging."
          },
          "orderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "GRUBBRR order type identifier to use for POS validation, for example the configured pickup or dine-in order type."
          },
          "externalOrderTypeId": {
            "minLength": 1,
            "type": "string",
            "description": "Required partner-facing order type code or delivery-mode identifier."
          },
          "externalOrderTypeLabel": {
            "type": "string",
            "description": "Optional partner-facing display label for the order type.",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "description": "Optional partner order identifier. When omitted, validation still returns a POS-calculated response.",
            "nullable": true
          },
          "tipAmount": {
            "type": "integer",
            "description": "Optional tip amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "charityAmount": {
            "type": "integer",
            "description": "Optional charity or donation amount in cents.",
            "format": "int32",
            "nullable": true
          },
          "customer": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderIdentity"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderItem"
            },
            "description": "Menu items to validate. A request must include at least one item or one combo; item-only,\r\ncombo-only, and mixed payloads are all accepted (enforced in M:GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateRequest.Validate(System.ComponentModel.DataAnnotations.ValidationContext)).",
            "nullable": true
          },
          "combos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCombo"
            },
            "description": "Combo selections to validate. A request must include at least one item or one combo.",
            "nullable": true
          },
          "discounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount"
            },
            "description": "Optional order-level discounts to apply, using the shared GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderDiscount model\r\nthat mirrors the POS `OrderDiscount` contract. Omitting this collection leaves the order\r\nundiscounted.",
            "nullable": true
          },
          "coupons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon"
            },
            "description": "Optional coupon discounts to apply, using the shared GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderCoupon model that\r\nmirrors the POS `CouponDiscount` contract — the same coupon channel the kiosk uses for\r\n`ValidateOrderWithStatus` and close-order. Each coupon rides on the order's POS coupon list\r\nand counts as one discount toward the per-location limit.",
            "nullable": true
          },
          "rewards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward"
            },
            "description": "Optional loyalty rewards already validated against this cart (via `POST /loyalty/apply`) to\r\ninclude in pricing. Reuses the GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward the client received from the\r\nloyalty endpoints; set each reward's GRUBBRR.Web.UI.Areas.Partner.Models.PartnerLoyaltyReward.RedeemedAmount (cents) so the\r\nPOS deducts the validated discount. Omitting this collection leaves the order without loyalty rewards.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request body for validating a partner order before submission."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderValidateResponse": {
        "type": "object",
        "properties": {
          "orderId": {
            "type": "string",
            "description": "POS or validation-system order identifier returned by the validation call.",
            "nullable": true
          },
          "totals": {
            "$ref": "#/components/schemas/GRUBBRR.Web.UI.Areas.Partner.Models.PartnerOrderTotals"
          }
        },
        "additionalProperties": false,
        "description": "Successful response from partner order validation."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerQualifyingItemGroupDto": {
        "type": "object",
        "properties": {
          "quantity": {
            "type": "integer",
            "description": "Required quantity from this group.",
            "format": "int32"
          },
          "qualifyingCondition": {
            "type": "string",
            "description": "How quantity is evaluated: `MultiplesOfQuantity`, `ExactQuantityMatch`, or `None`.",
            "nullable": true
          },
          "applyTo": {
            "type": "string",
            "description": "Which matching item receives the discount: `MostExpensive`, `LeastExpensive`, or `None`.",
            "nullable": true
          },
          "itemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Menu item ids that satisfy this group.",
            "nullable": true
          },
          "categoryIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Category ids that satisfy this group.",
            "nullable": true
          },
          "comboIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Combo ids that satisfy this group.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One qualifying-items group: the cart must contain the required quantity of any item, category, or combo\r\nlisted in this group."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerQualifyingModifierDto": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "string",
            "description": "Menu item id that must carry the modifiers.",
            "nullable": true
          },
          "modifierIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Required modifier ids, when the rule targets specific modifiers.",
            "nullable": true
          },
          "modifierGroupId": {
            "type": "string",
            "description": "Required modifier group id, when the rule targets a modifier group.",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "description": "Required item quantity.",
            "format": "int32",
            "nullable": true
          },
          "modifierSelectionCount": {
            "type": "integer",
            "description": "Required selections from the modifier group.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An item that must include specific modifiers to qualify."
      },
      "GRUBBRR.Web.UI.Areas.Partner.Models.PartnerRequiredDiscountDto": {
        "type": "object",
        "properties": {
          "discountId": {
            "type": "string",
            "description": "Discount id that must be on the cart.",
            "nullable": true
          },
          "redeemableLimit": {
            "type": "number",
            "description": "Maximum redemptions of the required discount per order; `null` when unlimited.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Another discount that must already be present on the cart."
      },
      "Microsoft.AspNetCore.Mvc.ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": { }
      },
      "Microsoft.AspNetCore.Mvc.ValidationProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "nullable": true
          }
        },
        "additionalProperties": { }
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "description": "Partner device access token from POST /auth/refresh. Paste the raw JWT only; Swagger adds the 'Bearer ' prefix automatically.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  }
}