Continuation of the previous commit (gdrive_backup.py removal landed
separately by accident) — adds the new module itself, the admin
routes/template using it, generalized Setting keys, updated docs/
provisioning notes, and tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the discussion on distributing this as an installable package for
new clients: .deb was considered and rejected (per-client interactive
config — domain, bot token, TLS, a frontend build step — fights debconf/
postinst rather than fitting it), Docker Compose picked as the direction
if this gets built.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Notes down what's discussed but not requested yet — per-customer notes,
customer tags/segments, segmented broadcast, finer admin roles, non-
Telegram broadcast channels, lead funnel, deeper analytics — so the idea
isn't lost, without committing to build any of it now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Setting["address_en"] (Адмінка → Налаштування → Заклад), opt-in fallback
like Product.name_en — used in place of "address" for English-locale
customers in /api/settings (Mini App contacts + closed screen), the bot's
/contacts command, and the pickup-ready notification's address line.
Untranslated (blank) address_en falls back to the Ukrainian address as before.
Two gaps in the earlier i18n scaffolding: the Mini App's UA/EN switcher
existed but most screens and all menu content stayed hardcoded Ukrainian.
Menu content (business data, not UI chrome):
- Category.name_en, Product.name_en/description_en (nullable, opt-in per
item — untranslated items fall back to Ukrainian rather than showing
blank). Editable from the existing category/product admin forms.
- Allergen labels (a small fixed EU-mandated set) get an English label
directly in ALLERGENS, no DB column needed.
- /api/categories and /api/products take an optional ?locale= param (no
session on these unauthenticated routes to read a stored preference from).
Full EN coverage for everything else:
- Every remaining webapp screen: MenuPage/ProductCard, CartItemRow (fixes a
pre-existing gap in the "already translated" cart screen), OrdersPage,
ContactsPage, ClosedScreen, BottomNav, AnnouncementPopup, plus app-level
status/error text.
- Every reachable bot message: /menu, /contacts (including its
locale-dependent reply-keyboard button, matched by text across all
locales since the label itself varies), and the rating thank-you flow.
- Every customer-facing checkout/order error message (empty cart, closed,
min order amount, payment/promo validation, cancel/pay errors) via
locale params threaded through workhours.closed_message() and
promo.validate_promo_code().
New: PATCH /api/me/locale persists an explicit in-app language choice back
to TelegramUser.locale (fire-and-forget from the webapp switcher), so it
also applies to the bot's own push notifications, not just the Mini App
session that made the choice.
Fixed a real bug caught while testing: product_form.html's allergen
checklist unpacked ALLERGENS values as 2-tuples, which broke (ValueError)
once they became 3-tuples (uk_label, emoji, en_label) — admin's "add
product" page 500'd until this was caught in browser testing.
Introduces bober_bbq/payments/base.py as the shared provider contract
(ProviderNotConfigured/ProviderError, the atomic claim_order_paid race
guard, and a documented function-shape convention) and
bober_bbq/payments/registry.py for provider selection/dispatch, so
checkout.py, orders.py, and reconciliation.py no longer hardcode
Monobank. monobank.py's public names/behavior are unchanged -- its
exceptions now subclass the generic ones and claim_order_paid is
re-exported from base.py, but every existing import keeps working.
Adds liqpay.py (Checkout/CNB API: base64(JSON)+sha1 signing, form-encoded
webhook, status polling, a local checkout-redirect bridge page since
LiqPay has no server-side "create invoice" call) and
api/payments_liqpay.py for its webhook + redirect routes. Wires a
"payment_provider" setting (admin-configurable, env fallback) and adds
matching LiqPay admin settings fields/test button.
No live LiqPay credentials were available to test against; verification
is a re-derived payment-race regression test (webhook/poll/reconciliation
racing to mark an order paid, for both providers) plus mocked-HTTP
structural checks. Uncertain LiqPay details (verify_token's error-code
heuristic) are flagged in liqpay.py and docs/PAYMENTS.md, which also
documents adding a third provider (Fondy, not implemented).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend: dependency-free t(key, locale) helper (bober_bbq/utils/i18n.py) in
the spirit of Setting.get(), a new TelegramUser.locale column (migrated via
the existing _add_column_if_missing pattern) populated from Telegram's
language_code on first contact, and order-status notification messages
(notify_customer.py) plus the bot's /start welcome translated to en as proof.
Webapp: a ~30-line I18nProvider/useI18n context (webapp/src/i18n/) with a
per-locale translations dict, auto-detecting locale from a saved choice,
Telegram's language_code, or the browser, with a manual UA/EN switcher in
the header. CartPage and CheckoutPage are fully bilingual end-to-end as the
representative slice; the rest of the app is unchanged Ukrainian-only.
Documented in docs/I18N.md: how to add strings/languages, and the explicit
boundary of what's translated today vs. left for later (admin panel is
intentionally untouched).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Converts the ad-hoc scratch-script verification pattern used throughout
this session's development (temp SQLite DB + create_app() + monkeypatched
send_message/fiscalize_in_background/get_invoice_status, print-based
check() assertions) into a real, repo-committed pytest suite under tests/.
Covers: claim_order_paid() atomicity, cross-path payment race dedup
(webhook/checkout-poll/reconciliation all observing the same paid
invoice), fiscalization error alerting + cooldown, admin login
brute-force lockout, the insecure-defaults startup check, supplier-import
path-traversal protection, basic checkout smoke, and the supplier
restock -> stock/supplier-link bug fixed earlier this session.
Adds requirements-dev.txt (pytest only, keeping the existing
monkeypatch-only style rather than a new mocking framework), pytest.ini
so `pytest` runs from the repo root with no flags, and docs/TESTING.md
covering how to run it and what's explicitly not covered yet (the React
webapp, bot conversation flows beyond the checkout API, live
Checkbox/Monobank/Google Drive integrations).
38 tests, all passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SQLite stays the default (fine for a small single-cafe deployment); this
makes Postgres a supported alternative via DATABASE_URL for clients who
outgrow SQLite's single-writer lock (multiple concurrent admin users,
high write volume).
- requirements.txt: add psycopg2-binary (Postgres driver for SQLAlchemy).
- bober_bbq/migrate.py: the admin_users.created_at/last_login_at ALTER
TABLE DDL used the type name DATETIME, which SQLite accepts (it only
has type affinity) but Postgres doesn't recognize as a real type — use
TIMESTAMP instead, valid on both.
- bober_bbq/admin/inventory.py: the ingredient-name duplicate check used
db.func.lower() in SQL, which is ASCII-only on SQLite and silently
fails to match Cyrillic names (same bug already fixed this session in
admin/suppliers.py's supplier-name dedup) — compare in Python instead.
Not a Postgres crash risk (Postgres's LOWER() is Unicode-aware), but
worth fixing for consistency since moving to Postgres would otherwise
silently change this check's behavior.
- bober_bbq/utils/gdrive_backup.py and admin/system.py were audited for
SQLite-specific behavior (PRAGMA integrity_check, direct file
copy/restore) but already guard every such path behind
`uri.startswith("sqlite:///")` / `_sqlite_path()` checks that
gracefully no-op with a clear message on Postgres — no changes needed.
- scripts/migrate_sqlite_to_postgres.py (new): standalone, manually-run,
one-off tool for actually moving a specific client's data off SQLite.
Reuses the app's own SQLAlchemy models (db.metadata) rather than raw
table introspection, copies rows in FK-safe order, refuses to run
against a non-empty target unless --truncate is passed, and resets
Postgres auto-increment sequences after copying explicit ids. Not
wired into run_web.py or any automatic startup path.
- docs/DEPLOYMENT.md: new section on when/how to use Postgres instead of
SQLite, standing up a Postgres instance, the DATABASE_URL format, and
when/how to run the migration script.
Verified: create_app() + db.create_all() + migrate() + seed() (including
the ALTER-TABLE-ADD-COLUMN code path exercised against a DB missing
those columns) still work cleanly against a fresh temp SQLite DB. The
migration script's row-copy/FK-ordering/truncate/non-empty-guard logic
was verified end-to-end using SQLite as a stand-in target, since no
Postgres server is available in this environment — the Postgres-specific
pieces (actual connection, pg_get_serial_sequence/setval) are
unverified beyond code review.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Покроковий посібник простою мовою для власника кафе/персоналу без
технічного бекграунду: замовлення, меню, промокоди, відгуки, склад,
постачальники, налаштування, персонал, резервні копії — за реальними
кнопками й пунктами меню з коду bober_bbq/admin/*.py та шаблонів.
Two gaps: an unpaid delivery had no way to later be marked paid (or
corrected at all) short of delete-and-recreate, losing history; and
there was no way to hand a supplier or an accountant a statement of
account.
- New GET+POST /suppliers/<id>/deliveries/<id>/edit
(supplier_delivery_edit.html) — edits every field including
paid_amount/payment_date. Deliberately a pure ledger correction: like
the existing delete button already warns, this never touches
current_stock/StockMovement even if quantity or the ingredient link
changes — stock only moves through the ingredient's own actions.
- New bober_bbq/utils/supplier_reports.py generates both a per-supplier
detailed statement and an all-suppliers summary, each in Excel
(openpyxl, already a dependency) and PDF (new: fpdf2), optionally
scoped to a delivery-date range. Download buttons added to the
supplier detail page and the suppliers list.
- PDF needs a real Cyrillic TTF; added fpdf2 to requirements.txt and
documented (and will install) fonts-dejavu-core on the server rather
than bundling a font in the repo — DejaVu is freely embeddable,
Windows fonts are not. A missing font produces a clear flash message,
not a 500.
Verified with a 33-check smoke test across both features (edit
correctness + stock isolation, date-range filtering on both statement
shapes, real route responses reopened/parsed, and the missing-font
fallback path) plus a full regression rerun of every supplier/inventory
smoke suite from this session.
Reliability gaps found while investigating today's suppliers 500 (which I
only learned about because the user reported it — nothing in the app
itself said anything):
- New bober_bbq/utils/error_alerts.py: notifies OWNER_IDS via the existing
Telegram send_message pattern (already used by gdrive_backup/
reconciliation/checkbox_prro) on ANY unhandled request exception, via
Flask's got_request_exception signal — purely observational, doesn't
change the actual error response. Same helper now also wraps every
APScheduler job in run_web.py, closing the one job (reconcile_payments)
that had no failure handling at all and could die silently. A 15-minute
per-(exception type, source) cooldown keeps a repeating failure from
spamming the chat.
- gdrive_backup.py: verifies each local DB backup with PRAGMA
integrity_check before it's ever uploaded (a corrupt copy now fails
loudly instead of silently becoming an unusable "backup"), and now also
archives+uploads static/uploads/ (product photos) alongside the DB —
previously never backed up at all.
Also, per the earlier per-client template-readiness pass: provision_client.sh
automates docs/DEPLOYMENT.md's clone/.env/nginx/TLS/systemd steps for a
NEW client deployment (confirmation prompt before every irreversible
step; refuses to run against an existing install-dir). Not executed
anywhere this session — no target VPS yet, verified via `bash -n` and a
step-by-step review against the runbook it automates.
All of this is additive and was smoke-tested to confirm zero behavior
change for the live instance: same error responses, same backup content,
same scheduler behavior when nothing is actually broken.
Groundwork for reusing this codebase per-client (own VPS/DB/bot per
cafe): the operational config (bot token, DB URL, secrets, Checkbox PRRO
creds) was already .env/Setting-driven, but ~13 spots still hardcoded the
literal string "Bober BBQ" instead of reading the existing cafe_name
Setting, and the webapp's static shell (index.html title, PWA manifest)
had no templating at all since Settings only load after JS boots.
- Route the remaining hardcoded strings through Setting.get("cafe_name",
"Bober BBQ") — same pattern already used correctly in bot/handlers/
start.py. Every fallback stays "Bober BBQ", so production is byte-
identical with no cafe_name override.
- Template webapp/index.html via Vite's native %VITE_CAFE_NAME% HTML
replacement, backed by a committed webapp/.env (default "Bober BBQ",
no secrets) with a per-client override via gitignored .env.local.
- Add webapp/scripts/gen-manifest.mjs to generate manifest.json from a
new manifest.template.json the same way, since Vite doesn't process
public/ assets — wired into the build script.
- Parameterize deploy.sh's systemd unit names and the admin log-viewer's
log paths via optional SERVICE_WEB/SERVICE_BOT/LOG_FILE_WEB/
LOG_FILE_BOT env vars, defaulting to today's literal values.
- Rewrite docs/DEPLOYMENT.md into a repeatable new-client runbook (fixed
an existing /opt/bober-bbq vs /opt/bober-bbq-bot inconsistency, added
a rebranding checklist).
Verified via a smoke test that every changed string still renders
"Bober BBQ" with no overrides present (matching prod's actual .env/DB
state), and that webapp builds both with and without a VITE_CAFE_NAME
override produce the expected output — including a no-override rebuild
confirming byte-identical output to before this change.
Flask API/admin backend, aiogram bot with delivery/pickup FSM flows,
monobank payment integration, and a Vite/React Telegram Mini App for
menu browsing and cart management.