Epic 13: Биллинг v5.1 — package-based подписки через PowerUp + P0 фиксы #49

Open
claude wants to merge 23 commits from feat/epic-13-billing-package-powerup into provider-billing
Owner

Зонтичный PR Epic 13 со стороны mono. Stories 13.2 (w.wal.bill USER_SHARED → COOPERATIVE), 13.3 (billing::topupaxon action + o.bil.axn), 13.4/13.5/13.6 (PowerupPlugin 8 guards + calendar+threshold + BillingProviderClient), 13.8 (desktop UI). + adversarial review P0 фиксы 6ccc167584: BlockchainService.powerUp no-swallow + tx_id return, PowerupPlugin mutex, двухфазный flow createPackageInvoice → powerUp → confirmTopupAxon, отключён фиктивный axon_pre_gate, ProviderSubscription расширен полями kind/monthly_quota_rub/packages_current_period_amount, ChairmanDesktop manifest зарегистрировал /:coopname/powerup. SDK regenerated. Связанный provider PR: C9S/provider#25

Зонтичный PR Epic 13 со стороны mono. Stories 13.2 (w.wal.bill USER_SHARED → COOPERATIVE), 13.3 (billing::topupaxon action + o.bil.axn), 13.4/13.5/13.6 (PowerupPlugin 8 guards + calendar+threshold + BillingProviderClient), 13.8 (desktop UI). + adversarial review P0 фиксы 6ccc1675840: BlockchainService.powerUp no-swallow + tx_id return, PowerupPlugin mutex, двухфазный flow createPackageInvoice → powerUp → confirmTopupAxon, отключён фиктивный axon_pre_gate, ProviderSubscription расширен полями kind/monthly_quota_rub/packages_current_period_amount, ChairmanDesktop manifest зарегистрировал /:coopname/powerup. SDK regenerated. Связанный provider PR: https://git.coopenomics.world/C9S/provider/pulls/25
claude added 7 commits 2026-05-30 07:00:53 +00:00
- LEDGER2_WALLET_REGISTRY: BILLING_FUND_PAY перенесён из USER_SHARED-блока
  в COOPERATIVE-блок (USER_SHARED 6→5, COOPERATIVE 10→11; total=16 stable).
- LEDGER2_USER_SHARED_PROGRAM_MAPPING: запись BILLING_FUND_PAY → program_id=1
  удалена (6 вместо 7), т.к. COOPERATIVE-кошельки не требуют программной проверки.
- ledger2_wallets::BILLING_FUND_PAY doc: переписан под COOPERATIVE scope=coopname,
  добавлено упоминание новой операции o.bil.axn (через action billing::topup_axon).
- billing.hpp main-doc: добавлены topup_axon (Epic 13) + объяснение, почему
  w.wal.bill переехал в COOPERATIVE (совпадение с источником авторизации PowerUp).
- convert.cpp doc: уточнено направление o.bil.fund — share[username] USER_SHARED
  → bill[coopname] COOPERATIVE; обе модели подписок (time + package) покрыты.
- cooptypes/src/ledger2/wallets.generated.ts: регенерация через
  pnpm --filter cooptypes gen:from-cpp — kind="COOPERATIVE", mapping=6.

Двухэтапная миграция: этап 1 — смена kind в реестре (этот коммит).
Этап 2 (отдельная задача deployment runbook) — backfill существующих
userwallets[w.wal.bill] → cooperative_wallet[w.wal.bill] на проде, когда
Epic 12 будет смерджен туда. На dev-стенде backfill не нужен (reboot:extra
обнуляет балансы).
- operations::billing::TOPUP_AXON = "o.bil.axn"_n; doc-комментарий описывает
  семантику BURN внутри 86 (расход членского взноса на инфраструктурный пакет).
- OPERATION_REGISTRY запись #22 — TOPUP_AXON, WalletOp::BURN с BILLING_FUND_PAY,
  без destination (реальный AXON-перевод между token-аккаунтами идёт inline
  отдельно из PowerupPlugin, не через ledger2).
- billing.hpp action: void topupaxon(coopname, amount, payment_hash) — имя без
  подчёркивания (eosio::name требует base32 без `_`).
- billing/src/topup_axon.cpp: реализация (require_auth(coopname),
  assert_payer_is_cooperative, проверки amount/symbol/payment_hash, Ledger2::apply).
- billing.cpp: подключение topup_axon.cpp в сборку.

