{"components":{"schemas":{"AddonInput":{"properties":{"addon_item_id":{"format":"uuid","type":"string"},"quantity":{"format":"int32","type":"integer"},"unit_price":{"description":"Charged unit price (piastres) the POS applied for this addon. When present\nit is RECORDED as the addon's unit_price; absent → the server's expected\n(catalog) price is used.","format":"int32","type":["integer","null"]}},"required":["addon_item_id"],"type":"object"},"CardBrand":{"description":"How a tenant's card should look.\n\nEvery field is optional and the site falls back to Madar's own palette, so a\ntenant who has set nothing still gets a finished card rather than an\nunstyled one. `org_name` is NOT optional: whose card this is must always be\non it, however little else has been configured.","properties":{"background_color":{"description":"`#RRGGBB`, validated on write.","type":["string","null"]},"card_image_url":{"description":"The wide photograph across the card — Apple's strip, Google's hero\nimage, and the band at the top of the web card. Absent is a finished\ncard, not a broken one.","type":["string","null"]},"foreground_color":{"type":["string","null"]},"label_color":{"type":["string","null"]},"logo_is_mark":{"description":"True when the logo is a shape on transparency, so the card may repaint\nit in the foreground for contrast. False for a logo with its background\nbaked in, which gets a plate to sit on instead — repainting that one\nwould give a solid rectangle. See `orgs::branding::is_mark`.","type":"boolean"},"logo_url":{"type":["string","null"]},"org_name":{"description":"The organisation's name. Always present.","type":"string"},"program_name":{"description":"What the programme calls itself (\"Rewards\", \"Bean Club\").","type":"string"},"program_name_ar":{"type":["string","null"]},"social_links":{"description":"Where else to find the shop, in the order a card prints them. Empty is\nthe common case, and the page draws nothing for it — no row, no\nplaceholder.\n\nNOT gated on the branding tier, like `OrgBrand::social_links` it is read\nfrom: a shop's Instagram is a fact about the shop in the way its name\nis, so a Madar-coloured card carries the links too.","items":{"$ref":"#/components/schemas/PublicSocialLink"},"type":"array"}},"required":["org_name","program_name","logo_is_mark","social_links"],"type":"object"},"CardPreferences":{"description":"What a customer can change about their own card, without an account.\n\nThe token in the URL is the credential — the same one their pass carries and\nthe till scans. That is deliberate: a person who has just been messaged must\nbe able to stop the messages by tapping the link in the message, not by\nremembering a password they never made.","properties":{"locale":{"description":"The language they are reading this page in.\n\nSent by the page itself rather than chosen in a form. We stored whatever\ntheir phone said at signup, and a phone that has since changed language\nis a customer still being written to in the wrong one. Opening their own\ncard is the moment we can tell.","type":["string","null"]},"marketing_opt_out":{"description":"Stop sending marketing. Covers the birthday greeting as well as the\nwin-back: someone asking us to stop is asking the SHOP to stop, not to\nbe excluded from one campaign.","type":["boolean","null"]}},"type":"object"},"CardView":{"description":"The member's own card page — what they see when they open the link again.\n\nThe token in the path is the member's secret, which is why this returns only\nwhat the pass already shows and never the phone number in full.","properties":{"balance":{"description":"The live balance, in `mode`'s currency.","format":"int32","type":"integer"},"brand":{"$ref":"#/components/schemas/CardBrand","description":"Whose card this is, and how it should look."},"can_redeem":{"type":"boolean"},"marketing_opt_out":{"description":"They have asked this shop to stop sending them things.","type":"boolean"},"member_token":{"type":"string"},"mode":{"type":"string"},"name":{"type":"string"},"next_reward_cost":{"format":"int32","type":"integer"},"order_now_url":{"description":"\"Order now\": the ordering page, opened knowing who this is. Present only\nwhen public ordering is configured and the shop takes online orders —\nrender the primary button when it is there, nothing when it is not.","type":["string","null"]},"passes":{"$ref":"#/components/schemas/PassLinks"},"points_to_next_reward":{"format":"int32","type":"integer"},"progress_to_next":{"description":"Progress towards the next one, after the earned ones are set aside.","format":"int32","type":"integer"},"rewards":{"items":{"$ref":"#/components/schemas/PublicReward"},"type":"array"},"rewards_ready":{"description":"Rewards the balance has already earned — a card does not stop at full.","format":"int32","type":"integer"}},"required":["name","balance","mode","next_reward_cost","rewards_ready","progress_to_next","points_to_next_reward","can_redeem","member_token","rewards","passes","brand","marketing_opt_out"],"type":"object"},"CartLineInput":{"description":"One line of a public cart. Prices are NOT taken from the client — the server\nresolves them. `addons` reuses [`AddonInput`] but its `unit_price` is ignored.","properties":{"addons":{"items":{"$ref":"#/components/schemas/AddonInput"},"type":"array"},"combo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ComboInput","description":"A combo line's picks (§3.1); the server prices every part. Additive."}]},"menu_item_id":{"format":"uuid","type":"string"},"notes":{"type":["string","null"]},"optional_field_ids":{"items":{"format":"uuid","type":"string"},"type":"array"},"quantity":{"format":"int32","type":"integer"},"size_label":{"type":["string","null"]}},"required":["menu_item_id","quantity"],"type":"object"},"CartQuote":{"properties":{"deal_discount":{"description":"Σ deals' discount.","format":"int32","type":"integer"},"deals":{"items":{"$ref":"#/components/schemas/QuotedDeal"},"type":"array"},"items_total":{"description":"Σ line_total, before deals.","format":"int32","type":"integer"},"lines":{"items":{"$ref":"#/components/schemas/QuotedLine"},"type":"array"},"total_after_deals":{"description":"`items_total − deal_discount` (before the channel discount, tax and fees).","format":"int32","type":"integer"}},"required":["lines","items_total","deals","deal_discount","total_after_deals"],"type":"object"},"ChannelToggles":{"description":"The four sale channels' toggles, resolved: `pos` (the till), `qr` (the\ntable QR menu), `online` (the storefront, all four delivery sub-channels)\nand `delivery` (aggregator apps; stored and returned, honoured by the\nfuture menu push). Every channel is on until the owner switches it off.","properties":{"delivery":{"type":"boolean"},"online":{"type":"boolean"},"pos":{"type":"boolean"},"qr":{"type":"boolean"}},"type":"object"},"ComboInput":{"description":"The `combo` field of an order line naming a combo item.","properties":{"picks":{"items":{"$ref":"#/components/schemas/ComboPickInput"},"type":"array"}},"required":["picks"],"type":"object"},"ComboPickInput":{"description":"One pick of a combo line. On replay the till's `share` and `surcharge`\n(per combo unit) and add-on prices are stored as charged; live they are\nignored and the server prices every part.","properties":{"addons":{"items":{"$ref":"#/components/schemas/AddonInput"},"type":"array"},"menu_item_id":{"format":"uuid","type":"string"},"notes":{"type":["string","null"]},"optional_field_ids":{"items":{"format":"uuid","type":"string"},"type":"array"},"quantity":{"description":"Units per combo unit; the part line's quantity is this × the line's.","format":"int32","type":"integer"},"share":{"description":"Replay only: this pick's share of P, per combo unit.","format":"int32","type":["integer","null"]},"size_label":{"type":["string","null"]},"slot_id":{"format":"uuid","type":"string"},"surcharge":{"description":"Replay only: this pick's surcharge (choice + size), per combo unit.","format":"int32","type":["integer","null"]}},"required":["slot_id","menu_item_id"],"type":"object"},"CustomerAddress":{"description":"A place a customer has had an order sent to.","properties":{"address_line":{"type":["string","null"]},"branch_id":{"description":"The branch it was last ordered from.","format":"uuid","type":["string","null"]},"channel":{"description":"The channel it was last used with: `in_mall`, `outside` or `umbrella`.","type":"string"},"created_at":{"format":"date-time","type":"string"},"customer_id":{"format":"uuid","type":"string"},"delivery_notes":{"type":["string","null"]},"delivery_zone_id":{"format":"uuid","type":["string","null"]},"floor":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"label":{"type":["string","null"]},"landmark":{"type":["string","null"]},"last_used_at":{"format":"date-time","type":"string"},"lat":{"format":"double","type":["number","null"]},"lng":{"format":"double","type":["number","null"]},"place_name":{"type":["string","null"]},"unit_number":{"type":["string","null"]},"use_count":{"format":"int32","type":"integer"}},"required":["id","customer_id","channel","use_count","last_used_at","created_at"],"type":"object"},"DealBranchOverride":{"properties":{"branch_id":{"format":"uuid","type":"string"},"is_active":{"type":"boolean"}},"required":["branch_id","is_active"],"type":"object"},"DealLineInput":{"description":"Units of one order line a deal takes.","properties":{"line_index":{"description":"Index into the order's `items[]`.","format":"int32","type":"integer"},"units":{"format":"int32","type":"integer"}},"required":["line_index","units"],"type":"object"},"DealPoolEntry":{"description":"An item or a category (every kind=item item of it) a deal counts, at one\nsize or any (`size_label` null).","properties":{"category_id":{"format":"uuid","type":["string","null"]},"menu_item_id":{"format":"uuid","type":["string","null"]},"size_label":{"type":["string","null"]}},"type":"object"},"DealRule":{"description":"A deal rule. `n_for_price`: any `qty` units of the pool for `price`.\n`buy_get`: buy `qty`, get `get_qty` at `get_percent`% off (100 = free),\nthe rewarded units drawn from `reward_pool` (or the pool when empty).\nA deal covers the item's size price only; add-ons always pay.\n\nAlso the `deal_rule` feed row of `/sync/pull`, where `is_active` is\nresolved for the device's branch and `sell` carries the branch's channel\ntoggles.","properties":{"branch_overrides":{"items":{"$ref":"#/components/schemas/DealBranchOverride"},"type":"array"},"created_at":{"format":"date-time","type":"string"},"get_percent":{"format":"int32","type":["integer","null"]},"get_qty":{"format":"int32","type":["integer","null"]},"id":{"format":"uuid","type":"string"},"is_active":{"type":"boolean"},"kind":{"description":"`n_for_price` | `buy_get`.","type":"string"},"max_per_order":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"name_translations":{"type":"object"},"pool":{"items":{"$ref":"#/components/schemas/DealPoolEntry"},"type":"array"},"price":{"description":"n_for_price: the price of `qty` units, piastres.","format":"int32","type":["integer","null"]},"qty":{"format":"int32","type":"integer"},"reward_pool":{"description":"buy_get only; `[]` = the rewarded units come from `pool`.","items":{"$ref":"#/components/schemas/DealPoolEntry"},"type":"array"},"sell":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ChannelToggles","description":"Feed rows only: the branch's channel toggles (§11.1). Absent elsewhere."}]},"sort":{"format":"int32","type":"integer"},"updated_at":{"format":"date-time","type":"string"},"windows":{"items":{"$ref":"#/components/schemas/SaleWindow"},"type":"array"}},"required":["id","name","name_translations","kind","qty","sort","is_active","pool","reward_pool","windows","branch_overrides","created_at","updated_at"],"type":"object"},"DeliveryAddonOption":{"description":"One option in the org-wide addon catalog. The catalog is global (the POS\nmodel): every item can use any addon; swap-vs-additive is decided server-side\nfrom the addon `type` + the item recipe at order time. `price` is the\nchannel-effective surcharge in piastres (branch_channel → branch → catalog\ndefault). Channel-unavailable options are excluded from the catalog entirely.","properties":{"addon_item_id":{"format":"uuid","type":"string"},"is_available":{"type":"boolean"},"name":{"type":"string"},"name_translations":{"type":"object"},"price":{"description":"Channel-effective surcharge (piastres). Always present (resolved here).","format":"int32","type":"integer"},"type":{"description":"`milk_type` | `coffee_type` | `extra` — the option's category.","type":"string"}},"required":["addon_item_id","name","name_translations","type","price","is_available"],"type":"object"},"DeliveryMenu":{"properties":{"addons":{"description":"Org-wide addon catalog (global, POS model): channel-effective, grouped by\n`type`, applicable to every item. Channel-unavailable options are excluded.","items":{"$ref":"#/components/schemas/DeliveryAddonOption"},"type":"array"},"categories":{"items":{"$ref":"#/components/schemas/DeliveryMenuCategory"},"type":"array"},"deals":{"description":"The deals on offer on this channel now (§11.2): checkout applies the\nbest ones automatically (`POST …/cart-quote` shows them). Additive.","items":{"$ref":"#/components/schemas/DealRule"},"type":"array"},"discount":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeliveryMenuDiscount","description":"The active discount for this channel (customer-facing) or `null`. Applies\nto the item subtotal only — the delivery fee is always charged in full."}]},"items":{"items":{"$ref":"#/components/schemas/DeliveryMenuItem"},"type":"array"}},"required":["categories","items","addons","deals"],"type":"object"},"DeliveryMenuCategory":{"properties":{"id":{"format":"uuid","type":"string"},"image_url":{"type":["string","null"]},"name":{"type":"string"},"name_translations":{"type":"object"}},"required":["id","name","name_translations"],"type":"object"},"DeliveryMenuDiscount":{"description":"Customer-facing summary of a channel's active discount, so the public UI can\ntell the customer \"you've got X off\" and show a discounted estimate.","properties":{"dtype":{"description":"\"percentage\" | \"fixed\".","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"name_translations":{"type":"object"},"value":{"description":"LEGACY SPELLING — an integer, 0-100 for a percentage; piastres for\n`fixed`. See `discounts::wire`.","format":"int64","type":"integer"},"value_rate":{"description":"The stored fraction, for clients that know to ask.","format":"double","type":"number"}},"required":["id","name","name_translations","dtype","value","value_rate"],"type":"object"},"DeliveryMenuItem":{"properties":{"allowed_addon_ids":{"description":"Explicit per-item addon allowlist (IDs from `menu_item_allowed_addons`).\nWhen non-empty the customizer filters the global catalog to these IDs by\ndefault, with a \"show all\" escape hatch. Empty = no restriction.","items":{"format":"uuid","type":"string"},"type":"array"},"category_id":{"format":"uuid","type":["string","null"]},"combo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PublicCombo","description":"A kind=combo row: its slots with every choice priced for this channel\n(categories expanded to their available items). The server still\nprices the order."}]},"default_milk_addon_id":{"description":"The item's base/default milk: the `milk_type` addon whose ingredient\nmatches the item recipe's milk ingredient. The online customizer\npre-selects it (mirrors the POS default-milk selection). `None` when the\nitem has no milk in its recipe or no matching milk addon exists.","format":"uuid","type":["string","null"]},"description":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"image_url":{"type":["string","null"]},"kind":{"description":"`item` | `combo` (combos module). Additive.","type":"string"},"meal":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MealLink","description":"A kind=item row: its \"make it a meal\" upsell (C14), when that combo is\non this menu."}]},"modifier_groups":{"description":"The item's modifier groups (unified model), channel-effective: every\nactive attached group, a group with no option here included (options\n`[]`). Non-empty ⇒ the item's add-ons are SET: the page offers only\nwhat these groups hold, and no \"show all\" (none at all when every\ngroup is empty). Empty ⇒ not set up in the unified model: the page\nfalls back to `addons` + `allowed_addon_ids`.","items":{"$ref":"#/components/schemas/DeliveryModifierGroup"},"type":"array"},"name":{"type":"string"},"name_translations":{"type":"object"},"optionals":{"items":{"$ref":"#/components/schemas/DeliveryOptionalField"},"type":"array"},"price":{"format":"int32","type":"integer"},"sizes":{"items":{"$ref":"#/components/schemas/DeliveryMenuSize"},"type":"array"}},"required":["id","name","name_translations","price","sizes","optionals","allowed_addon_ids","modifier_groups","kind"],"type":"object"},"DeliveryMenuSize":{"properties":{"label":{"type":"string"},"price":{"format":"int32","type":"integer"}},"required":["label","price"],"type":"object"},"DeliveryModifierGroup":{"description":"A per-item modifier group from the unified model (`menu_item_modifier_groups`\n→ `modifier_groups`/`modifier_options`), constraints resolved (attachment\noverrides beat group defaults) and options already filtered to the\nattachment's `included_option_ids`. Only addon-sourced options appear here —\nthe item's priced optionals stay in `optionals`. Empty until the org's\ncatalog is backfilled onto the unified tables; the customizer falls back to\nthe flat `addons` catalog + `allowed_addon_ids` in that case.","properties":{"addon_type":{"description":"The group's legacy addon type (`milk_type` / `coffee_type` / `extra` /\ncustom) — the swap-family hint the customizer keys its delta-price\nestimate on. `None` for groups with no legacy lineage.","type":["string","null"]},"group_id":{"format":"uuid","type":"string"},"is_required":{"type":"boolean"},"max_selections":{"format":"int32","type":["integer","null"]},"min_selections":{"format":"int32","type":"integer"},"name":{"type":"string"},"name_translations":{"type":"object"},"options":{"items":{"$ref":"#/components/schemas/DeliveryModifierOption"},"type":"array"},"selection_type":{"description":"\"single\" | \"multi\".","type":"string"}},"required":["group_id","name","name_translations","selection_type","min_selections","is_required","options"],"type":"object"},"DeliveryModifierOption":{"description":"One option inside a per-item modifier group. `option_id` is the STABLE id —\nit equals the legacy `addon_item_id`, so order intake accepts it unchanged in\n`addons[].addon_item_id` (menu-unification stable-id rule).","properties":{"name":{"type":"string"},"name_translations":{"type":"object"},"option_id":{"format":"uuid","type":"string"},"price":{"description":"Channel-effective surcharge (piastres): branch_channel → branch →\nchannel → catalog default. Unavailable options are excluded entirely.","format":"int32","type":"integer"}},"required":["option_id","name","name_translations","price"],"type":"object"},"DeliveryOptionalField":{"description":"A per-item optional toggle (e.g. \"Extra hot\", \"No sugar\"). `price` is the\npiastres surcharge; `size_label` is set when the optional only applies to a\nspecific size.","properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"name_translations":{"type":"object"},"price":{"format":"int32","type":"integer"},"size_label":{"type":["string","null"]}},"required":["id","name","name_translations","price"],"type":"object"},"DeliveryOrder":{"properties":{"address_id":{"description":"The saved address it was sent to, when it was saved.","format":"uuid","type":["string","null"]},"address_line":{"type":["string","null"]},"branch_id":{"format":"uuid","type":"string"},"cancel_reason":{"type":["string","null"]},"cancel_restocked":{"type":["boolean","null"]},"cancelled_at":{"format":"date-time","type":["string","null"]},"cart":{"description":"The frozen priced line snapshot the POS renders before finalize.","type":"object"},"channel":{"type":"string"},"confirmed_at":{"format":"date-time","type":["string","null"]},"contact_override":{"description":"\"Ordered by X for Y\": the snapshot phone is not the customer's own.","type":"boolean"},"created_at":{"format":"date-time","type":"string"},"customer_id":{"description":"The customer this order belongs to (design §2.5). `customer_name` and\n`customer_phone` beside it are the SNAPSHOT — what was typed, what the\ndriver calls — and stay as they were whatever happens to the customer.\n`None` for an order from before customers existed whose phone is not a\nvalid number.","format":"uuid","type":["string","null"]},"customer_lat":{"format":"double","type":["number","null"]},"customer_lng":{"format":"double","type":["number","null"]},"customer_name":{"type":"string"},"customer_phone":{"type":"string"},"delivered_at":{"format":"date-time","type":["string","null"]},"delivery_fee":{"format":"int32","type":"integer"},"delivery_notes":{"type":["string","null"]},"delivery_ref":{"type":["string","null"]},"delivery_zone_id":{"format":"uuid","type":["string","null"]},"discount_amount":{"format":"int32","type":"integer"},"discount_id":{"description":"Frozen channel discount on the item subtotal. `discount_amount` is 0\nwhen none.","format":"uuid","type":["string","null"]},"discount_rate":{"description":"The stored value — a fraction for a percentage. Same column as\n[`DeliveryOrder::discount_value`].","format":"double","type":"number"},"discount_type":{"type":["string","null"]},"discount_value":{"description":"LEGACY SPELLING on the wire — an integer, 0-100 for a percentage. See\n`discounts::wire`: a double here fails to deserialise the whole\nDELIVERY ORDER on every shipped till, not just this field.","format":"int64","type":"integer"},"distance_source":{"description":"How `road_distance_meters` was measured: `osrm` (routed) or `haversine`\n(straight line — the routing fallback, and always the in-mall walking\ndistance). `None` exactly when no distance was recorded.","type":["string","null"]},"extra_prep_minutes":{"description":"Extra prep minutes the teller added on top of the branch base (multiples of 5).","format":"int32","type":"integer"},"floor":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"landmark":{"type":["string","null"]},"order_id":{"format":"uuid","type":["string","null"]},"org_id":{"format":"uuid","type":"string"},"otp_verified":{"type":"boolean"},"out_for_delivery_at":{"format":"date-time","type":["string","null"]},"payment_method":{"description":"What was actually taken at the door. Set at finalize and only then;\n`Some` exactly when the order is `delivered`.","type":["string","null"]},"payment_method_hint":{"description":"What the customer SAID they would pay with, at checkout. Display only.","type":["string","null"]},"place_name":{"type":["string","null"]},"preparing_at":{"format":"date-time","type":["string","null"]},"ready_at":{"format":"date-time","type":["string","null"]},"receipt_printed_at":{"format":"date-time","type":["string","null"]},"rejected_at":{"format":"date-time","type":["string","null"]},"road_distance_meters":{"format":"int32","type":["integer","null"]},"service_charge_amount":{"description":"Always 0: the service charge is dine-in only. Present so the till can\nrender the same breakdown for every kind of sale.","format":"int32","type":"integer"},"service_charge_rate_applied":{"format":"double","type":"number"},"status":{"type":"string"},"subtotal":{"format":"int32","type":"integer"},"tax_amount":{"description":"The tax as priced at intake, under the policy frozen beside it. Inside\n`total` when `tax_inclusive`, added to it otherwise. Finalize does not\nre-price: a rate the shop changes between the quote and the door does\nnot move a bill the customer already agreed.","format":"int32","type":"integer"},"tax_inclusive":{"description":"Copy of the ONE inclusivity flag (org, branch override) as it stood at\nintake — not a setting of its own.","type":"boolean"},"tax_rate_applied":{"description":"Fraction, not a percentage: `0.14` is 14%.","format":"double","type":"number"},"total":{"description":"The quote: `subtotal - discount_amount + delivery_fee`, plus\n`tax_amount` when the tax is exclusive. Replayed verbatim at finalize.","format":"int32","type":"integer"},"unit_number":{"type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","org_id","branch_id","channel","status","customer_name","customer_phone","subtotal","delivery_fee","total","extra_prep_minutes","cart","otp_verified","created_at","updated_at"],"type":"object"},"DeliveryOrderInput":{"properties":{"address_line":{"type":["string","null"]},"branch_id":{"format":"uuid","type":"string"},"channel":{"type":"string"},"contact_device_token":{"description":"A one-time order to a different phone, at a branch that requires OTP:\nthe device token proving THAT phone.","type":["string","null"]},"customer_lat":{"format":"double","type":["number","null"]},"customer_lng":{"format":"double","type":["number","null"]},"customer_name":{"type":"string"},"customer_phone":{"type":"string"},"delivery_notes":{"type":["string","null"]},"device_token":{"description":"Device-trust token from OTP verify (proves the phone). With\n`member_token` it must prove the CUSTOMER's phone, not the typed one.","type":"string"},"floor":{"type":["string","null"]},"identity_change":{"description":"What a typed name/phone that differs from the customer's MEANS:\n`\"one_time\"` (ordering for someone else: the order's snapshot carries\nthe typed contact, the profile is untouched) or `\"update_name\"` (correct\nthe stored name). A different PHONE with no choice is refused with 409\n`IDENTITY_CHOICE_REQUIRED` (`kind: \"phone\"`); a different name alone\ndefaults to one-time. Ignored without `member_token`.","type":["string","null"]},"items":{"items":{"$ref":"#/components/schemas/CartLineInput"},"type":"array"},"landmark":{"type":["string","null"]},"member_token":{"description":"Ordering from a loyalty card (\"order now\"): the order belongs to the\ncard's customer whatever name and phone are typed. The server compares\nthe typed contact with the customer's and classifies the difference —\nsee `identity_change`.","type":["string","null"]},"payment_method_hint":{"description":"\"cash\" | \"card\" — a hint the teller can change at finalize.","type":"string"},"place_name":{"type":["string","null"]},"save_address":{"description":"Keep the address on the customer's profile. Defaults to yes for an\nordinary order and to NO for a one-time order for someone else.","type":["boolean","null"]},"unit_number":{"type":["string","null"]}},"required":["branch_id","channel","customer_name","customer_phone","payment_method_hint","device_token","items"],"type":"object"},"DeliveryTracking":{"description":"Customer-safe tracking view of a delivery order, keyed by its opaque UUID\n(same capability-URL trust model as the device-token flow). No phone number\nis exposed; the destination fields are the customer's own inputs. Powers the\npublic `/track/{id}` page (polled, since the public surface has no SSE).","properties":{"address_line":{"type":["string","null"]},"branch_name":{"type":"string"},"cancel_reason":{"type":["string","null"]},"cancelled_at":{"format":"date-time","type":["string","null"]},"channel":{"type":"string"},"confirmed_at":{"format":"date-time","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"customer_name":{"type":"string"},"delivered_at":{"format":"date-time","type":["string","null"]},"delivery_fee":{"format":"int32","type":"integer"},"delivery_ref":{"type":["string","null"]},"discount_amount":{"format":"int32","type":"integer"},"estimated_prep_minutes":{"description":"Branch base prep time + the teller's per-order addition (minutes).","format":"int32","type":"integer"},"floor":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"org_id":{"format":"uuid","type":"string"},"out_for_delivery_at":{"format":"date-time","type":["string","null"]},"payment_method_hint":{"type":["string","null"]},"place_name":{"type":["string","null"]},"preparing_at":{"format":"date-time","type":["string","null"]},"ready_at":{"format":"date-time","type":["string","null"]},"rejected_at":{"format":"date-time","type":["string","null"]},"status":{"type":"string"},"subtotal":{"format":"int32","type":"integer"},"tax_amount":{"description":"The tax line, frozen at intake. Inside `total` when `tax_inclusive`\n(render \"includes VAT\"), added to it otherwise (render a tax line).","format":"int32","type":"integer"},"tax_inclusive":{"type":"boolean"},"tax_rate_applied":{"format":"double","type":"number"},"total":{"format":"int32","type":"integer"},"unit_number":{"type":["string","null"]}},"required":["id","org_id","branch_name","channel","status","created_at","estimated_prep_minutes","subtotal","delivery_fee","discount_amount","tax_amount","tax_rate_applied","tax_inclusive","total","customer_name"],"type":"object"},"ErrorBody":{"description":"Wire shape of every error JSON. Keep in lockstep with\n`AppError::error_response` below.","properties":{"code":{"description":"Stable, machine-readable code for the error classes a client must branch\non programmatically (e.g. `ORG_SUSPENDED`). Omitted for the generic\ncases where the status code alone is enough.","example":"ORG_SUSPENDED","type":["string","null"]},"error":{"description":"Human-readable error message.","example":"Something went wrong","type":"string"},"retry_after_seconds":{"description":"How long to wait before trying again, in seconds. Present on a\n`PIN_THROTTLED` refusal, absent everywhere else, so the PIN pad can run\na countdown rather than inventing one.","format":"int64","type":["integer","null"]},"till":{"description":"The till a `TILL_OPEN_AT_OTHER_BRANCH` / `TILL_OPEN_ELSEWHERE` refusal\nis about (`TillBrief`). Omitted everywhere else.","type":["object","null"]},"vars":{"description":"The figures of a coded refusal (`CodedVars`), for the client's own\nwording. Omitted everywhere else.","type":["object","null"]}},"required":["error"],"type":"object"},"GuestSavedLocation":{"properties":{"address_line":{"type":["string","null"]},"branch_id":{"format":"uuid","type":"string"},"channel":{"type":"string"},"customer_lat":{"format":"double","type":["number","null"]},"customer_lng":{"format":"double","type":["number","null"]},"floor":{"type":["string","null"]},"landmark":{"type":["string","null"]},"last_used_at":{"format":"date-time","type":"string"},"place_name":{"type":["string","null"]},"unit_number":{"type":["string","null"]}},"required":["branch_id","channel","last_used_at"],"type":"object"},"HoursEntry":{"description":"One weekly booking window. `dow`: 0 = Sunday … 6 = Saturday. `open`/`close`\nare `HH:MM` local; a close at or before open means \"closes after midnight\".","properties":{"close":{"type":"string"},"dow":{"format":"int32","minimum":0,"type":"integer"},"open":{"type":"string"}},"required":["dow","open","close"],"type":"object"},"JoinInfo":{"description":"What the signup page needs to render itself before anyone types anything.","properties":{"birthday_enabled":{"description":"Ask for a date of birth. False means the form does not show the field —\na shop that does not run birthday rewards is not given one to hold.","type":"boolean"},"birthday_reward_amount":{"description":"What the birthday is worth here, so the page can say what it is FOR\nrather than asking for a date of birth and explaining nothing.","format":"int32","type":["integer","null"]},"branch_id":{"description":"Absent for an org-wide code — the customer has not told us where they\nare, and nothing in the programme needs to know.","format":"uuid","type":["string","null"]},"branch_name":{"type":["string","null"]},"brand":{"$ref":"#/components/schemas/CardBrand","description":"Whose programme this is, and how the page should look."},"earn_piastres_per_point":{"description":"EGP that earns one point — the page's \"a point for every N EGP\" line.\nPiastres on the wire, as everywhere; the page divides by 100. Only\nmeaningful when `mode` is `\"points\"`.","format":"int32","type":"integer"},"enabled":{"description":"False when the program is off here — the page says so instead of taking\na signup that would go nowhere.","type":"boolean"},"mode":{"description":"`\"points\"` (earned on spend) or `\"visits\"` (a stamp per order) — which\nsentence the page writes.","type":"string"},"next_reward_cost":{"description":"The cheapest reward on offer, in `mode`'s currency.","format":"int32","type":"integer"},"require_otp":{"description":"The page collects an OTP only when the branch asks for one.","type":"boolean"},"rewards":{"description":"The rewards on offer, each with what it costs.","items":{"$ref":"#/components/schemas/PublicReward"},"type":"array"},"terms":{"type":["string","null"]},"terms_ar":{"type":["string","null"]}},"required":["brand","enabled","require_otp","mode","next_reward_cost","earn_piastres_per_point","rewards","birthday_enabled"],"type":"object"},"JoinInput":{"properties":{"birth_day":{"format":"int32","type":["integer","null"]},"birth_month":{"description":"The day of their birthday, 1–12 and 1–31. Accepted ONLY where the org\nasked for one: a field the shop turned off must not be storable by\nposting past the form.\n\nNo year, deliberately. A greeting needs to know WHEN, not how old — and\na full date of birth is an identity credential, which is a great deal\nmore than an annual message needs.","format":"int32","type":["integer","null"]},"branch_id":{"description":"The branch whose counter code was scanned, when one was. Absent for an\norg-wide code — see [`BranchQuery`].","format":"uuid","type":["string","null"]},"device_token":{"description":"Device-trust token from `/public/otp/verify`. Required only when the\nbranch's `require_otp` is on.","type":["string","null"]},"locale":{"description":"'en' or 'ar' — the language the pass is written in.","type":["string","null"]},"name":{"type":"string"},"org_id":{"format":"uuid","type":["string","null"]},"phone":{"type":"string"}},"required":["name","phone"],"type":"object"},"JoinResult":{"description":"What the customer sees after signing up: their card, and the buttons — or,\nfor a phone that is already a member and has not been proved, an invitation\nto prove it.\n\nThe member token is a bearer credential: whoever holds it holds the card,\nthe balance, the purchase history and the wallet passes. So it is handed out\non exactly two occasions — to a NEW member, whose token nobody else could\nwant yet, and to an existing member whose device has verified THIS phone by\nOTP. Typing a phone number is not proof of owning it; anyone who knows a\ncustomer's number can type it.","properties":{"already_member":{"description":"True when this phone was already a member — the page says \"welcome back\"\nrather than pretending to have made a new card.","type":"boolean"},"balance":{"description":"The live balance, in `mode`'s currency. Zero for a fresh member, and zero\n(not the real figure) while `verify_required`.","format":"int32","type":"integer"},"brand":{"$ref":"#/components/schemas/CardBrand"},"card_link_sent":{"description":"While `verify_required`: the card link was also sent to the number on\nfile, by WhatsApp — the one channel that proves possession without a\ncode. False when no gateway is configured or there is no public base to\nbuild a link on; the page then offers only the OTP.","type":"boolean"},"member_token":{"description":"Absent when `verify_required`: the page has nothing to show yet.","type":["string","null"]},"mode":{"type":"string"},"name":{"description":"The name as the caller typed it. For a returning member the name ON FILE\nis not echoed until they have verified — it is a fact about the person\nwho owns the phone, not about the person typing it.","type":"string"},"next_reward_cost":{"format":"int32","type":"integer"},"passes":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PassLinks","description":"Absent when `verify_required`."}]},"verify_required":{"description":"This phone already has a card and the device has not proved it owns the\nphone. The page should run the ordinary OTP flow (`/public/otp/request`\nthen `/public/otp/verify`) and POST here again with the `device_token`\nit is handed; the card comes back on that call.","type":"boolean"}},"required":["name","balance","mode","next_reward_cost","brand","already_member","verify_required","card_link_sent"],"type":"object"},"LinksItemKind":{"description":"What a button is. The four modules are a closed list, like the social\nplatforms: each one is a page we serve, and `custom` is the shop's own link.","enum":["order","menu","rewards","book","custom"],"type":"string"},"MealLink":{"description":"An item's \"make it a meal\" upsell: the combo, and the slot this item fills.","properties":{"combo_id":{"format":"uuid","type":"string"},"slot_id":{"format":"uuid","type":"string"}},"required":["combo_id","slot_id"],"type":"object"},"OnlineTaxPolicy":{"description":"What the storefront needs to render a bill honestly: the rate, and whether\nthe menu prices already contain it. Sent with the branch list and with every\nquote so a page can show a tax line (exclusive) or an \"includes VAT\" note\n(inclusive) instead of quietly under-quoting. No service-charge fields: an\nonline order never carries one, and a field that is always zero invites a\npage to render a line for it.","properties":{"tax_inclusive":{"description":"`true` = menu prices already contain the tax; the total will not grow.","type":"boolean"},"tax_rate":{"description":"Fraction, NOT a percentage: `0.14` is 14%.","example":0.14,"format":"double","type":"number"}},"required":["tax_rate","tax_inclusive"],"type":"object"},"OpenTicketItemView":{"properties":{"id":{"format":"uuid","type":"string"},"line":{"description":"The frozen priced SnapshotLine (name, size, addons, totals)."},"line_total":{"format":"int32","type":"integer"},"menu_item_id":{"format":"uuid","type":["string","null"]},"round_fired_at":{"description":"When the round this line came in on was fired. A bill is read as a\nsequence of visits to the table — \"the drinks at seven, the food at\nhalf past\" — and without the clock a till can only show a flat list\nthat says nothing about how the evening went.","format":"date-time","type":"string"},"round_number":{"format":"int32","type":"integer"},"voided":{"type":"boolean"}},"required":["id","round_number","round_fired_at","line","line_total","voided"],"type":"object"},"OpenTicketView":{"properties":{"bill":{"$ref":"#/components/schemas/TicketBill","description":"The bill as the SERVER prices it — see [`TicketBill`]. This is the\nfigure the till shows and the drawer collects, because it is the figure\nthe settle will book; `subtotal` above is only its first line."},"booking_id":{"description":"The booking this ticket seated, if the party had one.","format":"uuid","type":["string","null"]},"branch_id":{"format":"uuid","type":"string"},"customer_id":{"description":"The customer this bill belongs to, when one is known (design §2.5): a\ntable-QR guest who gave a phone, the party's booking, or one the waiter\nattached. `customer_name` is the free-text snapshot and may be set\nwithout it. Settling carries it onto the sale.","format":"uuid","type":["string","null"]},"customer_name":{"type":["string","null"]},"discount_id":{"description":"The discount the waiter put on the bill at fire time, if any. Shown so\nthe cashier can SEE what a settle will inherit — and clear it with an\nexplicit `discount_type: \"none\"` rather than have it applied silently.","format":"uuid","type":["string","null"]},"discount_type":{"type":["string","null"]},"discount_value":{"format":"double","type":["number","null"]},"guest_count":{"format":"int32","type":["integer","null"]},"id":{"format":"uuid","type":"string"},"items":{"items":{"$ref":"#/components/schemas/OpenTicketItemView"},"type":"array"},"notes":{"type":["string","null"]},"opened_at":{"format":"date-time","type":"string"},"opened_by":{"format":"uuid","type":"string"},"opened_by_name":{"type":["string","null"]},"order_id":{"format":"uuid","type":["string","null"]},"ready":{"description":"The kitchen has plated every line of every round. DERIVED from the\nticket's `kitchen_tickets` at read time, so it is always what the KDS\nsays now. `false` for a ticket nothing was ever fired to the kitchen for\n(routing mode `off`): there is nothing to be ready.","type":"boolean"},"ready_at":{"description":"The last moment the kitchen had the whole ticket plated. History for\nthe timing reports; `ready` is the live fact.","format":"date-time","type":["string","null"]},"settled_at":{"format":"date-time","type":["string","null"]},"status":{"description":"The bill: `open`, `settled` or `voided`. Never `ready` — see [`Self::ready`].","type":"string"},"subtotal":{"format":"int32","type":"integer"},"table_id":{"format":"uuid","type":["string","null"]},"ticket_ref":{"type":["string","null"]},"timezone":{"description":"The branch's effective IANA timezone (see `crate::tz`) — the zone this\nticket's times are shown in. Additive.","type":["string","null"]},"void_note":{"type":["string","null"]},"void_reason":{"description":"Categorised like an order void, so void-rate reports read dine-in and\ncounter alike.","type":["string","null"]},"voided_at":{"format":"date-time","type":["string","null"]}},"required":["id","branch_id","status","opened_by","subtotal","opened_at","items"],"type":"object"},"OrderHistorySummary":{"properties":{"address_line":{"type":["string","null"]},"branch_id":{"format":"uuid","type":"string"},"branch_name":{"type":"string"},"channel":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"customer_lat":{"format":"double","type":["number","null"]},"customer_lng":{"format":"double","type":["number","null"]},"customer_name":{"type":"string"},"delivery_fee":{"format":"int32","type":"integer"},"delivery_ref":{"type":["string","null"]},"discount_amount":{"format":"int32","type":"integer"},"id":{"format":"uuid","type":"string"},"items":{"description":"Frozen cart snapshot: the items at the time of the order (for display)."},"place_name":{"type":["string","null"]},"status":{"type":"string"},"subtotal":{"format":"int32","type":"integer"},"tax_amount":{"description":"Tax as frozen at intake: inside `total` when `tax_inclusive`, added to\nit otherwise.","format":"int32","type":"integer"},"tax_inclusive":{"type":"boolean"},"total":{"format":"int32","type":"integer"}},"required":["id","status","channel","created_at","branch_id","branch_name","subtotal","delivery_fee","discount_amount","tax_amount","tax_inclusive","total","customer_name","items"],"type":"object"},"OrderItemInput":{"properties":{"addons":{"items":{"$ref":"#/components/schemas/AddonInput"},"type":"array"},"combo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ComboInput","description":"A line naming a combo item (`kind=combo`) carries its picks here; see\nCOMBOS_CONTRACT.md §3.1. On replay `unit_price` is P as the till\ncharged it and each pick's `share`/`surcharge` are per combo unit.\nAdditive."}]},"menu_item_id":{"format":"uuid","type":["string","null"]},"notes":{"type":["string","null"]},"optional_field_ids":{"items":{"format":"uuid","type":"string"},"type":"array"},"quantity":{"format":"int32","type":"integer"},"size_label":{"type":["string","null"]},"staff_drink":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StaffDrinkLine","description":"Put this line on the branch's STAFF POOL: a normal sale whose base\nconfiguration (cheapest size + the default of each required choice) is\ncomped, extras still charged. Needs `orders.staff_drink.record`. The\nserver prices the comp; see `docs/staff-drink-comp-contract.md`.\nAdditive — a client that omits it rings an ordinary paid line. Not\ncarried by a ticket's line."}]},"unit_price":{"description":"What the customer was actually charged, in piastres.\n\nRead ONLY when a queued offline sale is replayed — see [`ClientPrices`].\nOn the live path the server prices the line and this is ignored, so a\ntill cannot charge a price of its own choosing and no manual override\nexists to let anyone try.","format":"int32","type":["integer","null"]}},"required":["quantity"],"type":"object"},"OrderNowAddress":{"allOf":[{"$ref":"#/components/schemas/CustomerAddress"},{"properties":{"stale":{"description":"True when it can no longer be delivered to from the last branch.","type":"boolean"},"stale_reason":{"description":"`out_of_zone` | `zone_unavailable` | `branch_unavailable`.","type":["string","null"]}},"required":["stale"],"type":"object"}]},"OrderNowBranch":{"description":"The branch the customer last ordered from, re-checked now.","properties":{"channel":{"description":"The channel they last used there.","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"stale":{"description":"True when it cannot be used as-is right now; the client falls back to\nits branch/channel chooser and keeps the rest of the prefill.","type":"boolean"},"stale_reason":{"description":"`branch_unavailable` | `channel_closed`.","type":["string","null"]}},"required":["id","name","channel","stale"],"type":"object"},"OrderNowContext":{"properties":{"first_name":{"description":"First word of the name on file.","type":"string"},"full":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/OrderNowFull"}]},"last_branch_name":{"description":"NAME only on the masked path; the full context carries the id.","type":["string","null"]},"logo_url":{"type":["string","null"]},"org_id":{"format":"uuid","type":"string"},"org_name":{"type":"string"},"phone_hint":{"description":"`•••• 4567`.","type":"string"},"verify_required":{"description":"True → this is the masked context; verify the phone\n(`/public/otp/request|verify`) and ask again with the device token.","type":"boolean"}},"required":["verify_required","org_id","org_name","first_name","phone_hint"],"type":"object"},"OrderNowFull":{"description":"Everything a verified device gets. Absent from the masked context.","properties":{"addresses":{"description":"Most recently used first.","items":{"$ref":"#/components/schemas/OrderNowAddress"},"type":"array"},"customer_id":{"format":"uuid","type":"string"},"last_branch":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/OrderNowBranch","description":"Derived from the latest order; `None` before the first one."}]},"last_payment_hint":{"description":"`cash` | `card`, as they last said.","type":["string","null"]},"locale":{"type":"string"},"name":{"type":"string"},"phone":{"description":"Canonical (`2010…`).","type":"string"}},"required":["customer_id","name","phone","locale","addresses"],"type":"object"},"OtpRequestInput":{"properties":{"phone":{"type":"string"}},"required":["phone"],"type":"object"},"OtpRequestResponse":{"properties":{"sent":{"type":"boolean"}},"required":["sent"],"type":"object"},"OtpVerifyInput":{"properties":{"code":{"type":"string"},"phone":{"type":"string"}},"required":["phone","code"],"type":"object"},"OtpVerifyResponse":{"properties":{"device_token":{"type":"string"}},"required":["device_token"],"type":"object"},"PassLinks":{"description":"What signup hands the customer. Either side may be absent: a tenant with only\nGoogle credentials configured shows one button, not a broken one.","properties":{"any":{"description":"False when neither wallet is configured — the site shows the member's\nQR on the page instead of dead buttons.","type":"boolean"},"apple_url":{"description":"Downloads the signed `.pkpass`. Site-relative, because the signup page\nis served from the same origin as the API — so a pass needs a\nCERTIFICATE, not a configured base URL.","type":["string","null"]},"google_url":{"description":"`https://pay.google.com/gp/v/save/<jwt>`.","type":["string","null"]}},"required":["any"],"type":"object"},"PastOrder":{"description":"One past visit, as the customer's own page shows it.","properties":{"branch_name":{"type":"string"},"id":{"format":"uuid","type":"string"},"items":{"description":"What they had. The customer asked for this to be here; see the note on\nthe handler about who else can see it.","items":{"type":"string"},"type":"array"},"placed_at":{"format":"date-time","type":"string"},"total":{"description":"In piastres, like every other figure on the wire.","format":"int32","type":"integer"}},"required":["id","placed_at","branch_name","total","items"],"type":"object"},"PastOrders":{"properties":{"orders":{"items":{"$ref":"#/components/schemas/PastOrder"},"type":"array"}},"required":["orders"],"type":"object"},"PublicBookingBranch":{"properties":{"code":{"type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"}},"required":["id","name","code"],"type":"object"},"PublicBookingChange":{"properties":{"notes":{"type":["string","null"]},"party_size":{"format":"int32","type":["integer","null"]},"starts_at":{"format":"date-time","type":["string","null"]}},"type":"object"},"PublicBookingInfo":{"properties":{"blackout_dates":{"items":{"type":"string"},"type":"array"},"branch_id":{"format":"uuid","type":"string"},"branch_name":{"type":"string"},"default_duration_minutes":{"format":"int32","type":"integer"},"enabled":{"type":"boolean"},"horizon_days":{"format":"int32","type":"integer"},"hours":{"items":{"$ref":"#/components/schemas/HoursEntry"},"type":"array"},"lead_time_minutes":{"format":"int32","type":"integer"},"max_party":{"format":"int32","type":"integer"},"min_party":{"format":"int32","type":"integer"},"org_name":{"type":"string"},"require_otp":{"type":"boolean"},"slot_minutes":{"format":"int32","type":"integer"},"timezone":{"type":"string"},"today":{"description":"Today's service date in the branch zone (the picker's floor).","type":"string"}},"required":["branch_id","branch_name","org_name","enabled","timezone","slot_minutes","default_duration_minutes","min_party","max_party","lead_time_minutes","horizon_days","require_otp","hours","blackout_dates","today"],"type":"object"},"PublicBookingInput":{"properties":{"branch_id":{"format":"uuid","type":"string"},"device_token":{"description":"From `/public/otp/verify`; required when the branch requires OTP.","type":["string","null"]},"guest_name":{"type":"string"},"locale":{"type":["string","null"]},"notes":{"type":["string","null"]},"party_size":{"format":"int32","type":"integer"},"phone":{"type":"string"},"starts_at":{"format":"date-time","type":"string"}},"required":["branch_id","starts_at","party_size","guest_name","phone"],"type":"object"},"PublicBookingView":{"description":"What a guest sees on the manage page — no table ids, no staff fields.","properties":{"branch_id":{"format":"uuid","type":"string"},"branch_name":{"type":"string"},"can_modify":{"description":"Still confirmed and further away than the branch's lead time.","type":"boolean"},"ends_at":{"format":"date-time","type":"string"},"guest_name":{"type":"string"},"id":{"format":"uuid","type":"string"},"manage_token":{"type":"string"},"notes":{"type":["string","null"]},"party_size":{"format":"int32","type":"integer"},"starts_at":{"format":"date-time","type":"string"},"status":{"type":"string"},"timezone":{"type":"string"}},"required":["id","manage_token","status","branch_id","branch_name","timezone","party_size","starts_at","ends_at","guest_name","can_modify"],"type":"object"},"PublicBranch":{"properties":{"code":{"type":"string"},"id":{"format":"uuid","type":"string"},"in_mall_enabled":{"type":"boolean"},"in_mall_open_now":{"description":"Effective-open right now (enabled + open shift + override + window).","type":"boolean"},"in_mall_require_location":{"description":"When false, in-mall ordering does not require a device GPS location.","type":"boolean"},"name":{"type":"string"},"otp_required":{"description":"When false, the public checkout skips OTP verification for this branch.","type":"boolean"},"outside_enabled":{"type":"boolean"},"outside_open_now":{"type":"boolean"},"pickup_enabled":{"type":"boolean"},"pickup_open_now":{"type":"boolean"},"tax_policy":{"$ref":"#/components/schemas/OnlineTaxPolicy","description":"The tax this branch prices online orders under. The storefront renders\na tax line from it (exclusive) or an \"includes VAT\" note (inclusive) —\nthe same policy intake will freeze onto the order."},"umbrella_enabled":{"type":"boolean"},"umbrella_open_now":{"type":"boolean"}},"required":["id","name","code","in_mall_enabled","outside_enabled","umbrella_enabled","pickup_enabled","in_mall_open_now","outside_open_now","umbrella_open_now","pickup_open_now","otp_required","in_mall_require_location","tax_policy"],"type":"object"},"PublicBrand":{"description":"A shop, as a guest page needs to know it.","properties":{"accent_color":{"type":"string"},"background_color":{"description":"`#RRGGBB`. Madar's own when the shop is not on the tier.","type":"string"},"card_image_url":{"type":["string","null"]},"custom_branding":{"description":"Whether the rest of this is the shop's or Madar's.\n\nThe page does not need it to render — the palette below is already\nresolved — but it decides how loudly Madar signs the footer.","type":"boolean"},"foreground_color":{"type":"string"},"logo_is_mark":{"description":"True when the logo is a shape on transparency and may be repainted for\ncontrast. See `orgs::branding::is_mark`.","type":"boolean"},"logo_url":{"type":["string","null"]},"name":{"description":"Always the shop's own name, at every tier. A page that does not say\nwhose it is helps nobody, and that was never the thing being sold.","type":"string"},"org_id":{"format":"uuid","type":"string"},"slug":{"description":"`None` when the shop has no address of its own — reached by `org_id`,\nwhich every page that already knows the shop uses.","type":["string","null"]}},"required":["org_id","name","custom_branding","logo_is_mark","background_color","foreground_color","accent_color"],"type":"object"},"PublicCartQuoteRequest":{"description":"`POST /public/branches/{id}/cart-quote` (online) and\n`POST /public/tables/{id}/cart-quote` (QR): the cart priced by the server\nexactly as the order will be, with the best deals applied automatically.","properties":{"channel":{"description":"Online only: the delivery sub-channel whose prices apply\n(`in_mall` | `outside` | `umbrella` | `pickup`); default `pickup`.","type":["string","null"]},"items":{"items":{"$ref":"#/components/schemas/OrderItemInput"},"type":"array"}},"required":["items"],"type":"object"},"PublicCombo":{"description":"The `combo` object of a public menu item.","properties":{"is_fixed":{"type":"boolean"},"slots":{"items":{"$ref":"#/components/schemas/PublicComboSlot"},"type":"array"}},"required":["is_fixed","slots"],"type":"object"},"PublicComboChoice":{"description":"One concrete item a public combo slot offers (categories expanded to the\ncategory's active items), with whether this channel and branch sell it now.","properties":{"available":{"description":"`false`: the item is not sold on this menu right now (switched off at\nthe branch or on the channel, or inactive). The page shows it greyed\nwith \"Unavailable\" and never lets it be picked (an order that picks it\nis refused `COMBO_ITEM_UNAVAILABLE`); it has no `sizes` and is never\nthe slot's default. Absent from an older server: available.","type":"boolean"},"base_price":{"description":"The included size's channel price (what the split weighs it by).","format":"int32","type":"integer"},"image_url":{"type":["string","null"]},"included_size_label":{"type":"string"},"menu_item_id":{"format":"uuid","type":"string"},"name":{"type":"string"},"name_translations":{"type":"object"},"sizes":{"items":{"$ref":"#/components/schemas/PublicComboSize"},"type":"array"},"surcharge":{"description":"The choice's own surcharge, per pick unit.","format":"int32","type":"integer"}},"required":["menu_item_id","name","name_translations","base_price","included_size_label","sizes","surcharge"],"type":"object"},"PublicComboSize":{"description":"A size of a public combo choice.","properties":{"extra":{"description":"What picking it adds inside the combo (the owner's surcharge, else the\ndifference over the included size, floored at 0; 0 for the included size).","format":"int32","type":"integer"},"label":{"type":"string"},"price":{"description":"Its normal channel price.","format":"int32","type":"integer"}},"required":["label","price","extra"],"type":"object"},"PublicComboSlot":{"properties":{"choices":{"items":{"$ref":"#/components/schemas/PublicComboChoice"},"type":"array"},"default_item_id":{"format":"uuid","type":["string","null"]},"default_size_label":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"max":{"format":"int32","type":"integer"},"min":{"format":"int32","type":"integer"},"name":{"type":"string"},"name_translations":{"type":"object"},"sort":{"format":"int32","type":"integer"}},"required":["id","name","name_translations","sort","min","max","choices"],"type":"object"},"PublicLinksBranch":{"properties":{"address":{"type":["string","null"]},"directions_url":{"description":"The shop's Maps link, else a search for the coordinates, else for the\naddress. `None` when the branch has none of the three.","type":["string","null"]},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"phone":{"type":["string","null"]}},"required":["id","name"],"type":"object"},"PublicLinksItem":{"description":"One button on the public page, already resolved.","properties":{"branch_names":{"description":"Order / book: the branches where it is on.","items":{"type":"string"},"type":"array"},"channels":{"description":"Order: the channels on anywhere — `pickup`, `delivery`, `in_mall`,\n`umbrella`.","items":{"type":"string"},"type":"array"},"href":{"description":"Absolute. For a module: the shop's own host when it has one, else the\ngeneric host. For a custom link: the shop's URL.","type":"string"},"kind":{"$ref":"#/components/schemas/LinksItemKind"},"path":{"description":"Modules only: the same place as a path on the shop's own host, for a\npage being read ON that host.","type":["string","null"]},"title_ar":{"type":["string","null"]},"title_en":{"description":"Custom links only (a module's title is the page's own words).","type":["string","null"]}},"required":["kind","href","branch_names","channels"],"type":"object"},"PublicLinksPage":{"description":"Everything the links page shows, in one request.","properties":{"branches":{"description":"Empty when \"Visit us\" is off.","items":{"$ref":"#/components/schemas/PublicLinksBranch"},"type":"array"},"brand":{"$ref":"#/components/schemas/PublicBrand"},"cover_image_url":{"description":"The card image, when the shop uses it as the cover (and is on the\nbranding tier — the loader already applied that).","type":["string","null"]},"items":{"description":"Visible, available buttons, in the shop's order.","items":{"$ref":"#/components/schemas/PublicLinksItem"},"type":"array"},"loyalty_mode":{"description":"`points` or `visits`, when the rewards button is shown.","type":["string","null"]},"socials":{"items":{"$ref":"#/components/schemas/PublicSocialLink"},"type":"array"},"tagline_ar":{"type":["string","null"]},"tagline_en":{"type":["string","null"]}},"required":["brand","items","socials","branches"],"type":"object"},"PublicReward":{"description":"A reward as the signup page lists it: what it is, and what it costs.","properties":{"cost_amount":{"format":"int32","type":"integer"},"cost_currency":{"type":"string"},"name":{"type":"string"}},"required":["name","cost_currency","cost_amount"],"type":"object"},"PublicSlot":{"properties":{"available":{"type":"boolean"},"starts_at":{"format":"date-time","type":"string"}},"required":["starts_at","available"],"type":"object"},"PublicSlots":{"properties":{"date":{"type":"string"},"slots":{"items":{"$ref":"#/components/schemas/PublicSlot"},"type":"array"},"timezone":{"type":"string"}},"required":["date","timezone","slots"],"type":"object"},"PublicSocialLink":{"description":"One place the shop can be found, as a page prints it.\n\nThe same three things the wallet passes render (`wallet::apple`,\n`wallet::google`), so the card in the phone and the card on the page list\nthe same links in the same order.","properties":{"key":{"description":"One of `orgs::social::PLATFORMS` — what the page picks its glyph by.","type":"string"},"label":{"description":"What a human calls it. The page falls back to this where it has no\nglyph for `key`, so a platform added on the server still renders.","type":"string"},"url":{"description":"`https://…` and nothing else — checked on write and again on read, see\n`orgs::social::links_of`.","type":"string"}},"required":["key","label","url"],"type":"object"},"PublicTable":{"description":"A table, as the page that opened from its code needs to know it.","properties":{"accepting":{"description":"The branch is not serving right now — no till is open. The page says so\ninstead of letting someone build a basket the kitchen will refuse.","type":"boolean"},"bill":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PublicTableBill","description":"The meal in progress, when there is one. `None` means the table is\nfree and this scan will start the bill."}]},"branch_id":{"format":"uuid","type":"string"},"branch_name":{"type":"string"},"label":{"description":"What the table is called in the room — \"7\", \"T7\", \"Terrace 2\".","type":"string"},"org_id":{"format":"uuid","type":"string"},"table_id":{"format":"uuid","type":"string"}},"required":["table_id","branch_id","org_id","label","branch_name","accepting"],"type":"object"},"PublicTableBill":{"description":"What the table has ordered so far.\n\nThe page draws this above the menu, so a customer who scans halfway through\na meal sees what is already on their bill rather than an empty basket that\nlooks like a fresh start. It is also how the second, third and fourth\nperson at the table see each other's rounds.\n\nPublic and unauthenticated, like everything else here. Whoever can read the\ncode stuck to the table can see what that table ordered — which is the\npeople sitting at it.","properties":{"opened_at":{"description":"When the party's bill was opened. The page counts up from this; a\nduration computed here would be wrong by the time it arrived.","format":"date-time","type":"string"},"ready":{"description":"The kitchen has finished everything fired so far.","type":"boolean"},"rounds":{"description":"Every round fired, oldest first, with what went to the kitchen in each.","items":{"$ref":"#/components/schemas/PublicTableRound"},"type":"array"},"subtotal":{"description":"Lines as charged, before discount — the bill's first line, not the bill.","format":"int32","type":"integer"},"ticket_id":{"format":"uuid","type":"string"},"total":{"description":"What the table will be asked to pay, as the SERVER prices it: the\ndiscount, the service charge and the tax are all in here, and none of\nthem is something this page should be recomputing.","format":"int32","type":"integer"}},"required":["ticket_id","opened_at","ready","subtotal","total","rounds"],"type":"object"},"PublicTableLine":{"properties":{"line_total":{"format":"int32","type":"integer"},"name":{"type":"string"},"quantity":{"format":"int32","type":"integer"},"voided":{"description":"Taken off the bill after it was ordered — struck through rather than\nhidden, so a customer who watches a plate go back sees it go.","type":"boolean"}},"required":["name","quantity","line_total","voided"],"type":"object"},"PublicTableRound":{"properties":{"fired_at":{"format":"date-time","type":"string"},"items":{"items":{"$ref":"#/components/schemas/PublicTableLine"},"type":"array"},"number":{"format":"int32","type":"integer"}},"required":["number","fired_at","items"],"type":"object"},"QuoteResponse":{"properties":{"distance_meters":{"format":"int32","type":["integer","null"]},"fee":{"format":"int32","type":["integer","null"]},"status":{"description":"\"ok\" | \"out_of_range\" | \"unavailable\"","type":"string"},"tax_policy":{"$ref":"#/components/schemas/OnlineTaxPolicy","description":"The tax the cart will be priced under at this branch. A quote is a fee\nquote — it has no cart, so no tax amount — but the page rendering the\ncheckout total needs the rate and the inclusivity beside the fee, or it\nshows the customer one number and intake records another. Present on\nevery outcome: the policy is the branch's, not the address's."},"zone_id":{"format":"uuid","type":["string","null"]},"zone_name":{"type":["string","null"]}},"required":["status","tax_policy"],"type":"object"},"QuotedCombo":{"properties":{"parts":{"items":{"$ref":"#/components/schemas/QuotedComboPart"},"type":"array"},"price":{"description":"P per combo unit.","format":"int32","type":"integer"},"saving_unit":{"description":"À la carte value of one combo minus `unit_total` (may be ≤ 0).","format":"int32","type":"integer"},"unit_total":{"description":"One combo with its surcharges and add-ons.","format":"int32","type":"integer"}},"required":["price","unit_total","saving_unit","parts"],"type":"object"},"QuotedComboPart":{"description":"One part of a quoted combo line.","properties":{"addons_total":{"description":"Its add-ons and optional fields, whole line.","format":"int32","type":"integer"},"combo_share":{"format":"int32","type":"integer"},"combo_surcharge":{"format":"int32","type":"integer"},"line_total":{"description":"`combo_share + combo_surcharge`.","format":"int32","type":"integer"},"menu_item_id":{"format":"uuid","type":"string"},"quantity":{"format":"int32","type":"integer"},"size_label":{"type":"string"},"slot_id":{"format":"uuid","type":"string"},"unit_price":{"description":"The item's normal price at this size.","format":"int32","type":"integer"}},"required":["slot_id","menu_item_id","size_label","quantity","unit_price","combo_share","combo_surcharge","line_total","addons_total"],"type":"object"},"QuotedDeal":{"description":"A deal the server applied to the cart.","properties":{"deal_rule_id":{"format":"uuid","type":"string"},"discount":{"format":"int32","type":"integer"},"lines":{"items":{"$ref":"#/components/schemas/DealLineInput"},"type":"array"},"name":{"type":"string"},"name_translations":{"type":"object"},"times":{"format":"int32","type":"integer"}},"required":["deal_rule_id","name","name_translations","times","discount","lines"],"type":"object"},"QuotedLine":{"properties":{"combo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/QuotedCombo"}]},"deal_minor":{"description":"What the applied deals took off this line.","format":"int32","type":"integer"},"index":{"description":"Index into the request's `items[]`.","format":"int32","type":"integer"},"line_total":{"description":"The whole line with its add-ons, before deals.","format":"int32","type":"integer"},"quantity":{"format":"int32","type":"integer"},"unit_price":{"description":"The size price per unit (a combo: 0; see `combo`).","format":"int32","type":"integer"}},"required":["index","quantity","unit_price","line_total","deal_minor"],"type":"object"},"ReplaceIdentityRequest":{"properties":{"device_token":{"description":"Proof of the CURRENT phone.","type":"string"},"name":{"description":"Also correct the name.","type":["string","null"]},"new_phone":{"type":"string"},"new_phone_device_token":{"description":"Proof of the NEW phone (the client runs `/public/otp/request|verify` on it).","type":"string"}},"required":["device_token","new_phone","new_phone_device_token"],"type":"object"},"ReplaceIdentityResponse":{"properties":{"combined":{"description":"True when two customers were combined into this one.","type":"boolean"},"customer_id":{"format":"uuid","type":"string"},"first_name":{"type":"string"},"phone_hint":{"description":"The new number, masked. The client already holds its device token.","type":"string"}},"required":["customer_id","phone_hint","first_name","combined"],"type":"object"},"SaleWindow":{"description":"An availability window. A combo or deal with no window is always\navailable; with windows, it is available while any window that applies to\nthe branch (its own, or an all-branch one) is open.","properties":{"branch_id":{"description":"`null` = every branch.","format":"uuid","type":["string","null"]},"ends_at":{"type":["string","null"]},"id":{"description":"Ignored on write (windows are replaced as a set).","format":"uuid","type":["string","null"]},"starts_at":{"description":"\"HH:MM\"; with `ends_at`, or neither (the whole day). `ends_at` before\n`starts_at` crosses midnight: the part after midnight belongs to the day\nthe window started (its weekday and date range).","type":["string","null"]},"valid_from":{"description":"Optional date range, inclusive, judged on the day the window started.","format":"date","type":["string","null"]},"valid_to":{"format":"date","type":["string","null"]},"weekdays":{"description":"bit0 = Sunday … bit6 = Saturday; 127 = every day (the default).","format":"int32","type":"integer"}},"type":"object"},"StaffDrinkLine":{"description":"`staff_drink` on an order line. Additive: a client that never heard of it\nomits it and the line is an ordinary paid line.","properties":{"comp_minor":{"description":"What the TILL comped on this line (whole line, minor units). Read ONLY\nwhen a queued offline sale is replayed; live, the server prices the comp\nand this is ignored.","format":"int32","type":["integer","null"]},"id":{"description":"Client-minted; the idempotency key AND the `staff_drinks` row's id. A\nrow an older flow already recorded under this id is reused and the\norder attached to it — never a second drink off the allowance.","format":"uuid","type":"string"},"note":{"description":"REQUIRED. Who the drink is for and why, in the teller's own words.","type":"string"},"overspent":{"description":"Whether the till believed this drink went past the allowance. Replay\nonly, and only to tell a convergence from a surprise.","type":["boolean","null"]}},"required":["id","note"],"type":"object"},"TableOrderRequest":{"description":"One scan's worth of order.","properties":{"customer_name":{"description":"Who is at the table, if they offered a name. Shown on the bill so the\nwaiter can find them.","type":["string","null"]},"customer_phone":{"description":"Their phone, if they offered one. Optional and never required: with a\nvalid number (and a name) the bill is linked to that customer — created\non first contact, `source = table_qr` — so the visit counts toward them.\nA number that is not valid is ignored; the order is never refused.","type":["string","null"]},"idempotency_key":{"description":"Client-minted, so a phone that resends on a flaky connection does not\norder twice. This is the ONLY protection against a double-send, because\na customer's browser has no outbox to dedup against.","format":"uuid","type":["string","null"]},"items":{"description":"What they want. Named, never priced — see the module docs.","items":{"$ref":"#/components/schemas/OrderItemInput"},"type":"array"},"table_id":{"format":"uuid","type":"string"}},"required":["table_id","items"],"type":"object"},"TicketBill":{"description":"What the party owes, priced where the books are priced.\n\nThe till used to show the ticket's running `subtotal` and collect that,\nwhile the settle booked subtotal − discount + service charge + tax. Every\nsuch drawer was short by the tax on every table sale, and the drift check\nat settle now refuses that figure outright — so the till must be shown the\nright one, and only the server knows the branch's policy. Priced under the\nsame engine and the same resolved policy as `create_order_inner`, with the\nservice charge on (a ticket is dine-in by definition, ruling 2). For a\nsettled ticket the figures are the ORDER's, as booked, not a repricing\nunder today's policy.","properties":{"discount_amount":{"description":"The waiter's discount, resolved (a `discount_id` is looked up the way\nthe settle looks it up). A cashier who clears it at settle will see a\ndifferent total than this one, and that is the point of showing it.","format":"int32","type":"integer"},"service_charge_amount":{"format":"int32","type":"integer"},"service_charge_rate":{"format":"double","type":"number"},"service_charge_taxable":{"description":"Whether the service charge sits inside the tax base. Frozen on the bill\nwith the rates, so a till re-pricing the bill (a discount, a reward, a\nvoided line) prices it the way the settle will. Additive: a bill from\nan older server decodes as `true`, `TaxPolicy::default()`'s value.","type":"boolean"},"subtotal":{"description":"Live lines as charged, before discount. Gross when tax-inclusive.","format":"int32","type":"integer"},"tax_amount":{"description":"Inside the total when `tax_inclusive`, on top of it otherwise.","format":"int32","type":"integer"},"tax_inclusive":{"type":"boolean"},"tax_rate":{"description":"The rates the figures were computed under, for the printed bill.","format":"double","type":"number"},"total":{"description":"What the drawer must collect.","format":"int32","type":"integer"}},"required":["subtotal","discount_amount","service_charge_amount","tax_amount","tax_inclusive","total","tax_rate","service_charge_rate"],"type":"object"}}},"info":{"contact":{"email":"shawket.4@icloud.com","name":"Madar POS","url":"https://get.madar-pos.cloud/"},"description":"The endpoints a café's own pages use without an account: its brand and links page, branches and menus, delivery and table ordering, order tracking, bookings and the rewards card. Every call names the shop (`slug` or an id); there is no cross-shop listing. Madar POS is a point of sale for cafés and restaurants in Egypt: https://get.madar-pos.cloud/","license":{"identifier":"LicenseRef-Madar-Proprietary","name":"Proprietary"},"title":"Madar POS public API","version":"0.1.0"},"openapi":"3.1.0","paths":{"/public/booking-branches":{"get":{"operationId":"booking_branches","parameters":[{"in":"query","name":"org_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PublicBookingBranch"},"type":"array"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/bookings":{"post":{"operationId":"create_public_booking","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingView"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/bookings/{token}":{"get":{"operationId":"get_public_booking","parameters":[{"description":"Manage token from the confirmation link","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingView"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]},"patch":{"operationId":"update_public_booking","parameters":[{"description":"Manage token","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingChange"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingView"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/bookings/{token}/cancel":{"post":{"operationId":"cancel_public_booking","parameters":[{"description":"Manage token","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingView"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/branches":{"get":{"operationId":"public_branches","parameters":[{"in":"query","name":"org_id","required":true,"schema":{"format":"uuid","type":"string"}},{"description":"The read-only menu (`/menu`): every active branch, not only the ones\ntaking online orders — a shop with ordering switched off still has a\nmenu to show. Each branch's channel flags stay as they are, so a client\nnever offers an order where none is taken.","in":"query","name":"browse","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PublicBranch"},"type":"array"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/branches/{id}/booking-info":{"get":{"operationId":"booking_info","parameters":[{"description":"Branch ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBookingInfo"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/branches/{id}/booking-slots":{"get":{"operationId":"booking_slots","parameters":[{"description":"Branch ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"date","required":true,"schema":{"type":"string"}},{"in":"query","name":"party_size","required":true,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSlots"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["bookings-public"]}},"/public/branches/{id}/cart-quote":{"post":{"operationId":"public_branch_cart_quote","parameters":[{"description":"Branch ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCartQuoteRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartQuote"}}},"description":"The cart as the order will be priced"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"Price an online (storefront) cart at a branch, deals applied.","tags":["delivery"]}},"/public/branches/{id}/delivery-quote":{"get":{"operationId":"delivery_quote","parameters":[{"in":"query","name":"lat","required":true,"schema":{"format":"double","type":"number"}},{"in":"query","name":"lng","required":true,"schema":{"format":"double","type":"number"}},{"in":"query","name":"channel","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/branches/{id}/menu":{"get":{"operationId":"public_menu","parameters":[{"description":"A delivery channel, or `dine_in` — the dine-in menu (branch prices, no\nchannel discount), accepted ONLY with `preview=true`: the read-only\nmenu of a shop that takes no online orders. Nothing can be ordered\nagainst it; quote and intake know no such channel.","in":"query","name":"channel","required":true,"schema":{"type":"string"}},{"description":"Read-only browse preview. When `true`, the menu is returned even if the\nchannel is closed right now, so customers can browse while a branch is\nclosed. This NEVER relaxes the channel-*enabled* check, and the\ndelivery-quote / order-intake endpoints stay gated on open-now — so a\npreview can never become a real order against a closed channel.","in":"query","name":"preview","required":false,"schema":{"type":["boolean","null"]}},{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryMenu"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/delivery-orders":{"post":{"operationId":"create_delivery_order","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryOrderInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryOrder"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/delivery-orders/history":{"get":{"operationId":"guest_order_history","parameters":[{"in":"query","name":"phone","required":true,"schema":{"type":"string"}},{"in":"query","name":"org_id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"device_token","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/OrderHistorySummary"},"type":"array"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/delivery-orders/past-locations":{"get":{"operationId":"guest_past_locations","parameters":[{"in":"query","name":"phone","required":true,"schema":{"type":"string"}},{"in":"query","name":"org_id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"branch_id","required":false,"schema":{"format":"uuid","type":["string","null"]}},{"in":"query","name":"device_token","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GuestSavedLocation"},"type":"array"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/delivery-orders/{id}/track":{"get":{"operationId":"track_delivery_order","parameters":[{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryTracking"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/loyalty/card/{token}":{"get":{"operationId":"loyalty_card","parameters":[{"description":"Member token from the pass barcode","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardView"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["loyalty-public"]}},"/public/loyalty/card/{token}/orders":{"get":{"description":"Authenticated by the token in the URL — the same one their pass carries and\nthe till scans — because that is the only credential a loyalty member has.\nWhich means anyone holding the link can read it, and that is worth stating\nrather than glossing: a forwarded card link forwards the history with it.\nThe shop decides whether to run a programme on those terms, and the privacy\npolicy says so plainly.\n\nVoided orders are excluded. A sale that was reversed is not something the\ncustomer bought, and showing it invites a question the page cannot answer.","operationId":"loyalty_card_orders","parameters":[{"description":"Member token from the pass barcode","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PastOrders"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The member's own purchase history.","tags":["loyalty-public"]}},"/public/loyalty/card/{token}/preferences":{"post":{"operationId":"set_loyalty_card_preferences","parameters":[{"description":"Member token from the pass barcode","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardPreferences"}}},"required":true},"responses":{"204":{"description":"Saved"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["loyalty-public"]}},"/public/loyalty/card/{token}/qr.png":{"get":{"description":"Rendered server-side with the same renderer the printed cards use, rather\nthan shipping a QR library to the browser — and rendered from the token\nDIRECTLY, never through a Shlink short link: a short URL is a public\nredirect, and the member token is the one value here that has to stay\nbetween the customer and the till.\n\nThis is the fallback that makes the program usable before either wallet is\nconfigured — and the answer for a customer whose phone has no wallet app.","operationId":"loyalty_card_qr","parameters":[{"description":"Member token","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Member QR as a PNG"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The member's QR as a PNG.","tags":["loyalty-public"]}},"/public/loyalty/join":{"post":{"operationId":"loyalty_join","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinResult"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["loyalty-public"]}},"/public/loyalty/join-info":{"get":{"operationId":"loyalty_join_info","parameters":[{"description":"The counter QR of one branch. Its settings and its catalogue apply.","in":"query","name":"branch_id","required":false,"schema":{"format":"uuid","type":"string"}},{"description":"The organisation's own code, for a shop that wants ONE card to hand out\n— a poster, a receipt footer, a link in a bio. The programme's org-level\nsettings apply, which is also what the wallet pass has always used.","in":"query","name":"org_id","required":false,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinInfo"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["loyalty-public"]}},"/public/loyalty/pass/{token}/apple.pkpass":{"get":{"description":"503s until Apple credentials are configured — see\n`wallet::apple::sign_manifest`. The button that leads here is only rendered\nwhen `apple::is_configured()`, so a customer does not meet this by accident.","operationId":"loyalty_apple_pass","parameters":[{"description":"Member token","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Apple Wallet pass"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"Download the signed `.pkpass`.","tags":["loyalty-public"]}},"/public/order-now/{token}":{"get":{"operationId":"order_now_context","parameters":[{"description":"Member token (the card's QR)","in":"path","name":"token","required":true,"schema":{"type":"string"}},{"description":"From `/public/otp/verify`, for the customer's current phone. Absent or\nnot valid for that phone → the masked context.","in":"query","name":"device_token","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderNowContext"}}},"description":"Masked without a valid device token for the customer's current phone; full with one"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["order-now"]}},"/public/order-now/{token}/combine":{"post":{"operationId":"order_now_combine","parameters":[{"description":"Member token — this customer survives","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceIdentityRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceIdentityResponse"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["order-now"]}},"/public/order-now/{token}/replace-identity":{"post":{"operationId":"order_now_replace_identity","parameters":[{"description":"Member token","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceIdentityRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceIdentityResponse"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"429":{"description":"`IDENTITY_REPLACE_LIMIT`"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["order-now"]}},"/public/orgs/brand":{"get":{"description":"Public and unauthenticated by necessity: it is the first request a customer's\nbrowser makes, before there is any notion of a session. Nothing here is\nprivate — a name, a logo and three colours are on the shopfront.","operationId":"public_org_brand","parameters":[{"description":"The shop, when the page already knows which one it is.","in":"query","name":"org_id","required":false,"schema":{"format":"uuid","type":"string"}},{"description":"The first label of the hostname, when it does not — `rue` for\n`rue.madar-pos.cloud`.","in":"query","name":"slug","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBrand"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The shop behind a guest page.","tags":["orgs"]}},"/public/orgs/favicon":{"get":{"description":"Square, opaque, and on the shop's own ground — the same treatment the\nwallet badge gets, and for the same reason: a browser tab and an iOS home\nscreen both draw this against a background they choose, so a mark on\ntransparency is a coin flip and a wide wordmark cropped to a square loses\nthe shop's name. [`crate::orgs::branding::on_ground`] fits the artwork\nwhole and centres it, which is why a wordmark reads as a band rather than\nas two letters.\n\nThe inset is wider than the wallet's. Nothing masks a favicon to a circle,\nso there is no reason to leave the corners empty.\n\nA shop with no logo gets a 404, and the page falls back to whatever icon it\nshipped with — Madar's. That is the honest answer: this endpoint serves a\nshop's logo, and there isn't one.","operationId":"public_org_favicon","parameters":[{"in":"query","name":"org_id","required":false,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"slug","required":false,"schema":{"type":"string"}},{"description":"Rounded up to 32, 180 or 512. Defaults to 180 — big enough for a home\nscreen, and a browser downsamples for the tab perfectly well.","in":"query","name":"size","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"image/png":{}},"description":"Square PNG"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The shop's own logo, as a favicon.","tags":["orgs"]}},"/public/orgs/links":{"get":{"description":"Public for the same reason `/public/orgs/brand` is — it is the first thing\na customer's browser asks — and it answers \"no shop\" identically for a shop\nthat does not exist and one that is switched off, for the same reason.","operationId":"public_org_links","parameters":[{"description":"The shop, when the page already knows which one it is.","in":"query","name":"org_id","required":false,"schema":{"format":"uuid","type":"string"}},{"description":"The first label of the hostname, when it does not — `rue` for\n`rue.madar-pos.cloud`.","in":"query","name":"slug","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicLinksPage"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The shop's links page, in one request.","tags":["orgs"]}},"/public/otp/request":{"post":{"operationId":"otp_request","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequestInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequestResponse"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/otp/verify":{"post":{"operationId":"otp_verify","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpVerifyInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpVerifyResponse"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["delivery-public"]}},"/public/table-orders":{"post":{"description":"Opens the bill if the table has none, adds a round if it does. Both answer\nwith the bill as it now stands, so the page can show what the table has\nordered so far — including the rounds somebody else at the table sent.","operationId":"public_table_order","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableOrderRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenTicketView"}}},"description":"The bill as it now stands"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"Send this table's order to the kitchen.","tags":["open_tickets"]}},"/public/tables/{id}":{"get":{"operationId":"public_table","parameters":[{"description":"Table ID, from the QR","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTable"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"tags":["open_tickets"]}},"/public/tables/{id}/cart-quote":{"post":{"operationId":"public_table_cart_quote","parameters":[{"description":"Table ID, from the QR","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCartQuoteRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartQuote"}}},"description":"The cart as the order will be priced"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"Price a QR table cart, deals applied.","tags":["open_tickets"]}},"/public/tables/{id}/menu":{"get":{"description":"The DINE-IN menu — branch prices, the whole catalogue, no channel discount\n— because a table's order settles as a dine-in bill. Quoting a customer the\nin-mall delivery menu and then charging them the till's prices is the same\nclass of mistake as letting the till price its own sales, and it would be\ninvisible until someone compared a receipt to a phone.","operationId":"public_table_menu","parameters":[{"description":"Table ID, from the QR","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryMenu"}}},"description":""},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Bad request — validation failed or malformed input"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Unauthorized — missing or invalid bearer token"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Forbidden — insufficient permission or wrong org"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Conflict — FK or domain invariant violation"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}},"description":"Internal server error"}},"summary":"The menu at this table.","tags":["open_tickets"]}}},"servers":[{"description":"Production","url":"https://api.madar-pos.cloud"}],"tags":[{"description":"Organization CRUD. Super-admin only.","name":"orgs"},{"description":"Waiter fire-now-pay-later open tickets: fire, add rounds, settle into a paid dine-in order.","name":"open_tickets"},{"description":"Guest self-service booking: branch info, open slots, book with WhatsApp OTP, manage link.","name":"bookings-public"},{"description":"Public: ordering from the wallet pass. The member token identifies; a device token for the customer's phone authorises.","name":"order-now"},{"description":"Delivery config (settings, zones, org defaults), the staff queue, status transitions, finalize, and cancel/waste.","name":"delivery"},{"description":"Unauthenticated, rate-limited customer endpoints: branch selector, channel menu, OSRM quote, WhatsApp OTP, order intake.","name":"delivery-public"}]}