Commit graph

24 commits

Author SHA1 Message Date
375022e739 Add backup_remote.py and wire up the rest of the rclone rework
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>
2026-09-05 22:57:59 +03:00
87870d6e4b docs/TODO.md: add packaging notes (Docker Compose over .deb)
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>
2026-08-27 21:59:15 +03:00
848d6f0d75 Add docs/TODO.md: future CRM-enhancement ideas
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>
2026-08-27 21:54:07 +03:00
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
96737e4f3f Add optional English translation for product weight/volume
Product.weight_en (e.g. "300 г" -> "300 g"), same opt-in-per-item fallback
as name_en/description_en. Editable from the product admin form.
2026-08-27 17:14:36 +03:00
a1bda220df Add optional English translation for the cafe's address
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.
2026-08-27 16:52:27 +03:00
ff232d1a0f Bilingual menu content + finish EN coverage across bot and Mini App
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.
2026-08-27 16:38:10 +03:00
391da771f7 Document Postgres backup gap as a TODO before any real migration 2026-08-27 15:19:10 +03:00
52eec01477 Merge pluggable payment-provider layer; add LiqPay alongside Monobank 2026-08-27 13:41:27 +03:00
27f1d5f149 Add pluggable payment-provider layer; implement LiqPay alongside Monobank
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>
2026-08-27 13:38:24 +03:00
399e6a48e9 Merge i18n scaffolding: bilingual bot notifications + cart/checkout 2026-08-27 13:37:00 +03:00
3e8314925c Add i18n scaffolding: bot/notification locale support + bilingual cart/checkout
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>
2026-08-27 13:35:47 +03:00
11a155bca5 Merge repo-committed pytest test suite 2026-08-27 13:33:34 +03:00
8bba2873b5 Add repo-committed pytest test suite
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>
2026-08-27 13:32:09 +03:00
fb97a3c1e0 Merge opt-in PostgreSQL support 2026-08-27 13:31:51 +03:00
1ede4b9692 Add opt-in PostgreSQL support alongside default SQLite
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>
2026-08-27 13:31:12 +03:00
fddde6e0a5 Merge non-technical owner's guide 2026-08-27 13:28:54 +03:00
dd5688ab9f Add API reference docs for /api/* (Markdown + OpenAPI)
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.
2026-08-27 13:27:43 +03:00
78df456653 docs: додати нетехнічний посібник власника для адмін-панелі
Покроковий посібник простою мовою для власника кафе/персоналу без
технічного бекграунду: замовлення, меню, промокоди, відгуки, склад,
постачальники, налаштування, персонал, резервні копії — за реальними
кнопками й пунктами меню з коду bober_bbq/admin/*.py та шаблонів.
2026-08-27 13:27:35 +03:00
a3c29095c8 Edit supplier deliveries after the fact; downloadable Excel/PDF statements
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.
2026-08-26 20:17:48 +03:00
c1e3e83d55 Add owner-alerting on unhandled errors, backup integrity checks, and a client-provisioning script
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.
2026-08-25 18:11:15 +03:00
922d9c5172 Make brand identity reusable for redeploying to other clients
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.
2026-08-25 17:54:29 +03:00
c3b01952c4 Document the one-command deploy.sh script on the live server 2026-08-08 22:54:54 +03:00
0eabebaca3 Initial commit: Bober BBQ Telegram bot + backend + admin + Mini App
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.
2026-08-08 15:50:50 +03:00