ARM-build: docker run dicoop/blockchain_v5.1.1:dev → billing.wasm успешно
собран (Linking CXX executable billing.wasm; [100%] Built target billing).
Зеркало existing confirmPayment, но для package-модели Epic 13: POST
`/billing/topup-axon-confirmed` с {payment_hash, tx_id}. Используется
(a) на coopback'е пайщика-кооператива в PowerupPlugin после успешного
on-chain topupaxon (Story 13.5); (b) на coopback'е Воскхода
(BILLING_HUB_MODE=true) как defensive-handler, если PowerupPlugin не успел.
Идемпотентность — на стороне provider (markPackageTopupReceived).
defaultConfig + Zod-схема расширены:
1. cooldownMinutes (мин пауза между докупками)
2. dailyAxonCap (потолок AXON/день)
3. dailyPackageCap (потолок пакетов/день)
4. axonMinBalance (pre-gate AXON-баланса coopname@active)
5. rubFloor (минимум RUB на w.wal.bill)
6. circuitBreakerUntilMonth (YYYY-MM до которого блокировка после
   MonthlyPackageQuotaExceeded)
7. monthlyRubCap (локальный потолок плагина; финальная проверка — у provider'а)
8. dailyPackageSize override + lastCalendarPackageMonth + дневные счётчики.

Helper-методы:
- checkRunawayGuards(): возвращает имя сработавшего guard'а или null.
  Порядок проверок: circuit_breaker → cooldown → daily_axon_cap →
  daily_package_cap → axon_pre_gate → rub_floor.
- recordTopup(): fresh-snapshot read-modify-write коммит факта докупки
  (dayChanged → ресет счётчиков; lastCalendarPackageMonth обновляется
  только для calendar-trigger'а).
- tripCircuitBreaker(): активация блокировки до конца месяца — вызывается
  из логики триггеров (Story 13.5) при MonthlyPackageQuotaExceeded от provider'а.

Логика триггеров и интеграция с provider createPackageInvoice — Story 13.5.
- calendarCronJob: cron('0 0 1 * *', timezone:'UTC') — runCalendarTask.
  Идемпотентность через config.lastCalendarPackageMonth (YYYY-MM): повтор в
  том же месяце — no-op. Гарантирует «свежий пакет в начале месяца», даже
  если threshold-триггер ещё не сработал.
- runCalendarTask → executePackageTopup('calendar', nowIso).
- runTask (per-minute): при needPowerUp вместо прямого powerUp() теперь зовёт
  executePackageTopup('threshold', nowIso) — единая точка для всех guards.
- executePackageTopup: единая обработка обоих триггеров с checkRunawayGuards +
  recordTopup (fresh-snapshot commit lastTopupAt + daily counters +
  lastCalendarPackageMonth для calendar-trigger'а). При
  MonthlyPackageQuotaExceeded от provider'а → tripCircuitBreaker(monthKey).
- onModuleDestroy: остановка calendarCronJob.

TODO (по мере раскатки): подключить BillingProviderClient.confirmTopupAxon
(через provider createPackageInvoice → on-chain billing::topupaxon → confirm).
- SubscriptionsCard (widgets/ConnectionDashboard): ветка v-if=isPackage(sub)
  показывает «спалено / квота пакеты RUB/мес»; обычные time-подписки — старая
  ветка «RUB/месяц». isPackage = sub.kind === 'package'.
- widgets/PowerUp/ResourceMonitor: BaseCard + 3 q-linear-progress (CPU/NET/RAM)
  с severity-цветом по порогу 70%; pug, токены --p-*, без magic hex.
- features/PowerUp/SettingsForm: BaseForm + BaseInput'ы под 8 guards
  (dailyPackageSize, thresholds, cooldownMinutes, dailyAxonCap, dailyPackageCap,
  monthlyRubCap). Эмит submit (config) → внешний store вызывает Mutations.Extension.Update.
- pages/PowerUp/ResourceMonitorPage: 12-колоночный layout (8 ResourceMonitor +
  4 BaseCard «Сводка пакета»), под ней PowerupSettingsForm; TODO: подключение
  GraphQL-store'а (Mutations.Extension.Update через @coopenomics/sdk).

Соответствует mono-desktop-canon: BaseCard/BaseButton/BaseInput/BaseBanner/BaseForm
из shared/ui/base; никаких прямых q-input/q-card/q-btn в feature-коде; токены
--p-* для отступов/цветов/типографики; English имена; pug-template + setup script.
- BlockchainService.powerUp убран swallow try/catch и теперь возвращает on-chain tx_id — PowerupPlugin полагается на факт отказа, чтобы НЕ инкрементить счётчики (BLOCKER #1 ревью).
- BillingProviderClient: новый метод createPackageInvoice + axios timeout 10s на обоих confirmTopupAxon/createPackageInvoice.
- PowerupPlugin переработан:
  · in-process mutex packageTopupInflight против race calendar + threshold (BLOCKER #3);
  · executePackageTopup теперь делает двухфазный flow createPackageInvoice → powerUp → confirmTopupAxon → recordTopup, когда subscriptionId>0 и provider настроен (BLOCKER «confirm не подключён»);
  · убран фиктивный guard axon_pre_gate (cpu_limit.max ≠ AXON balance) с явным комментарием TODO; AXON-нехватку теперь ловит сам powerUp (после удаления swallow);
  · поля subscriptionId / monthlyPackageIdx в defaultConfig + Zod schema; idx инкрементится с 1 на смене месяца;
  · все три cron'а получили {timezone:'UTC'} + .catch обёртки;
  · boot-time backfill calendar тика — если процесс был down 1-го числа, runCalendarTask() прогоняется один раз при initialize;
  · BillingProviderClient зарегистрирован providers в PowerupPluginModule.
- ProviderSubscriptionDTO + ProviderSubscriptionType расширены kind / monthly_quota_rub / packages_current_period_amount (приходят от provider'а через @coopenomics/provider-client).
- ChairmanDesktop manifest зарегистрировал /:coopname/powerup → ResourceMonitorPage (Story 13.8 UI теперь доступен через меню).
- generate-schema + generate-client + sdk build → providerSubscriptionSelector добавил три новых поля для desktop SubscriptionsCard.isPackage.
claude added 2 commits 2026-05-30 11:13:26 +00:00
Переисследование показало: soviet::converttoaxn в коде — это онбординг-конвертация
паевого в ДЕЛЕГАТСКИЙ членский (w.sov.delgte) + инъекция AXON, с документом
registry_id 51. По решению (Вариант A) заменяем на двухшаговую billing-модель.

Контракты:
- billing::converttoaxn(coopname, amount RUB, payment_hash) — НОВЫЙ: BURN членского
  с w.wal.bill[coopname] (o.bil.axn) + инъекция AXON 10₽=1AXON; coopname@active,
  бездокументарно. Заменяет прежний billing::topupaxon (тот жёг членский, но НЕ
  эмитировал AXON — не имел источника токена).
- soviet::converttoaxn удалён (action+impl+include); онбординг переведён на billing::convert.
- eosio::system::injection: require_auth(_soviet) → require_auth(_billing) (единственный
  вызывающий теперь billing::converttoaxn; иначе эмиссия AXON из billing падала бы).
- operations: TOPUP_AXON → CONVERT_TO_AXON (код o.bil.axn сохранён).

cooptypes:
- billing/actions/converttoaxn.ts (coopname@active) + IConverttoaxn; убрана
  soviet/actions/system/converttoaxn.ts. IConvert дополнен convert_hash (был рассинхрон с ABI).

controller:
- system.adapter.convertToAxon: soviet::converttoaxn → billing::convert (паевой→w.wal.bill,
  документ сохранён). AXON кооператив получает стартовым грантом провайдера.
- BlockchainPort/Service: packagePowerUp — атомарно billing::converttoaxn + eosio::powerup
  одной транзакцией coopname@active.
- PowerupPlugin: provider УБРАН с горячего пути. payment_hash считается локально
  (sha256(coopname|YYYY-MM-01|converttoaxn|idx)), monthlyRubCap enforce'ится локально
  (monthRubSpent), provider узнаёт реактивно через парсер→callback. pre-burn pending
  recovery продвигает idx без повтора tx (контракт payment_hash не дедупит).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Epic 13 v5.1. Coopback Воскхода (BILLING_HUB_MODE) узнаёт о on-chain
конвертации членский→AXON РЕАКТИВНО из блокчейна, без push с горячего
пути PowerupPlugin — кооператив-спица подписывает converttoaxn сам
(coopname@active) даже если провайдер лежит.

- parser: добавлен контракт `billing` в subscribedContracts (иначе action
  не попадёт в Redis-шину парсера → не дойдёт до EventEmitter2).
- BillingConversionListener: @OnEvent('action::billing::converttoaxn')
  парсит coopname/amount/payment_hash + tx_id и зовёт
  BillingProviderClient.confirmTopupAxon. Парсер-агностичен (легаси parser
  и parser2 сходятся на шине action::). Ошибки не пробрасываются —
  автономность коопа важнее, провайдер до-сверится реконсиляцией.
- BillingProviderClient.confirmTopupAxon: добавлены coopname + amount_rub
  (провайдер реактивно заводит package-invoice и списывает квоту); правлен
  устаревший docstring topupaxon → converttoaxn.
- BlockchainConsumerService: на хабе billing-actions всех коопов-спиц
  проходят coopname-фильтр (иначе converttoaxn партнёров отбрасывался).
- billing.module: регистрация листенера.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-05-30 11:22:42 +00:00
Epic 13 v5.1 powerup-UI:
- routes/index.ts — регистрация PowerUpPage (/:coopname/powerup, requiresAuth)
  в реальном роутере (Story desktop #141).
- ResourceMonitorPage — привязка к данным мониторинга ресурсов (#142).
Канон desktop соблюдён (base-обёртки, без hex/inline-px). schema.gql не включён
в этот коммит — крупная регенерация выносится отдельно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-05-30 12:04:05 +00:00
- BlockchainPort: добавлена сигнатура packagePowerUp (реализация в BlockchainService
  была, в интерфейсе — нет). PowerupPlugin вызывает её через тип порта → без этого
  TS2339 и coopback не собирался в CI. Блокер реактивного пакетного пути.
- BillingConversionListener docstring: убрано ложное обещание реконсиляции; явно
  зафиксировано окно потери эмита после ACK и что реконсиляция converttoaxn — follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-05-30 12:16:23 +00:00
Парный фикс к provider: /billing/package-invoice удалён на стороне провайдера.
PowerupPlugin автономен (provider убран с горячего пути докупки), этот клиентский
метод никем не вызывался — мёртвый код.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ant requested changes 2026-06-02 07:04:33 +00:00
@@ -34,1 +35,4 @@
lastDailyReplenishmentDate: '',
// ===== Epic 13 v5.1 — 8 слоёв защиты от runaway =====
// 1) Cooldown между двумя докупками — минимум 5 минут.
Owner

у нас вообще покупок нет - мы кооператив - есть аренда вычислительных ресурсов.

у нас вообще покупок нет - мы кооператив - есть аренда вычислительных ресурсов.
@@ -115,0 +228,4 @@
.default(defaultConfig.lastTopupAt)
.describe(describeField({ label: 'Время последней докупки (ISO)', visible: false })),
subscriptionId: z
.number()
Owner

visible - false должен быть! Иначе редактировать смогут и ломать. Не надо. Просто по-умолчанию 0 и все.

visible - false должен быть! Иначе редактировать смогут и ломать. Не надо. Просто по-умолчанию 0 и все.
@@ -115,0 +253,4 @@
.number()
.min(0)
.default(defaultConfig.monthRubSpent)
.describe(describeField({ label: 'RUB потрачено за месяц', visible: false })),
Owner

Ну и RUB надо убрать отсюда напрямую и взять из config символ валюты на случай смены.

Ну и RUB надо убрать отсюда напрямую и взять из config символ валюты на случай смены.
@@ -0,0 +6,4 @@
import { BillingProviderClient } from './billing-provider.client';
/**
* Epic 13 v5.1 реактивный мост on-chain провайдер для пакетного PowerUp.
Owner

И запомни, что когда указываешь Эпики - ты должен указывать в рамках какого проекта ты работаешь. Конкретно этот эпик 13 - это проект провайдера. А то у меня есть и другие эпики из других проектов и хрен разберешься уже зачем вообще тогда их указывать было.

И запомни, что когда указываешь Эпики - ты должен указывать в рамках какого проекта ты работаешь. Конкретно этот эпик 13 - это проект провайдера. А то у меня есть и другие эпики из других проектов и хрен разберешься уже зачем вообще тогда их указывать было.
@@ -92,0 +110,4 @@
coopname: string;
amountRub: number;
}): Promise<void> {
const url = `${this.baseUrl}/billing/topup-axon-confirmed`;
Owner

А че на клиента не перешел то здесь? КОторый генерируется провайдером.

А че на клиента не перешел то здесь? КОторый генерируется провайдером.
@@ -269,3 +268,1 @@
await this.transact(actions);
} catch (error) {
this.logger.info('Предупреждение при выполнении транзакции powerup:', String(error));
// Epic 13 v5.1: пропускаем исключения наружу — PowerupPlugin полагается на
Owner

аналогично про эпики

аналогично про эпики
@@ -273,0 +282,4 @@
return txId as string;
}
public async packagePowerUp(
Owner

Чет не понятно здесь. Ты этот поверап делаешь на сумму 1500 руб? Т.е. конвертируешь 1500 руб это понятно, но поверап то должен быть на сумму 5 AXON а не на всю сумму. Как ты делаешь сейчас??

Чет не понятно здесь. Ты этот поверап делаешь на сумму 1500 руб? Т.е. конвертируешь 1500 руб это понятно, но поверап то должен быть на сумму 5 AXON а не на всю сумму. Как ты делаешь сейчас??
@@ -0,0 +30,4 @@
)
BaseInput(
v-model.number="form.cooldownMinutes"
label="Cooldown между докупками (мин)"
Owner

НЕТ ДОКУПОК! И НЕТ ПОКУПОК! ЕСТЬ АРЕНДА ВЫЧИСЛИТЕЛЬНЫХ РЕСУРСОВ.

НЕТ ДОКУПОК! И НЕТ ПОКУПОК! ЕСТЬ АРЕНДА ВЫЧИСЛИТЕЛЬНЫХ РЕСУРСОВ.
Author
Owner

Спасибо за ревью, отвечаю по пунктам (Epic 13 — проект «Облачный провайдер»). По коду внесу отдельным фикс-коммитом; здесь — ответы по сути и вопрос по №8.

№2 (сумма PowerUp). Суммы разделены, powerup НЕ на всю сумму. packagePowerUp(coopname, rubAmount, axonQuantity, hash) шлёт две action атомарно: billing::converttoaxn на rubAmount и eosio::powerup на axonQuantity. Здесь axonQuantity = dailyPackageSize AXON (напр. 5 AXON) — это и есть размер дневной квоты; rubAmount = dailyPackageSize × 10 ₽ (10 ₽ = 1 AXON) — ровно столько членского, сколько нужно под эти AXON. А 1500 ₽ — это monthly_quota_rub / monthlyRubCap, месячный потолок (runaway-guard), не разовая аренда. Т.е. powerup идёт на ~5 AXON, а конвертируется ~50 ₽, не 1500.

№8 (клиент провайдера). Согласен — заменю ручной axios в billing-provider.client.ts на генерируемый провайдером клиент (типобезопасность контракта). Уточни: использовать публикуемый пакет клиента провайдера (components/client / @coopenomics/provider-client)? Он покрывает billing-эндпоинты (/billing/topup-axon-confirmed, /subscriptions/billing-summary)? Если да — перейду на него.

№3, №4 (терминология). Принято: «аренда вычислительных ресурсов», не «покупка/докупка». Поправлю user-facing тексты (desktop SettingsForm + describeField labels/notes) и комментарии. Технические идентификаторы (on-chain converttoaxn, endpoint topup-axon-confirmed, имена методов) оставлю — это контракт с провайдером/чейном, не видно пользователю.

№5 (visible). Поле настройки → visible: false + default 0 (чтобы оператор не редактировал и не ломал).

№6 (RUB из config). Уберу хардкод RUB, возьму символ валюты из config (root_govern_symbol) — на случай смены символа.

№1, №7 (эпики + проект). Понял и запомнил: при ссылке на эпик всегда указываю проект, т.к. номера пересекаются между проектами. Поправлю комментарии «Epic 13» → «Epic 13 (проект «Облачный провайдер»)».

Спасибо за ревью, отвечаю по пунктам (Epic 13 — проект «Облачный провайдер»). По коду внесу отдельным фикс-коммитом; здесь — ответы по сути и вопрос по №8. **№2 (сумма PowerUp).** Суммы разделены, powerup НЕ на всю сумму. `packagePowerUp(coopname, rubAmount, axonQuantity, hash)` шлёт две action атомарно: `billing::converttoaxn` на `rubAmount` и `eosio::powerup` на `axonQuantity`. Здесь `axonQuantity = dailyPackageSize` AXON (напр. 5 AXON) — это и есть размер дневной квоты; `rubAmount = dailyPackageSize × 10` ₽ (10 ₽ = 1 AXON) — ровно столько членского, сколько нужно под эти AXON. А 1500 ₽ — это `monthly_quota_rub` / `monthlyRubCap`, **месячный потолок** (runaway-guard), не разовая аренда. Т.е. powerup идёт на ~5 AXON, а конвертируется ~50 ₽, не 1500. **№8 (клиент провайдера).** Согласен — заменю ручной `axios` в `billing-provider.client.ts` на генерируемый провайдером клиент (типобезопасность контракта). Уточни: использовать публикуемый пакет клиента провайдера (`components/client` / `@coopenomics/provider-client`)? Он покрывает billing-эндпоинты (`/billing/topup-axon-confirmed`, `/subscriptions/billing-summary`)? Если да — перейду на него. **№3, №4 (терминология).** Принято: «аренда вычислительных ресурсов», не «покупка/докупка». Поправлю user-facing тексты (desktop `SettingsForm` + `describeField` labels/notes) и комментарии. Технические идентификаторы (on-chain `converttoaxn`, endpoint `topup-axon-confirmed`, имена методов) оставлю — это контракт с провайдером/чейном, не видно пользователю. **№5 (visible).** Поле настройки → `visible: false` + default 0 (чтобы оператор не редактировал и не ломал). **№6 (RUB из config).** Уберу хардкод `RUB`, возьму символ валюты из config (`root_govern_symbol`) — на случай смены символа. **№1, №7 (эпики + проект).** Понял и запомнил: при ссылке на эпик всегда указываю проект, т.к. номера пересекаются между проектами. Поправлю комментарии «Epic 13» → «Epic 13 (проект «Облачный провайдер»)».
claude added 1 commit 2026-06-02 07:34:18 +00:00
Epic 13 (проект «Облачный провайдер»). По замечаниям ревью #40:
- терминология: «докупка/покупка» → «аренда вычислительных ресурсов» (SettingsForm + describeField labels);
- visible:false для subscriptionId (оператор не редактирует, default 0);
- хардкод RUB → символ из config (root_govern_symbol) в labels module + SettingsForm;
- комментарии Epic 13 теперь указывают проект «Облачный провайдер»;
- billing-provider.client: TODO+обоснование почему axios (provider-client alpha без billing-эндпоинтов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

Фиксы внесены коммитом b51e9e0b435:

  • №3,4: терминология «аренда вычислительных ресурсов» (SettingsForm + describeField labels);
  • №5: subscriptionIdvisible:false, default 0;
  • №6: хардкод RUB → символ из config (root_govern_symbol) в labels (module + SettingsForm);
  • №1,7: комментарии Epic 13 теперь с проектом «Облачный провайдер»;
  • №8: billing-provider.client — пока axios; добавлен TODO+обоснование (опубликованный @coopenomics/provider-client alpha 2025.11 не содержит billing-эндпоинтов Epic 13; миграция = regen+publish клиента из актуального swagger, отдельная задача). Если есть свежесобранный клиент — скажи, перейду.
  • №2: ответ выше — powerup идёт на axonQuantity (dailyPackageSize AXON), 1500 = месячный потолок.
Фиксы внесены коммитом `b51e9e0b435`: - №3,4: терминология «аренда вычислительных ресурсов» (SettingsForm + describeField labels); - №5: `subscriptionId` → `visible:false`, default 0; - №6: хардкод `RUB` → символ из config (`root_govern_symbol`) в labels (module + SettingsForm); - №1,7: комментарии Epic 13 теперь с проектом «Облачный провайдер»; - №8: `billing-provider.client` — пока axios; добавлен TODO+обоснование (опубликованный `@coopenomics/provider-client` alpha 2025.11 не содержит billing-эндпоинтов Epic 13; миграция = regen+publish клиента из актуального swagger, отдельная задача). Если есть свежесобранный клиент — скажи, перейду. - №2: ответ выше — powerup идёт на `axonQuantity` (dailyPackageSize AXON), 1500 = месячный потолок.
ant added 4 commits 2026-06-04 05:03:38 +00:00
Epic 14: типобезопасные workflow в @coopenomics/notifications для напоминаний
пайщикам о предстоящей оплате и смене статуса подписки. Шаги email+in-app+push,
payload по zod, шаблоны на {{payload.*}} (без Handlebars-ветвлений). Регистрация
в allWorkflows. Триггер — провайдер через coopback (Вариант А), Story 14.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Epic 14: входящий REST-канал /billing-notifications (ServerSecretGuard по общему
SERVER_SECRET, симметрично провайдеру). DTO {kind,coopname,payload}; bridge маппит
стабильный kind → Workflows.X.id и шлёт через NotificationSenderService (он сам
резолвит subscriber_id/email по coopname). coopName в payload обогащается из
getDisplayName. Ошибки Novu не пробрасываются — провайдеру важен факт приёма.

Зависит от пересборки @coopenomics/notifications (Story 14.1) — Workflows.* id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ревью PR mono#56 (finding #1): провайдер теперь шлёт биллинговые оповещения
через существующую типизированную мутацию triggerNotificationWorkflow
(application/notification), поэтому отдельный REST-эндпоинт
/billing-notifications на coopback больше не нужен — это был дубль уже
имеющейся инфры (NotificationSenderService + Novu).

Удалено целиком application/billing-notification/ (controller + guard + dto +
bridge service + module) и его регистрация в app.module. Workflow-определения
Epic 14 (trial-ending / subscription-payment-due / past-due / suspended) и
NotificationSenderService остаются — провайдер адресует их по Workflows.X.id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #56
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
claude added 2 commits 2026-06-11 06:16:15 +00:00
- billing::pay: require_auth(coopname) → require_auth(_provider): рекуррентные
  списания авторизует оператор платформы; ключей кооперативов-спиц в vault
  хаба нет и быть не должно.
- Таблица payments (scope=_billing): реестр проведённых payment_hash.
  Повтор pay/converttoaxn с тем же hash отклоняется assert'ом — упавший парсер
  или backend между transact и callback провайдеру больше не приводят к
  повторным списаниям. RAM ограничен скользящим окном (TTL 90д, gc ≤8/такт).
- Терминология convert: заявление = «прошу сконвертировать паевой взнос в
  членский», членский целевой — отдельных распоряжений на списания не требует.
- cooptypes: IPayment приведён к фактической структуре таблицы.

Требует пересборки billing.wasm/ABI перед деплоем.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BillingCronService: перед on-chain pay создаёт PENDING-invoice у провайдера
  (POST /billing/invoice — раньше invoice никто не создавал и подтверждение
  падало на 'Invoice не найден'), подтверждает на POST /billing/payment-confirmed
  (прежний /subscriptions/confirm-batch-payment на провайдере выпилен в v5).
  Ошибка anti-replay контракта = оплата уже в чейне → доносим подтверждение
  без повторного списания.
- BillingPaymentListener: второй (реактивный) путь подтверждения по событию
  парсера action::billing::pay — переживает падение backend'а между transact
  и callback; провайдер идемпотентен по payment_hash.
- BillingBlockchainAdapter.pay: подписывает оператор (config.coopname узла-хаба,
  = _provider контрактов), не кооператив-спица.
- BILLING_CRON_PAYER удалён: пайщик-плательщик = сам кооператив (w.wal.bill его),
  молчаливый пропуск тика по пустому конфигу больше невозможен.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Правки по итогам ревью потока оплат (2026-06-11), коммиты 6047b… (контракт) + 9463ab7 (coopback):

  1. On-chain anti-replay: таблица payments в контракте billing — повтор payment_hash в pay/converttoaxn отклоняется. Зависший парсер / упавший backend больше не приводят к повторным списаниям средств. RAM — скользящее окно (TTL 90д, gc ≤8/транзакция).
  2. billing::pay авторизует _provider (Восход): рекуррентные списания подписывает оператор своим ключом; ключи спиц на хабе не нужны.
  3. Цепочка подтверждения: cron теперь summary → POST /billing/invoice → pay → POST /billing/payment-confirmed (раньше confirm бил в выпиленный endpoint, а invoice никто не создавал — подписки не продлялись). Ошибка anti-replay = «уже оплачено» → доносим подтверждение без повторного списания.
  4. BillingPaymentListener: реактивный второй путь подтверждения по событию парсера action::billing::pay (идемпотентно у провайдера).
  5. BILLING_CRON_PAYER выпилен (рудимент: пустой конфиг молча отключал биллинг).

⚠️ Контракт требует пересборки wasm/ABI. Остаётся архитектурный блокер e2e: scope-разрыв w.wal.bill (конвертация на хабе кладёт в bill@voskhod, расход смотрит bill@спица) — схема и целевая модель v5.2 в blago проект 43, требование d9 «Поток оплаты подписок».

Правки по итогам ревью потока оплат (2026-06-11), коммиты 6047b… (контракт) + 9463ab7 (coopback): 1. **On-chain anti-replay**: таблица `payments` в контракте billing — повтор `payment_hash` в `pay`/`converttoaxn` отклоняется. Зависший парсер / упавший backend больше не приводят к повторным списаниям средств. RAM — скользящее окно (TTL 90д, gc ≤8/транзакция). 2. **`billing::pay` авторизует `_provider`** (Восход): рекуррентные списания подписывает оператор своим ключом; ключи спиц на хабе не нужны. 3. **Цепочка подтверждения**: cron теперь `summary → POST /billing/invoice → pay → POST /billing/payment-confirmed` (раньше confirm бил в выпиленный endpoint, а invoice никто не создавал — подписки не продлялись). Ошибка anti-replay = «уже оплачено» → доносим подтверждение без повторного списания. 4. **BillingPaymentListener**: реактивный второй путь подтверждения по событию парсера `action::billing::pay` (идемпотентно у провайдера). 5. **BILLING_CRON_PAYER выпилен** (рудимент: пустой конфиг молча отключал биллинг). ⚠️ Контракт требует пересборки wasm/ABI. Остаётся архитектурный блокер e2e: scope-разрыв `w.wal.bill` (конвертация на хабе кладёт в bill@voskhod, расход смотрит bill@спица) — схема и целевая модель v5.2 в blago проект 43, требование d9 «Поток оплаты подписок».
claude added 1 commit 2026-06-11 06:30:55 +00:00
Решение @ant 2026-06-11: биллинг живёт в леджере оператора (_provider), где
каждый кооператив-пайщик — обычный username. COOPERATIVE-вариант Epic 13 не
смыкал поток: конвертация на хабе пополняла bill@voskhod, а расход
(converttoaxn спицы, pay cron'а) искал bill@спица.

- ledger2 wallets: BILLING_FUND_PAY → USER_SHARED (+program-mapping 1 «ЦК»);
- contract converttoaxn: Ledger2::apply со scope=_provider, L3-разрез по
  coopname-пайщику; подпись спицы (coopname@active) сохранена — payload
  PowerupPlugin и cooptypes не меняются;
- contract pay/convert: семантика параметров задокументирована (coopname =
  кооператив-оператор, username = пайщик; для спиц username = их coopname);
  pay дополнительно проверяет username по реестру кооперативов;
- coopback cron: pay шлётся с coopname=config.coopname (оператор), username=спица.

Требует пересборки billing.wasm/ABI и ledger2-потребителей.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude added 1 commit 2026-06-11 08:11:30 +00:00
Решение @ant 2026-06-11: контракт billing снова без таблиц (RAM чейна конечна,
платежи в ней не хранятся) — таблица payments и anti-replay assert удалены.

Идемпотентность переезжает в PG оператора (BillingPaymentLogService,
billing_payment_log, payment_hash UNIQUE):
- запись SUBMITTING создаётся ДО transact — повтор payment_hash заблокирован
  при любом сценарии (зависший парсер, упавший backend, наложение тиков);
- SUBMITTED после принятия нодой, CONFIRMED после callback провайдеру;
- доменный отказ ноды → FAILED (повтор безопасен, CAS-переход);
- сетевая ошибка transact → остаётся SUBMITTING: автоповтор запрещён до сверки
  с чейном, листенер парсера доведёт до CONFIRMED, если pay попал в блок;
- BillingPaymentListener дозакрывает зависшие записи по событию парсера.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude added 1 commit 2026-06-11 09:22:28 +00:00
- контракт converttoaxn: require_auth(_provider) вместо coopname — членские
  взносы лежат в леджере оператора, транзакции по своему леджеру подписывает
  сам кооператив-оператор
- hub-cron (BillingCronService.processPackageTopup): мониторит ликвидный
  AXON-баланс спиц (порог BILLING_PACKAGE_LOW_WATER_AXON, дефолт 15) и при
  исчерпании докупает пакет через provider package-invoice + PG-журнал
  идемпотентности (тот же протокол, что и time-поток pay)
- BillingConversionListener дозакрывает SUBMITTING-записи журнала
- PowerupPlugin спицы: только eosio::powerup из своего AXON-баланса; вся
  package-нога (converttoaxn, payment_hash, runaway-guards, calendar-trigger,
  pre-burn recovery) удалена — у спицы нет прав на членские взносы
- packagePowerUp удалён из порта и BlockchainService; добавлен
  getCurrencyBalance (get_currency_balance) для мониторинга AXON
claude added 1 commit 2026-06-11 09:50:42 +00:00
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/epic-13-billing-package-powerup:feat/epic-13-billing-package-powerup
git checkout feat/epic-13-billing-package-powerup
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: C9S/mono#49