payment-service.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { selPool, updPool } from "#db/db.js";
  2. import { DatabaseTransactionConnection, sql } from "slonik";
  3. import {
  4. IPaymentProvider,
  5. CreatePaymentParams,
  6. CreateRefundParams,
  7. RefundResponse,
  8. PaymentStatusResponse,
  9. CancelPaymentParams,
  10. CancelPaymentResponse,
  11. } from "./payment-provider-types.js";
  12. import { v7 as uuidv7 } from "uuid";
  13. import { logger } from "#plugins/logger.js";
  14. import { dayjs } from "#plugins/dayjs.js";
  15. import { DbSchema } from "#db/db-schema.js";
  16. import { z } from "zod";
  17. import { PaymentProviderError } from "./shop-errors.js";
  18. export class PaymentService {
  19. private provider: IPaymentProvider;
  20. constructor(provider: IPaymentProvider) {
  21. this.provider = provider;
  22. }
  23. async createPayment(
  24. tr: DatabaseTransactionConnection,
  25. params: CreatePaymentParams,
  26. ) {
  27. const paymentId = uuidv7();
  28. logger.info(`Создание платежа: ${paymentId}...`);
  29. const paymentResponse = await this.provider.createPayment(
  30. params,
  31. paymentId,
  32. );
  33. const payment = await tr.one(sql.type(
  34. z.object({
  35. paymentId: DbSchema.shop.payments.paymentId,
  36. orderId: DbSchema.shop.payments.orderId,
  37. userId: DbSchema.shop.payments.userId,
  38. amount: DbSchema.shop.payments.amount,
  39. currencyCode: DbSchema.shop.payments.currencyCode,
  40. paymentMethod: DbSchema.shop.payments.paymentMethod,
  41. bank: DbSchema.shop.payments.bank,
  42. status: DbSchema.shop.payments.status,
  43. externalTransactionId: DbSchema.shop.payments.externalTransactionId,
  44. paymentGatewayDetails: DbSchema.shop.payments.paymentGatewayDetails,
  45. createdAt: DbSchema.shop.payments.createdAt,
  46. updatedAt: DbSchema.shop.payments.updatedAt,
  47. confirmation: DbSchema.shop.payments.confirmation,
  48. }),
  49. )`
  50. insert into shop.payments (
  51. payment_id,
  52. order_id,
  53. user_id,
  54. amount,
  55. currency_code,
  56. payment_method,
  57. bank,
  58. status,
  59. external_transaction_id,
  60. payment_gateway_details,
  61. created_at,
  62. confirmation
  63. ) values (
  64. ${paymentId},
  65. ${params.orderId},
  66. ${params.userId},
  67. ${paymentResponse.amount.value},
  68. ${paymentResponse.amount.currency},
  69. 'CARD',
  70. ${params.bank},
  71. 'PENDING',
  72. ${paymentResponse.id},
  73. ${sql.jsonb(paymentResponse.providerSpecificDetails)},
  74. ${paymentResponse.createdAt},
  75. ${paymentResponse.confirmation ? sql.jsonb(paymentResponse.confirmation) : null}
  76. ) returning
  77. payment_id as "paymentId",
  78. order_id as "orderId",
  79. user_id as "userId",
  80. amount::float as "amount",
  81. currency_code as "currencyCode",
  82. payment_method as "paymentMethod",
  83. bank,
  84. status,
  85. external_transaction_id as "externalTransactionId",
  86. payment_gateway_details as "paymentGatewayDetails",
  87. created_at as "createdAt",
  88. updated_at as "updatedAt",
  89. confirmation
  90. `);
  91. const paymentDueDate = dayjs().add(1, "hour").toDate();
  92. await tr.query(sql.unsafe`
  93. update shop.orders
  94. set status = 'PENDING_PAYMENT',
  95. payment_due_date = ${paymentDueDate.toISOString()}
  96. where order_id = ${params.orderId}
  97. `);
  98. return payment;
  99. }
  100. async getPaymentStatus(paymentId: string): Promise<PaymentStatusResponse> {
  101. return this.provider.getPaymentStatus(paymentId);
  102. }
  103. async refundPayment(params: CreateRefundParams): Promise<RefundResponse> {
  104. // Аналогично, можно добавить логику
  105. const idempotencyKey = uuidv7();
  106. console.log(
  107. `Creating refund for payment ${params.paymentId} with idempotency key: ${idempotencyKey}`,
  108. );
  109. const refund = await this.provider.createRefund(params, idempotencyKey);
  110. if (refund.status !== "succeeded") {
  111. throw new PaymentProviderError(
  112. `Refund failed for payment ${params.paymentId}`,
  113. refund,
  114. );
  115. }
  116. await updPool.query(sql.unsafe`
  117. update shop.payments
  118. set status = 'REFUNDED'
  119. where payment_id = ${params.paymentId}
  120. `);
  121. return refund;
  122. }
  123. // Новая функция для отмены платежа
  124. async cancelPayment(
  125. // tr: DatabaseTransactionConnection,
  126. paymentIdToCancel: string,
  127. ): Promise<CancelPaymentResponse> {
  128. const idempotencyKey = uuidv7();
  129. logger.info(
  130. `Попытка отмены платежа: ${paymentIdToCancel} с ключом идемпотентности: ${idempotencyKey}`,
  131. );
  132. // Получаем order_id перед отменой, чтобы обновить заказ
  133. const paymentDetails = await selPool.maybeOne(sql.type(
  134. z.object({
  135. orderId: DbSchema.shop.payments.orderId,
  136. status: DbSchema.shop.payments.status,
  137. }),
  138. )`
  139. select order_id as "orderId", status from shop.payments where payment_id = ${paymentIdToCancel}
  140. `);
  141. if (!paymentDetails) {
  142. logger.error(`Платеж ${paymentIdToCancel} не найден для отмены.`);
  143. throw new PaymentProviderError(
  144. `Payment with ID ${paymentIdToCancel} not found.`,
  145. { paymentId: paymentIdToCancel },
  146. );
  147. }
  148. if (paymentDetails.status === "CANCELED") {
  149. logger.warn(`Платеж ${paymentIdToCancel} уже отменен.`);
  150. // Можно вернуть текущий статус или специальный ответ
  151. // Для простоты, запросим статус у провайдера, чтобы вернуть актуальные данные
  152. return this.provider.getPaymentStatus(paymentIdToCancel);
  153. }
  154. if (paymentDetails.status === "SUCCEEDED") {
  155. logger.error(
  156. `Платеж ${paymentIdToCancel} уже успешно выполнен и не может быть отменен. Используйте возврат.`,
  157. );
  158. throw new PaymentProviderError(
  159. `Payment ${paymentIdToCancel} is already succeeded and cannot be canceled. Use refund instead.`,
  160. );
  161. }
  162. const cancelParams: CancelPaymentParams = { paymentId: paymentIdToCancel };
  163. const cancelResponse = await this.provider.cancelPayment(
  164. cancelParams,
  165. idempotencyKey,
  166. );
  167. logger.info(
  168. `Ответ от провайдера по отмене платежа ${paymentIdToCancel}: статус ${cancelResponse.status}`,
  169. );
  170. // Обновляем статус в нашей БД, если провайдер подтвердил отмену или платеж уже был отменен у провайдера
  171. if (cancelResponse.status === "canceled") {
  172. await updPool.query(sql.unsafe`
  173. update shop.payments
  174. set status = 'CANCELED'
  175. where payment_id = ${paymentIdToCancel}
  176. `);
  177. logger.info(
  178. `Статус платежа ${paymentIdToCancel} обновлен на CANCELED в БД.`,
  179. );
  180. if (paymentDetails.orderId) {
  181. await updPool.query(sql.unsafe`
  182. update shop.orders
  183. set status = 'CANCELED'
  184. where order_id = ${paymentDetails.orderId}
  185. and status = 'PENDING_PAYMENT' -- Обновляем только если заказ ожидал оплату
  186. `);
  187. logger.info(
  188. `Статус заказа ${paymentDetails.orderId} обновлен на CANCELED в БД.`,
  189. );
  190. }
  191. } else {
  192. // Если провайдер не вернул 'canceled', но и не выбросил ошибку (маловероятно для API отмены, но для полноты)
  193. // Можно обновить статус на тот, что вернул провайдер
  194. await updPool.query(sql.unsafe`
  195. update shop.payments
  196. set status = 'FAILED'
  197. where payment_id = ${paymentIdToCancel}
  198. `);
  199. logger.warn(
  200. `Платеж ${paymentIdToCancel} не был отменен провайдером, текущий статус от провайдера: ${cancelResponse.status}. Статус в БД обновлен.`,
  201. );
  202. }
  203. return cancelResponse;
  204. }
  205. async waitPayment(paymentId: string, endDate: string) {
  206. const payment = await selPool.maybeOne(sql.type(
  207. z.object({
  208. paymentId: DbSchema.shop.payments.paymentId,
  209. orderId: DbSchema.shop.payments.orderId,
  210. status: DbSchema.shop.payments.status,
  211. externalTransactionId: DbSchema.shop.payments.externalTransactionId,
  212. }),
  213. )`
  214. select payment_id as "paymentId",
  215. order_id as "orderId",
  216. status,
  217. external_transaction_id as "externalTransactionId"
  218. from shop.payments
  219. where payment_id = ${paymentId}
  220. `);
  221. if (!payment) {
  222. logger.error(`Платеж ${paymentId} не найден.`);
  223. throw new PaymentProviderError(
  224. `Payment with ID ${paymentId} not found.`,
  225. { paymentId },
  226. );
  227. }
  228. if (!payment.externalTransactionId) {
  229. logger.error(`externalTransactionId платежа ${paymentId} не найден.`);
  230. throw new PaymentProviderError(
  231. `externalTransactionId платежа ${paymentId} не найден.`,
  232. { paymentId },
  233. );
  234. }
  235. do {
  236. logger.info(`Ожидание платежа: ${paymentId}...`);
  237. const paymentStatus = await this.getPaymentStatus(
  238. payment.externalTransactionId,
  239. );
  240. logger.info(`Статус платежа: ${paymentStatus.status}`);
  241. if (
  242. paymentStatus.status === "succeeded" ||
  243. paymentStatus.status === "waiting_for_capture"
  244. ) {
  245. await updPool.query(sql.unsafe`
  246. update shop.payments
  247. set status = 'SUCCEEDED'
  248. where payment_id = ${paymentId}
  249. `);
  250. return "succeeded";
  251. }
  252. if (paymentStatus.status === "canceled") {
  253. logger.info(`Платеж отменен провайдером: ${paymentId}`);
  254. await updPool.query(sql.unsafe`
  255. update shop.payments
  256. set status = 'CANCELED'
  257. where payment_id = ${paymentId}
  258. `);
  259. return "canceled";
  260. }
  261. if (paymentStatus.status === "pending") {
  262. await new Promise((resolve) => setTimeout(resolve, 5000));
  263. continue;
  264. }
  265. logger.error(
  266. `Неизвестный статус платежа ${paymentId}: ${paymentStatus.status}`,
  267. );
  268. return "failed";
  269. } while (dayjs().isBefore(endDate));
  270. logger.info(`Платеж отменен из-за времени ожидания: ${paymentId}`);
  271. await this.cancelPayment(paymentId);
  272. await updPool.query(sql.unsafe`
  273. update shop.payments
  274. set status = 'CANCELED'
  275. where payment_id = ${paymentId}
  276. `);
  277. return "canceled";
  278. }
  279. }