123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- import { selPool, updPool } from "#db/db.js";
- import { DatabaseTransactionConnection, sql } from "slonik";
- import {
- IPaymentProvider,
- CreatePaymentParams,
- CreateRefundParams,
- RefundResponse,
- PaymentStatusResponse,
- CancelPaymentParams,
- CancelPaymentResponse,
- } from "./payment-provider-types.js";
- import { v7 as uuidv7 } from "uuid";
- import { logger } from "#plugins/logger.js";
- import { dayjs } from "#plugins/dayjs.js";
- import { DbSchema } from "#db/db-schema.js";
- import { z } from "zod";
- import { PaymentProviderError } from "./shop-errors.js";
- export class PaymentService {
- private provider: IPaymentProvider;
- constructor(provider: IPaymentProvider) {
- this.provider = provider;
- }
- async createPayment(
- tr: DatabaseTransactionConnection,
- params: CreatePaymentParams,
- ) {
- const paymentId = uuidv7();
- logger.info(`Создание платежа: ${paymentId}...`);
- const paymentResponse = await this.provider.createPayment(
- params,
- paymentId,
- );
- const payment = await tr.one(sql.type(
- z.object({
- paymentId: DbSchema.shop.payments.paymentId,
- orderId: DbSchema.shop.payments.orderId,
- userId: DbSchema.shop.payments.userId,
- amount: DbSchema.shop.payments.amount,
- currencyCode: DbSchema.shop.payments.currencyCode,
- paymentMethod: DbSchema.shop.payments.paymentMethod,
- bank: DbSchema.shop.payments.bank,
- status: DbSchema.shop.payments.status,
- externalTransactionId: DbSchema.shop.payments.externalTransactionId,
- paymentGatewayDetails: DbSchema.shop.payments.paymentGatewayDetails,
- createdAt: DbSchema.shop.payments.createdAt,
- updatedAt: DbSchema.shop.payments.updatedAt,
- confirmation: DbSchema.shop.payments.confirmation,
- }),
- )`
- insert into shop.payments (
- payment_id,
- order_id,
- user_id,
- amount,
- currency_code,
- payment_method,
- bank,
- status,
- external_transaction_id,
- payment_gateway_details,
- created_at,
- confirmation
- ) values (
- ${paymentId},
- ${params.orderId},
- ${params.userId},
- ${paymentResponse.amount.value},
- ${paymentResponse.amount.currency},
- 'CARD',
- ${params.bank},
- 'PENDING',
- ${paymentResponse.id},
- ${sql.jsonb(paymentResponse.providerSpecificDetails)},
- ${paymentResponse.createdAt},
- ${paymentResponse.confirmation ? sql.jsonb(paymentResponse.confirmation) : null}
- ) returning
- payment_id as "paymentId",
- order_id as "orderId",
- user_id as "userId",
- amount::float as "amount",
- currency_code as "currencyCode",
- payment_method as "paymentMethod",
- bank,
- status,
- external_transaction_id as "externalTransactionId",
- payment_gateway_details as "paymentGatewayDetails",
- created_at as "createdAt",
- updated_at as "updatedAt",
- confirmation
- `);
- const paymentDueDate = dayjs().add(1, "hour").toDate();
- await tr.query(sql.unsafe`
- update shop.orders
- set status = 'PENDING_PAYMENT',
- payment_due_date = ${paymentDueDate.toISOString()}
- where order_id = ${params.orderId}
- `);
- return payment;
- }
- async getPaymentStatus(paymentId: string): Promise<PaymentStatusResponse> {
- return this.provider.getPaymentStatus(paymentId);
- }
- async refundPayment(params: CreateRefundParams): Promise<RefundResponse> {
- // Аналогично, можно добавить логику
- const idempotencyKey = uuidv7();
- console.log(
- `Creating refund for payment ${params.paymentId} with idempotency key: ${idempotencyKey}`,
- );
- const refund = await this.provider.createRefund(params, idempotencyKey);
- if (refund.status !== "succeeded") {
- throw new PaymentProviderError(
- `Refund failed for payment ${params.paymentId}`,
- refund,
- );
- }
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'REFUNDED'
- where payment_id = ${params.paymentId}
- `);
- return refund;
- }
- // Новая функция для отмены платежа
- async cancelPayment(
- // tr: DatabaseTransactionConnection,
- paymentIdToCancel: string,
- ): Promise<CancelPaymentResponse> {
- const idempotencyKey = uuidv7();
- logger.info(
- `Попытка отмены платежа: ${paymentIdToCancel} с ключом идемпотентности: ${idempotencyKey}`,
- );
- // Получаем order_id перед отменой, чтобы обновить заказ
- const paymentDetails = await selPool.maybeOne(sql.type(
- z.object({
- orderId: DbSchema.shop.payments.orderId,
- status: DbSchema.shop.payments.status,
- }),
- )`
- select order_id as "orderId", status from shop.payments where payment_id = ${paymentIdToCancel}
- `);
- if (!paymentDetails) {
- logger.error(`Платеж ${paymentIdToCancel} не найден для отмены.`);
- throw new PaymentProviderError(
- `Payment with ID ${paymentIdToCancel} not found.`,
- { paymentId: paymentIdToCancel },
- );
- }
- if (paymentDetails.status === "CANCELED") {
- logger.warn(`Платеж ${paymentIdToCancel} уже отменен.`);
- // Можно вернуть текущий статус или специальный ответ
- // Для простоты, запросим статус у провайдера, чтобы вернуть актуальные данные
- return this.provider.getPaymentStatus(paymentIdToCancel);
- }
- if (paymentDetails.status === "SUCCEEDED") {
- logger.error(
- `Платеж ${paymentIdToCancel} уже успешно выполнен и не может быть отменен. Используйте возврат.`,
- );
- throw new PaymentProviderError(
- `Payment ${paymentIdToCancel} is already succeeded and cannot be canceled. Use refund instead.`,
- );
- }
- const cancelParams: CancelPaymentParams = { paymentId: paymentIdToCancel };
- const cancelResponse = await this.provider.cancelPayment(
- cancelParams,
- idempotencyKey,
- );
- logger.info(
- `Ответ от провайдера по отмене платежа ${paymentIdToCancel}: статус ${cancelResponse.status}`,
- );
- // Обновляем статус в нашей БД, если провайдер подтвердил отмену или платеж уже был отменен у провайдера
- if (cancelResponse.status === "canceled") {
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'CANCELED'
- where payment_id = ${paymentIdToCancel}
- `);
- logger.info(
- `Статус платежа ${paymentIdToCancel} обновлен на CANCELED в БД.`,
- );
- if (paymentDetails.orderId) {
- await updPool.query(sql.unsafe`
- update shop.orders
- set status = 'CANCELED'
- where order_id = ${paymentDetails.orderId}
- and status = 'PENDING_PAYMENT' -- Обновляем только если заказ ожидал оплату
- `);
- logger.info(
- `Статус заказа ${paymentDetails.orderId} обновлен на CANCELED в БД.`,
- );
- }
- } else {
- // Если провайдер не вернул 'canceled', но и не выбросил ошибку (маловероятно для API отмены, но для полноты)
- // Можно обновить статус на тот, что вернул провайдер
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'FAILED'
- where payment_id = ${paymentIdToCancel}
- `);
- logger.warn(
- `Платеж ${paymentIdToCancel} не был отменен провайдером, текущий статус от провайдера: ${cancelResponse.status}. Статус в БД обновлен.`,
- );
- }
- return cancelResponse;
- }
- async waitPayment(paymentId: string, endDate: string) {
- const payment = await selPool.maybeOne(sql.type(
- z.object({
- paymentId: DbSchema.shop.payments.paymentId,
- orderId: DbSchema.shop.payments.orderId,
- status: DbSchema.shop.payments.status,
- externalTransactionId: DbSchema.shop.payments.externalTransactionId,
- }),
- )`
- select payment_id as "paymentId",
- order_id as "orderId",
- status,
- external_transaction_id as "externalTransactionId"
- from shop.payments
- where payment_id = ${paymentId}
- `);
- if (!payment) {
- logger.error(`Платеж ${paymentId} не найден.`);
- throw new PaymentProviderError(
- `Payment with ID ${paymentId} not found.`,
- { paymentId },
- );
- }
- if (!payment.externalTransactionId) {
- logger.error(`externalTransactionId платежа ${paymentId} не найден.`);
- throw new PaymentProviderError(
- `externalTransactionId платежа ${paymentId} не найден.`,
- { paymentId },
- );
- }
- do {
- logger.info(`Ожидание платежа: ${paymentId}...`);
- const paymentStatus = await this.getPaymentStatus(
- payment.externalTransactionId,
- );
- logger.info(`Статус платежа: ${paymentStatus.status}`);
- if (
- paymentStatus.status === "succeeded" ||
- paymentStatus.status === "waiting_for_capture"
- ) {
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'SUCCEEDED'
- where payment_id = ${paymentId}
- `);
- return "succeeded";
- }
- if (paymentStatus.status === "canceled") {
- logger.info(`Платеж отменен провайдером: ${paymentId}`);
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'CANCELED'
- where payment_id = ${paymentId}
- `);
- return "canceled";
- }
- if (paymentStatus.status === "pending") {
- await new Promise((resolve) => setTimeout(resolve, 5000));
- continue;
- }
- logger.error(
- `Неизвестный статус платежа ${paymentId}: ${paymentStatus.status}`,
- );
- return "failed";
- } while (dayjs().isBefore(endDate));
- logger.info(`Платеж отменен из-за времени ожидания: ${paymentId}`);
- await this.cancelPayment(paymentId);
- await updPool.query(sql.unsafe`
- update shop.payments
- set status = 'CANCELED'
- where payment_id = ${paymentId}
- `);
- return "canceled";
- }
- }
|