import { createHash, randomUUID, timingSafeEqual } from "crypto"; import { eq } from "drizzle-orm"; import { db } from "../db"; import { billingPayments } from "@shared/schema"; import type { PaymentProvider, CreatePaymentParams, PaymentCreated, WebhookVerification, PaymentStatusInfo, PaymentStatus, } from "./payment-provider"; // Тестовый провайдер: платежи создаются локально в статусе pending, // подтверждение — через кнопку «Подтвердить (тест)» или настоящий webhook // с подписью sha256(externalId + ':' + secret) в заголовке x-mock-signature. const DEFAULT_SECRET = "mock-payment-dev-secret"; const WEBHOOK_STATUSES: PaymentStatus[] = ['pending', 'succeeded', 'canceled']; export function getMockSecret(): string { return process.env.MOCK_PAYMENT_SECRET || DEFAULT_SECRET; } export function signMockWebhook(externalId: string): string { return createHash("sha256").update(`${externalId}:${getMockSecret()}`).digest("hex"); } export class MockPaymentProvider implements PaymentProvider { readonly name = "mock"; async createPayment(_params: CreatePaymentParams): Promise { return { externalId: `mock_${randomUUID()}`, status: 'pending', }; } verifyWebhook(headers: Record, body: unknown): WebhookVerification { const payload = (body ?? {}) as { externalId?: unknown; status?: unknown; idempotencyKey?: unknown }; const externalId = typeof payload.externalId === "string" ? payload.externalId : undefined; if (!externalId) { return { valid: false, error: "Поле externalId отсутствует" }; } const signature = typeof headers["x-mock-signature"] === "string" ? headers["x-mock-signature"] : ""; const expected = signMockWebhook(externalId); const sigBuf = Buffer.from(signature); const expBuf = Buffer.from(expected); if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) { return { valid: false, externalId, error: "Неверная подпись webhook" }; } if (!WEBHOOK_STATUSES.includes(payload.status as PaymentStatus)) { return { valid: false, externalId, error: "Неизвестный статус платежа" }; } return { valid: true, externalId, status: payload.status as PaymentStatus, idempotencyKey: typeof payload.idempotencyKey === "string" ? payload.idempotencyKey : undefined, }; } // Статус берём из локальной таблицы платежей — внешней системы у mock нет. async getPaymentStatus(externalId: string): Promise { const [row] = await db .select({ status: billingPayments.status }) .from(billingPayments) .where(eq(billingPayments.externalId, externalId)) .limit(1); if (!row) { throw new Error(`Платёж не найден: ${externalId}`); } return { externalId, status: row.status as PaymentStatus }; } }