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>
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>
No brute-force protection existed on the admin login form. Added an
in-memory per-IP lockout (5 failed attempts / 15 min -> 15 min lockout),
alerting the owner via Telegram when a lockout triggers.
Also warn loudly at startup (log + owner Telegram alert, not a hard
refusal) if SECRET_KEY or the admin password are still the config.py
defaults — previously a skipped .env line would silently run production
with session-forgeable SECRET_KEY or an admin/admin login.
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.
A ПРРО shift is legally capped at 24h open — nothing was ever closing
it. Adds an opt-in daily auto-close (configurable time, checked every
~10 min via APScheduler) plus a manual "Закрити зміну зараз" button in
Налаштування → Фіскалізація. Off by default like the rest of the
Checkbox integration; failures notify the owner in Telegram rather
than failing silently, since a stuck-open shift is a real compliance
risk.
Opt-in via "gdrive_backup_enabled" — off by default, no effect until
configured. Uses a Google service account (no interactive OAuth, so it
runs unattended from a headless server forever).
- bober_bbq/utils/gdrive_backup.py: uploads a fresh local SQLite backup
on a configurable interval, then deletes the oldest Drive files once
the folder holds more than gdrive_retention_count backups. The
scheduled job (hourly, in run_web.py) re-checks the interval itself
each tick rather than being tied to a fixed APScheduler schedule, so
changing the interval in the admin panel takes effect without a
restart. Failures notify the owner in Telegram, matching the
existing monobank-token-health pattern.
- Admin Backups page gets a new section: paste the service account's
JSON key + target Drive folder ID, set interval/retention, "Перевірити
підключення" to verify without uploading, "Backup зараз" to trigger
one immediately, plus inline step-by-step setup instructions (Cloud
Console → enable Drive API → service account → JSON key → share the
folder with its email → paste both here).
- gdrive_last_backup_at/gdrive_last_backup_status are intentionally
NOT in DEFAULT_SETTINGS (job-written only) — putting them there would
make the generic settings-form save loop blank them out on every
unrelated settings save, since there's no form field for them.
Webhooks can be missed (network blip, mid-migration domain change, etc.)
leaving a paid order stuck as "unpaid" forever. Add two background jobs
(APScheduler, in the web process):
- reconcile_pending_payments — every 10 min, re-checks unpaid card
orders from the last 6 hours directly against Monobank's invoice
status API and marks them paid + notifies customer/manager if the
webhook was missed.
- check_monobank_token_health — hourly ping via verify_token(); if a
previously-working token starts failing, alerts OWNER_IDS in Telegram
so a revoked/expired token doesn't silently break card payments.
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.