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>
Fiscal errors previously only wrote fiscal_status/fiscal_error and logged,
so an order could sit fiscally unissued indefinitely unless someone opened
its detail page — now routed through the existing error_alerts.notify().
Three independent code paths (Monobank webhook, the Mini App's own
"Перевірити оплату" poll, and the periodic reconciliation job) could all
observe the same "just paid" transition via a read-then-write check and
double up notifications/fiscalization. Added claim_order_paid() as a single
atomic UPDATE ... WHERE payment_status != 'paid', with side effects gated
on whichever caller actually won the transition.
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.
- Customer detail page loaded every order at once; now paginated
(20/page) like the main orders list, with stats still computed over
the full history rather than just the current page.
- Monobank's basketOrder items support an "icon" field for showing the
product photo on the hosted payment page — we were never sending it.
Now included as an absolute URL when the product has an image.
- New "Показувати список товарів на сторінці оплати" setting — when off,
the basketOrder field is omitted from the invoice so Monobank's
checkout page doesn't list items
- Manager chat order message: card orders now show "⏳ Очікує оплати"
instead of a bare ❌ (which read as an error, not the expected
pending state); cash orders say "при отриманні" instead of implying
something's wrong
Critical fix: verify_webhook_signature was calling load_der_public_key
on Monobank's pubkey response, but the key is PEM-wrapped, not raw DER
— every real webhook was being rejected with a signature error and
silently falling back to the 10-minute reconciliation poll instead of
updating instantly. Switched to load_pem_public_key.
Also:
- Add a separate test-mode Monobank token + "Тестовий режим оплати"
toggle in admin settings, so payments can be verified end-to-end
with fake (Luhn-valid) card numbers before going live. Monobank has
no shared test key — each admin gets their own via api.monobank.ua.
- Fix two misaligned buttons (clear-database, Monobank "Перевірити")
caused by the global input margin-bottom throwing off flex
align-items in forms that mix a label+input with a button.
- Monobank token and the payment description shown to customers are now
editable in Адмінка → Налаштування → Оплата, instead of requiring SSH
access to change .env. Falls back to MONOBANK_TOKEN in .env if the
admin panel field is empty, so existing deployments keep working.
- Add a "Перевірити" button that pings Monobank's merchant details
endpoint to validate a token live, without creating a real invoice —
catches typos/revoked tokens before a real customer hits them.
- Payment destination text is now a configurable template ({number}
placeholder) instead of a hardcoded string.
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.