Documents every route in bober_bbq/api/* (menu, settings, cart, orders, checkout, payments) sourced by reading each handler and its downstream model/service/util calls in full: auth via require_telegram_auth and the Telegram initData HMAC validation (including the debug-only dev_user_id bypass), exact request/response shapes, and every distinct error status and body. Adds docs/API.md as the primary narrative reference and docs/openapi.yaml as a machine-readable OpenAPI 3.0 spec covering the same surface.
688 lines
22 KiB
YAML
688 lines
22 KiB
YAML
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
|
||
|
||
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.
|
||
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 }
|
||
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: [] }]
|
||
responses:
|
||
'200':
|
||
description: OK
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/Cart' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
delete:
|
||
tags: [cart]
|
||
summary: Clear the caller's cart
|
||
security: [{ TelegramInitData: [] }]
|
||
responses:
|
||
'200':
|
||
description: OK (empty cart)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/Cart' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/cart/items:
|
||
post:
|
||
tags: [cart]
|
||
summary: Add a product to the cart (or increment its quantity)
|
||
security: [{ TelegramInitData: [] }]
|
||
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' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'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: [] }]
|
||
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' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'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: [] }]
|
||
responses:
|
||
'200':
|
||
description: OK
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/Cart' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/orders:
|
||
get:
|
||
tags: [orders]
|
||
summary: List the caller's most recent orders (max 50)
|
||
security: [{ TelegramInitData: [] }]
|
||
responses:
|
||
'200':
|
||
description: OK
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items: { $ref: '#/components/schemas/Order' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/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: [] }]
|
||
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' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'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: [] }]
|
||
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
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'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: [] }]
|
||
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' }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'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: [] }]
|
||
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'
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/checkout/pickup:
|
||
post:
|
||
tags: [checkout]
|
||
summary: Place a pickup order from the current cart
|
||
security: [{ TelegramInitData: [] }]
|
||
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'
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/checkout/dine-in:
|
||
post:
|
||
tags: [checkout]
|
||
summary: Place a dine-in order from the current cart
|
||
security: [{ TelegramInitData: [] }]
|
||
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'
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/checkout/promo/validate:
|
||
post:
|
||
tags: [checkout]
|
||
summary: Preview-validate a promo code against the current cart
|
||
security: [{ TelegramInitData: [] }]
|
||
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 }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/checkout/prefill:
|
||
get:
|
||
tags: [checkout]
|
||
summary: Most recent name/phone and delivery address for the caller
|
||
security: [{ TelegramInitData: [] }]
|
||
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 }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
|
||
/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: [] }]
|
||
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 }
|
||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||
'404':
|
||
description: not found (or not the caller's order)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/NotFoundError' }
|
||
|
||
/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' }
|