bober-bbq-bot/docs/openapi.yaml
byrsapty ac3716712b Update API docs for guest ordering (require_customer_auth, /checkout/track)
docs/API.md and docs/openapi.yaml still described every cart/orders/
checkout/me route as requiring @require_telegram_auth with a possible 401
- no longer accurate now that those routes use @require_customer_auth
(falls back to a cookie-bound guest instead of rejecting the request, see
the guest-ordering feature). Documents the new decorator, the guest
identity scheme (GuestIdSequence/allocate_guest_telegram_id), the new
unauthenticated GET /checkout/track/<token> endpoint, and the checkout
response's new card_unavailable_message/track_url fields. Also adds the
previously-undocumented /me/locale path to openapi.yaml and updates
README's architecture notes accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:47:16 +03:00

755 lines
24 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

openapi: 3.0.3
info:
title: Bober BBQ Mini App API
description: >
Backend API for the Bober BBQ Telegram Mini App, served under /api by the
Flask blueprint `api_bp` (bober_bbq/api/__init__.py). See docs/API.md for
the full narrative reference, including caveats and edge-case notes —
this file covers the same routes in machine-readable form but is
secondary to that document.
version: "1.0.0"
servers:
- url: /api
tags:
- name: menu
- name: settings
- name: cart
- name: orders
- name: checkout
- name: payments
- name: me
components:
securitySchemes:
TelegramInitData:
type: apiKey
in: header
name: X-Telegram-Init-Data
description: >
Raw Telegram Mini App initData string, validated per Telegram's WebApp
initData algorithm (HMAC-SHA256 against the bot token, <=24h old).
In Flask debug mode only, a `?dev_user_id=<int>` query param bypasses
this when validation fails.
GuestSession:
type: apiKey
in: cookie
name: session
description: >
Guest fallback used by every route below whose security lists both
schemes (see require_customer_auth in docs/API.md's Authentication
section): if TelegramInitData is missing or fails validation, the
request is NOT rejected — it's served on behalf of a cookie-bound
guest TelegramUser instead (created transparently on first contact).
As a result, none of those routes ever return 401.
parameters:
DevUserId:
name: dev_user_id
in: query
required: false
schema:
type: integer
description: >
Dev-mode-only auth bypass. Only honored when the failed initData
check occurs AND the Flask app is running with debug=True.
responses:
Unauthorized:
description: Missing or invalid Telegram initData
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: invalid or missing Telegram init data
schemas:
Category:
type: object
properties:
id: { type: integer }
name: { type: string }
slug: { type: string }
sort_order: { type: integer }
Allergen:
type: object
properties:
code: { type: string }
label: { type: string }
emoji: { type: string }
Product:
type: object
properties:
id: { type: integer }
category_id: { type: integer }
name: { type: string }
description: { type: string }
weight: { type: string, example: "300 г" }
price: { type: integer, description: "Whole UAH" }
image_url: { type: string }
allergens:
type: array
items: { $ref: '#/components/schemas/Allergen' }
CartItem:
type: object
properties:
product_id: { type: integer }
name: { type: string }
weight: { type: string }
image_url: { type: string }
price: { type: integer }
quantity: { type: integer }
line_total: { type: integer }
Cart:
type: object
properties:
items:
type: array
items: { $ref: '#/components/schemas/CartItem' }
items_total: { type: integer }
count: { type: integer }
Theme:
type: object
properties:
accent: { type: string }
accent_2: { type: string }
accent_grad: { type: string }
accent_ink: { type: string }
PublicSettings:
type: object
properties:
cafe_name: { type: string }
logo_url: { type: string }
phone: { type: string, nullable: true }
address: { type: string, nullable: true }
work_hours_from: { type: string, example: "11:00" }
work_hours_to: { type: string, example: "21:00" }
is_open: { type: boolean }
instagram_url: { type: string, nullable: true }
maps_url: { type: string, nullable: true }
delivery_fee: { type: integer }
free_delivery_threshold: { type: integer }
min_delivery_order_amount: { type: integer }
pickup_discount_percent: { type: integer }
force_closed: { type: boolean }
force_closed_message: { type: string }
announcement_enabled: { type: boolean }
announcement_message: { type: string }
theme: { $ref: '#/components/schemas/Theme' }
has_active_promos: { type: boolean }
OrderItemLine:
type: object
properties:
name: { type: string }
quantity: { type: integer }
price: { type: integer }
Order:
type: object
properties:
id: { type: integer }
number: { type: string, description: '"№1042" or "#42" fallback' }
order_type: { type: string, enum: [delivery, pickup, dine_in] }
status:
type: string
enum: [new, confirmed, cooking, ready, courier, completed, cancelled]
status_label: { type: string }
payment_method: { type: string, enum: [cash, card], nullable: true }
payment_status: { type: string, enum: [unpaid, paid] }
items_total: { type: integer }
delivery_fee: { type: integer }
discount_amount: { type: integer }
promo_code: { type: string, nullable: true }
promo_discount_amount: { type: integer }
total: { type: integer }
created_at: { type: string, format: date-time, nullable: true }
pickup_time: { type: string, nullable: true }
address_street: { type: string, nullable: true }
address_house: { type: string, nullable: true }
table_number: { type: string, nullable: true }
can_cancel: { type: boolean }
can_pay: { type: boolean }
items:
type: array
items: { $ref: '#/components/schemas/OrderItemLine' }
ValidationErrorMissing:
type: object
properties:
error: { type: string, example: validation }
missing:
type: array
items: { type: string }
ValidationErrorMessage:
type: object
properties:
error: { type: string, example: validation }
message: { type: string }
SimpleError:
type: object
properties:
error: { type: string }
message: { type: string }
NotFoundError:
type: object
properties:
error: { type: string, example: not_found }
CheckoutResult:
type: object
properties:
order_id: { type: integer }
order_number: { type: string }
total: { type: integer }
payment_method: { type: string, enum: [cash, card] }
pay_url: { type: string, nullable: true }
card_unavailable_message:
type: string
nullable: true
description: >
Non-null only when payment_method is "card" and the active
payment provider was unreachable/unconfigured — the same
translated text a real Telegram customer gets via a bot
message, surfaced here too so a guest (who has no Telegram
chat to message) still sees it.
track_url:
type: string
nullable: true
description: >
Same-origin SPA path, e.g. "/webapp/?track=<token>" — not an
absolute URL. Lets anyone with the link check order status via
GET /checkout/track/{token} even with no session cookie.
DeliveryCheckoutBody:
type: object
required: [name, phone, address_city, address_street, address_house, payment_method]
properties:
name: { type: string, minLength: 2 }
phone: { type: string }
address_city: { type: string }
address_street: { type: string }
address_house: { type: string }
address_apartment: { type: string, nullable: true }
address_comment: { type: string, nullable: true }
payment_method: { type: string, enum: [cash, card] }
cash_change_for: { type: integer, nullable: true }
promo_code: { type: string, nullable: true }
PickupCheckoutBody:
type: object
required: [name, phone, pickup_time, payment_method]
properties:
name: { type: string, minLength: 2 }
phone: { type: string }
pickup_time: { type: string, pattern: '^([01]?\d|2[0-3]):[0-5]\d$', example: "18:30" }
payment_method: { type: string, enum: [cash, card] }
promo_code: { type: string, nullable: true }
DineInCheckoutBody:
type: object
required: [name, phone, payment_method]
properties:
name: { type: string, minLength: 2 }
phone: { type: string }
table_number: { type: string, nullable: true }
payment_method: { type: string, enum: [cash, card] }
promo_code: { type: string, nullable: true }
paths:
/categories:
get:
tags: [menu]
summary: List active menu categories
security: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Category' }
/products:
get:
tags: [menu]
summary: List active products
security: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Product' }
/settings:
get:
tags: [settings]
summary: Public cafe configuration
security: []
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/PublicSettings' }
/cart:
get:
tags: [cart]
summary: Get the caller's cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Cart' }
delete:
tags: [cart]
summary: Clear the caller's cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK (empty cart)
content:
application/json:
schema: { $ref: '#/components/schemas/Cart' }
/cart/items:
post:
tags: [cart]
summary: Add a product to the cart (or increment its quantity)
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
product_id: { type: integer }
quantity: { type: integer, default: 1, minimum: 1 }
required: [product_id]
responses:
'201':
description: Created / updated
content:
application/json:
schema: { $ref: '#/components/schemas/Cart' }
'400':
description: quantity < 1
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
'404':
description: product not found or inactive
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
/cart/items/{product_id}:
parameters:
- name: product_id
in: path
required: true
schema: { type: integer }
patch:
tags: [cart]
summary: Set a cart line's quantity (deletes the line if <= 0)
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
quantity: { type: integer, default: 1 }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Cart' }
'404':
description: item not in cart
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
delete:
tags: [cart]
summary: Remove one line item (no-op if absent)
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Cart' }
/orders:
get:
tags: [orders]
summary: List the caller's most recent orders (max 50)
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Order' }
/orders/{order_id}/cancel:
parameters:
- name: order_id
in: path
required: true
schema: { type: integer }
post:
tags: [orders]
summary: Cancel an order the caller owns (only while new/confirmed)
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'400':
description: not cancellable in current status
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
'404':
description: not found (or not the caller's order)
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }
/orders/{order_id}/repeat:
parameters:
- name: order_id
in: path
required: true
schema: { type: integer }
post:
tags: [orders]
summary: Re-add a past order's items to the current cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
cart: { $ref: '#/components/schemas/Cart' }
skipped:
type: array
items: { type: string }
description: Product names skipped because no longer active/existing
'404':
description: not found (or not the caller's order)
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }
/orders/{order_id}/pay:
parameters:
- name: order_id
in: path
required: true
schema: { type: integer }
post:
tags: [orders]
summary: (Re-)generate a Monobank payment link for a card order
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
pay_url: { type: string, nullable: true }
'400':
description: not payable / already paid / Monobank not configured
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
'404':
description: not found (or not the caller's order)
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }
'502':
description: Monobank API call failed
content:
application/json:
schema: { $ref: '#/components/schemas/SimpleError' }
/checkout/delivery:
post:
tags: [checkout]
summary: Place a delivery order from the current cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DeliveryCheckoutBody' }
responses:
'201':
description: Order created
content:
application/json:
schema: { $ref: '#/components/schemas/CheckoutResult' }
'400':
description: >
cart_empty | closed | min_order_amount | promo_invalid |
validation (missing fields, invalid payment_method,
cash_change_for not a number, name/phone invalid)
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ValidationErrorMissing'
- $ref: '#/components/schemas/ValidationErrorMessage'
- $ref: '#/components/schemas/SimpleError'
/checkout/pickup:
post:
tags: [checkout]
summary: Place a pickup order from the current cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PickupCheckoutBody' }
responses:
'201':
description: Order created
content:
application/json:
schema: { $ref: '#/components/schemas/CheckoutResult' }
'400':
description: >
cart_empty | closed | promo_invalid | validation (missing
fields, invalid payment_method, name/phone invalid, bad
pickup_time format)
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ValidationErrorMissing'
- $ref: '#/components/schemas/ValidationErrorMessage'
- $ref: '#/components/schemas/SimpleError'
/checkout/dine-in:
post:
tags: [checkout]
summary: Place a dine-in order from the current cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DineInCheckoutBody' }
responses:
'201':
description: Order created
content:
application/json:
schema: { $ref: '#/components/schemas/CheckoutResult' }
'400':
description: >
cart_empty | closed | promo_invalid | validation (missing
fields, invalid payment_method, name/phone invalid)
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ValidationErrorMissing'
- $ref: '#/components/schemas/ValidationErrorMessage'
- $ref: '#/components/schemas/SimpleError'
/checkout/promo/validate:
post:
tags: [checkout]
summary: Preview-validate a promo code against the current cart
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code]
properties:
code: { type: string }
responses:
'200':
description: Valid promo code
content:
application/json:
schema:
type: object
properties:
valid: { type: boolean, example: true }
code: { type: string }
discount_percent: { type: integer }
discount_amount: { type: integer }
'400':
description: Invalid/inapplicable promo code (also used for blank code)
content:
application/json:
schema:
type: object
properties:
valid: { type: boolean, example: false }
message: { type: string }
/checkout/prefill:
get:
tags: [checkout]
summary: Most recent name/phone and delivery address for the caller
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
has_orders: { type: boolean }
name: { type: string, nullable: true }
phone: { type: string, nullable: true }
delivery:
type: object
nullable: true
properties:
address_city: { type: string, nullable: true }
address_street: { type: string, nullable: true }
address_house: { type: string, nullable: true }
address_apartment: { type: string, nullable: true }
/checkout/payment-status/{order_id}:
parameters:
- name: order_id
in: path
required: true
schema: { type: integer }
get:
tags: [checkout]
summary: Poll whether a card order has been paid
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
responses:
'200':
description: >
{"paid": true} once confirmed paid, otherwise {"paid": false}
(including transient Monobank check failures — indistinguishable
from genuinely unpaid).
content:
application/json:
schema:
type: object
properties:
paid: { type: boolean }
'404':
description: not found (or not the caller's order)
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }
/checkout/track/{token}:
parameters:
- name: token
in: path
required: true
schema: { type: string }
get:
tags: [checkout]
summary: Look up an order's status by its tracking token (no auth)
description: >
The only checkout_bp route with no security requirement at all —
Order.tracking_token itself (a secrets.token_urlsafe(16) value
generated for every order, guest or Telegram) is the sole
credential, same trust level as a payment provider's emailed
receipt link. Not scoped to any TelegramUser.
security: []
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'404':
description: no order with that tracking_token
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }
/me/locale:
patch:
tags: [me]
summary: Persist the caller's UA/EN language choice
security: [{ TelegramInitData: [] }, { GuestSession: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
locale: { type: string, example: uk }
required: [locale]
responses:
'200':
description: >
OK — returns the normalized locale actually stored (invalid
codes silently fall back to "uk" rather than erroring).
content:
application/json:
schema:
type: object
properties:
locale: { type: string, enum: [uk, en] }
/payments/monobank/webhook:
post:
tags: [payments]
summary: Monobank server-to-server payment status webhook
security: []
description: >
Authenticated via the X-Sign header (ECDSA/SHA256 signature verified
against Monobank's published public key), not Telegram initData.
parameters:
- name: X-Sign
in: header
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
invoiceId: { type: string }
status: { type: string }
responses:
'200':
description: Acknowledged (processed, or invoiceId unknown)
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean, example: true }
'400':
description: invalid signature or missing invoiceId
content:
application/json:
schema: { $ref: '#/components/schemas/NotFoundError' }