feat(billing): PaymentProvider + Mock, self-service пополнение баланса

Шаг 0.13 плана production-готовности:
- интерфейс PaymentProvider (createPayment/verifyWebhook/getPaymentStatus)
- MockPaymentProvider с настоящим webhook (sha256-подпись, идемпотентность)
- applySucceededPayment: транзакция с guard, credit, авто-снятие billingBlocked
- роуты payments + confirm-test (только mock) + публичный webhook
- Billing.tsx: Пополнить/Подтвердить (тест), фикс setLocation в рендере
- миграция 0080 billing_payments, 13 новых тестов (78/78)
This commit is contained in:
2026-09-07 22:45:30 +03:00
parent 23c49adcd1
commit 701af06290
11 changed files with 1039 additions and 14 deletions

View File

@@ -87,6 +87,26 @@ export const billingTransactions = pgTable("billing_transactions", {
export type BillingTransaction = typeof billingTransactions.$inferSelect;
export type InsertBillingTransaction = typeof billingTransactions.$inferInsert;
// Billing payments (self-service top-ups via payment provider)
export const billingPayments = pgTable("billing_payments", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
provider: varchar("provider", { length: 30 }).notNull(),
externalId: varchar("external_id", { length: 255 }).notNull().unique("billing_payments_external_id_key"),
amount: numeric("amount", { precision: 10, scale: 2 }).notNull(),
currency: varchar("currency", { length: 10 }).notNull().default("RUB"),
status: varchar("status", { length: 20 }).notNull().default("pending"), // 'pending' | 'succeeded' | 'canceled'
idempotencyKey: varchar("idempotency_key", { length: 255 }).notNull().unique("billing_payments_idempotency_key_key"),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
}, (table) => ({
orgStatusIdx: index("billing_payments_org_status_idx").on(table.organizationId, table.status),
}));
export type BillingPayment = typeof billingPayments.$inferSelect;
export type InsertBillingPayment = typeof billingPayments.$inferInsert;
// Users table
export const users = pgTable("users", {
id: serial("id").primaryKey(),