From 32c2cb0b89986239533d84de43ac63aac5b881b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 02:46:10 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=D0=94=D0=BE=D0=BD=D0=B0=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B5=20=D0=B8=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=BA=D0=B5=20=D0=B1=D0=B8=D0=BB=D0=BB=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Отдельный платеж don:, миграция 012, флаг и способы выключены. Кнопка «Поддержать проект», лента в админке, значок и список в аккаунте. Подписку и реферальные бонусы не меняет. Co-authored-by: ANDY BUM --- CHANGELOG.en.md | 2 + CHANGELOG.md | 2 + docs/superpowers/plans/2026-11-12-donate.md | 56 ++ .../specs/2026-11-12-donate-design.md | 15 + mobile/src/screens/Account.tsx | 51 +- mobile/src/screens/OmpJackettCard.tsx | 1 + ops/billing/admin/src/App.tsx | 4 + ops/billing/admin/src/components/palette.tsx | 2 +- ops/billing/admin/src/lib/types.ts | 21 + .../admin/src/panels/DonationsPanel.tsx | 263 +++++++ ops/billing/admin/vite.config.ts | 2 + ops/billing/public/assets/index-BBBXsVw3.js | 240 ------- ...{index-BA9iWkwX.css => index-CCVriGa7.css} | 2 +- ops/billing/public/assets/index-CnoPEUQl.js | 240 +++++++ ops/billing/public/index.html | 4 +- ops/billing/src/adminApi.mjs | 2 + ops/billing/src/bot.mjs | 22 +- ops/billing/src/db/pg/schema.mjs | 37 + ops/billing/src/db/pg/spec.mjs | 29 + ops/billing/src/donate.mjs | 649 ++++++++++++++++++ ops/billing/src/server.mjs | 181 ++++- ops/billing/src/services.mjs | 2 + ops/billing/src/state.mjs | 22 + ops/billing/test/botUx.test.ts | 2 +- ops/billing/test/donate.test.ts | 254 +++++++ ops/billing/test/donateHttp.test.ts | 272 ++++++++ ops/billing/test/logic.test.ts | 2 +- ops/billing/test/stores.test.ts | 1 + src/billing/session.ts | 2 + src/i18n/en.ts | 4 + src/i18n/ru.ts | 4 + src/sources/ompJackett.ts | 4 + 32 files changed, 2141 insertions(+), 253 deletions(-) create mode 100644 docs/superpowers/plans/2026-11-12-donate.md create mode 100644 docs/superpowers/specs/2026-11-12-donate-design.md create mode 100644 ops/billing/admin/src/panels/DonationsPanel.tsx delete mode 100644 ops/billing/public/assets/index-BBBXsVw3.js rename ops/billing/public/assets/{index-BA9iWkwX.css => index-CCVriGa7.css} (62%) create mode 100644 ops/billing/public/assets/index-CnoPEUQl.js create mode 100644 ops/billing/src/donate.mjs create mode 100644 ops/billing/test/donate.test.ts create mode 100644 ops/billing/test/donateHttp.test.ts diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 0ad3fe234..a11d76c25 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -6,6 +6,8 @@ The English translation covers the latest versions only; the full history (in Ru ## Unreleased +- [phone] Account: a Supporter mark on a linked account that donated, and a thanks list. The list shows only people who opened their name +- [tech] Bot and admin donations: a separate payment, migration 012, flag and methods off. It does not change a subscription or referral bonuses. Billing was not deployed - [phone] Catalog: one, two or three cards a row, list and compact. The poster keeps quality on the left and the season line at the bottom, plus watching and a progress bar. The list has a status line and no size - [phone] Search: plus opens magnet paste and a .torrent file, and a pasted magnet on any tab opens add. An http(s) .torrent link from another app is added the same way. A file error is visible at once. The feed line drops the tracker phrase, and 0-1 seeds are marked. The sources hint is one line with a close button - [phone] Calendar lists days, Earlier stays folded, and the aired/today plates are gone. Month view marks today with a yellow circle, the selected day in gray, up to three dots plus a count, and a yellow dot for a subscription. A subscription that is any quality and best-only reads "best" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d765ae23..79dae26ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Не выпущено +- [phone] Аккаунт: значок «Поддержал» у привязанного аккаунта с донатом и список благодарностей. В списке только те, кто сам открыл имя +- [tech] Донат в боте и админке: отдельный платёж, миграция 012, флаг и способы выключены. Подписку и реферальные бонусы не меняет. Биллинг не выкатывался - [phone] Каталог: одна, две или три карточки в ряд, список и компактный вид. На постере качество слева и строка сезонов снизу, «смотрите» с полоской. В списке строка статуса, без размера - [phone] Поиск: «+» для magnet и файла .torrent, вставленная magnet-ссылка на любой вкладке открывает добавление. Ссылка http(s) на .torrent из другого приложения добавляется так же. Ошибка файла видна сразу. В ленте короткая строка размера и сидов без «по данным трекера», 0-1 сид выделен. Подсказка источников - одна строка с крестиком - [phone] Календарь: список по дням, «Ранее» свернуто, без плашек «сегодня» и «вышла». В месяце сегодня - желтый круг, выбранный день серый, до трех точек и счетчик, желтая точка у подписки. У подписки «любое качество» и «только лучшее» пишется «лучшее» diff --git a/docs/superpowers/plans/2026-11-12-donate.md b/docs/superpowers/plans/2026-11-12-donate.md new file mode 100644 index 000000000..7dae3df74 --- /dev/null +++ b/docs/superpowers/plans/2026-11-12-donate.md @@ -0,0 +1,56 @@ +# Донат OMP Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Принять донат любой суммы в боте и показать его в админке, не продлевая подписку. + +**Architecture:** Модуль `ops/billing/src/donate.mjs` хранит черновик и оплату в коллекции `donations`. Вебхуки с меткой `don:` идут в `applyDonation`, остальные в `applyPayment`. Список благодарностей отдаёт `GET /v1/thanks` и рисуется на экране аккаунта. + +**Tech Stack:** Node, Postgres, Preact, React admin. + +**Spec:** `docs/superpowers/specs/2026-11-12-donate-design.md` + +## Global Constraints + +- Только дефис `-`. +- Флаг и способы доната выключены по умолчанию. +- Секреты не коммитить. +- Миграция `012_donations`, пока на main последняя `011_referral`. +- Не вливать PR и не деплоить биллинг. + +--- + +### Task 1: Правила суммы и звёзд + +**Files:** +- Create: `ops/billing/src/donate.mjs` +- Test: `ops/billing/test/donate.test.ts` + +- [x] Падающий тест на флаг, лимиты и `starsForRub` +- [x] Реализация `normalizeDonateSettings`, `validateDonateAmount`, `validateDonateComment`, `starsForRub` + +### Task 2: Бот + +**Files:** +- Modify: `ops/billing/src/bot.mjs` +- Test: `ops/billing/test/donate.test.ts` + +- [x] Кнопка «Поддержать проект», `/donate`, сумма, комментарий, имя, комиссия ЮMoney, счёт Stars + +### Task 3: Оплата и вебхук + +**Files:** +- Modify: `ops/billing/src/server.mjs`, `ops/billing/src/db/pg/schema.mjs`, `ops/billing/src/db/pg/spec.mjs`, `ops/billing/src/state.mjs` +- Test: `ops/billing/test/donateHttp.test.ts` + +- [x] Один вебхук - один донат, короткая сумма не проходит, подписка не меняется +- [x] Админ сохраняет флаги, скрывает имя, отмечает возврат, отдаёт CSV + +### Task 4: Админка и аккаунт + +**Files:** +- Create: `ops/billing/admin/src/panels/DonationsPanel.tsx` +- Modify: `mobile/src/screens/Account.tsx`, `mobile/src/screens/OmpJackettCard.tsx` + +- [x] Вкладка «Донаты» +- [x] Бейдж «Поддержал» и публичный список на экране аккаунта diff --git a/docs/superpowers/specs/2026-11-12-donate-design.md b/docs/superpowers/specs/2026-11-12-donate-design.md new file mode 100644 index 000000000..30ab46497 --- /dev/null +++ b/docs/superpowers/specs/2026-11-12-donate-design.md @@ -0,0 +1,15 @@ +# Донат OMP + +Донат - отдельный платёж. Он не продлевает подписку и не пишет строку в `payments`, поэтому реферальное начисление, которое смотрит только оплаченные подписки, его не видит. + +Флаг `settings.donate.enabled` и отдельные тумблеры ЮMoney, Stars и Crypto Pay выключены по умолчанию. Секреты остаются в env и в уже существующих полях оплаты. + +Метка платежа `don:`. Подпись вебхука и проверка суммы те же, что у подписки: ЮMoney через `settledCovers`, Stars и Crypto Pay через `altCovers`. Повтор того же `operationId` не создаёт второй донат. + +Кнопка «Поддержать проект» и команда `/donate` есть всегда. Пока флаг выключен, бот пишет, что поддержка выключена. Суммы 100/300/500/1000 и своя сумма с мин/макс из админки. Комментарий необязательный. Галочка имени по умолчанию выключена. После оплаты бот шлёт текст благодарности из админки. + +Список благодарностей живёт в экране аккаунта телефона и компьютера (один и тот же `mobile/src`). Экран уже читает биллинг. Сайт omp-player.online статический и выкатывается отдельно, поэтому публичный список там не ставим. Бейдж «Поддержал» берётся из `/v1/me` (`supporter`), список из `GET /v1/thanks`. + +Админка: вкладка «Донаты», лента по Москве, фильтры, итоги дня/недели/месяца, график 30 дней, средний чек, топ, CSV, возврат отметкой, скрытие имени и комментария, уведомление в Telegram о каждом донате и/или от порога. + +Миграция `012_donations`. Перед мерджем сверить номер со свежим main: на момент ветки последняя была `011_referral`. diff --git a/mobile/src/screens/Account.tsx b/mobile/src/screens/Account.tsx index 9ce5340f4..229cb5906 100644 --- a/mobile/src/screens/Account.tsx +++ b/mobile/src/screens/Account.tsx @@ -1,6 +1,6 @@ -import { useState } from 'preact/hooks'; +import { useEffect, useState } from 'preact/hooks'; import { t } from '../../../src/i18n'; -import { billingPost } from '../../../src/billing/client'; +import { billingGet, billingPost } from '../../../src/billing/client'; import { emitBillingChange } from '../../../src/billing/session'; import { withDeviceToken } from '../../../src/billing/tokens'; import { goBack } from '../nav'; @@ -21,11 +21,58 @@ export function Account() {

{t('settings.account')}

+ ); } +function ThanksList() { + const [names, setNames] = useState<{ name: string; amount?: number }[] | null>(null); + const [showAmounts, setShowAmounts] = useState(false); + const [down, setDown] = useState(false); + useEffect(() => { + let stop = false; + billingGet(phoneSourceContext(), '/v1/thanks', { timeoutMs: 8000 }) + .then((res) => { + if (stop) return; + if (res.status < 200 || res.status >= 300) { + setDown(true); + return; + } + try { + const body = JSON.parse(res.text) as { names?: { name?: string; amount?: number }[]; showAmounts?: boolean }; + const rows = Array.isArray(body.names) ? body.names : []; + setNames(rows.filter((row) => row && row.name).map((row) => ({ name: String(row.name), amount: typeof row.amount === 'number' ? row.amount : undefined }))); + setShowAmounts(body.showAmounts === true); + } catch (e) { + setDown(true); + } + }) + .catch(() => { + if (!stop) setDown(true); + }); + return () => { + stop = true; + }; + }, []); + return ( +
+

{t('sources.billing.thanksTitle')}

+ {down ?

{t('sources.billing.thanksDown')}

: null} + {names && names.length === 0 && !down ?

{t('sources.billing.thanksEmpty')}

: null} + {names && names.length + ? names.map((row) => ( +
+ {row.name} + {showAmounts && row.amount != null ? ' - ' + row.amount + ' ₽' : ''} +
+ )) + : null} +
+ ); +} + function PromoEntry() { const [code, setCode] = useState(''); const [note, setNote] = useState(''); diff --git a/mobile/src/screens/OmpJackettCard.tsx b/mobile/src/screens/OmpJackettCard.tsx index 3a78ba1a1..b29aa83bb 100644 --- a/mobile/src/screens/OmpJackettCard.tsx +++ b/mobile/src/screens/OmpJackettCard.tsx @@ -208,6 +208,7 @@ export function OmpJackettCard() { })} {account && account.overLimit ?
{t('sources.billing.overLimit')}
: null} + {account && account.supporter ?
{t('sources.billing.supporter')}
: null} {cryptoError ? (