orders-service.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { selPool, updPool } from "#db/db.js";
  2. import { DatabaseTransactionConnection, SerializableValue, sql } from "slonik";
  3. import { v7 as uuidv7 } from "uuid";
  4. import { cartService } from "./cart/cart-service.js";
  5. import { ApiError } from "#exceptions/api-error.js";
  6. import { z } from "zod";
  7. import { DbSchema } from "#db/db-schema.js";
  8. import { CustomFieldWithValue } from "#api/v_0.1.0/types/custom-fields-types.js";
  9. import { generateRandomNumber } from "#utils/other-utils.js";
  10. import { Cart } from "./cart/cart-types.js";
  11. import { dayjs } from "#plugins/dayjs.js";
  12. import { PaymentService } from "./payment/payment-service.js";
  13. import { logger } from "#plugins/logger.js";
  14. import { YooKassaProvider } from "./payment/yookassa-provider.js";
  15. import { cActService } from "../activities/c-act-service.js";
  16. type OrderUserData = Record<string, SerializableValue> & {
  17. firstName: string;
  18. lastName: string;
  19. patronymic: string | null;
  20. email: string;
  21. phone: string;
  22. };
  23. class OrdersService {
  24. async createOrder(
  25. tr: DatabaseTransactionConnection,
  26. {
  27. cart,
  28. userId,
  29. userData,
  30. }: {
  31. cart: Cart;
  32. userId?: string;
  33. userData: OrderUserData;
  34. },
  35. ) {
  36. const products = await Promise.all(
  37. cart.items.map(async (item) => {
  38. const p = await cartService.getProductById(item.productId);
  39. if (!p) {
  40. throw ApiError.BadRequest(
  41. "productNotFound",
  42. "Не найден товар из корзины",
  43. );
  44. }
  45. if (item.priceAtAddition !== p.price) {
  46. throw ApiError.BadRequest(
  47. "productPriceChanged",
  48. "Цена товара изменилась",
  49. );
  50. }
  51. return {
  52. ...p,
  53. quantity: item.quantity,
  54. activityRegId: item.activityRegId,
  55. peMemberId: item.peMemberId,
  56. cartItemId: item.cartItemId,
  57. };
  58. }),
  59. );
  60. // order
  61. const orderId = uuidv7();
  62. let orderNumber = generateRandomNumber();
  63. while (await this.checkOrderNumber(orderNumber)) {
  64. orderNumber = generateRandomNumber();
  65. }
  66. const totalAmount = products.reduce((acc, product) => {
  67. return acc + product.price * product.quantity;
  68. }, 0);
  69. const paymentDueDate = dayjs().add(1, "hour").toDate();
  70. await tr.query(sql.unsafe`
  71. insert into shop.orders (
  72. order_id,
  73. order_number,
  74. user_id,
  75. status,
  76. total_amount,
  77. currency_code,
  78. billing_data_snapshot,
  79. payment_due_date
  80. )
  81. values (
  82. ${orderId},
  83. ${orderNumber},
  84. ${userId || null},
  85. 'PENDING_PAYMENT',
  86. ${totalAmount},
  87. 'RUB',
  88. ${sql.jsonb(userData)},
  89. ${paymentDueDate.toISOString()}
  90. )
  91. `);
  92. for (const product of products) {
  93. await tr.query(sql.unsafe`
  94. insert into shop.order_items (
  95. order_item_id,
  96. order_id,
  97. product_id,
  98. quantity,
  99. unit_price,
  100. total_price,
  101. activity_reg_id,
  102. pe_member_id,
  103. attributes_snapshot
  104. )
  105. values (
  106. ${uuidv7()},
  107. ${orderId},
  108. ${product.productId},
  109. ${product.quantity},
  110. ${product.price},
  111. ${product.price * product.quantity},
  112. ${"activityRegId" in product ? product.activityRegId : null},
  113. ${"peMemberId" in product ? product.peMemberId : null},
  114. ${sql.jsonb(product.attributes)}
  115. )
  116. `);
  117. // чтобы скрыть из корзины
  118. await tr.query(sql.unsafe`
  119. update shop.cart_items set order_id = ${orderId} where cart_item_id = ${product.cartItemId}
  120. `);
  121. }
  122. return {
  123. orderId,
  124. orderNumber,
  125. totalAmount,
  126. currencyCode: "RUB",
  127. billingDataSnapshot: userData,
  128. paymentDueDate,
  129. items: products,
  130. };
  131. }
  132. private async checkOrderNumber(orderNumber: string) {
  133. const order = await selPool.exists(sql.unsafe`
  134. select 1 from shop.orders where order_number = ${orderNumber}
  135. `);
  136. return !!order;
  137. }
  138. async getOrder(order: { orderId: string } | { orderNumber: string }) {
  139. return await selPool.maybeOne(sql.type(
  140. z.object({
  141. orderId: DbSchema.shop.orders.orderId,
  142. orderNumber: DbSchema.shop.orders.orderNumber,
  143. userId: DbSchema.shop.orders.userId,
  144. status: DbSchema.shop.orders.status,
  145. totalAmount: DbSchema.shop.orders.totalAmount,
  146. currencyCode: DbSchema.shop.orders.currencyCode,
  147. billingDataSnapshot: DbSchema.shop.orders.billingDataSnapshot,
  148. createdAt: DbSchema.shop.orders.createdAt,
  149. paymentDueDate: DbSchema.shop.orders.paymentDueDate,
  150. items: z.array(
  151. z.object({
  152. orderItemId: DbSchema.shop.orderItems.orderItemId,
  153. orderId: DbSchema.shop.orderItems.orderId,
  154. productId: DbSchema.shop.orderItems.productId,
  155. quantity: DbSchema.shop.orderItems.quantity,
  156. unitPrice: DbSchema.shop.orderItems.unitPrice,
  157. totalPrice: DbSchema.shop.orderItems.totalPrice,
  158. activityRegId: DbSchema.shop.orderItems.activityRegId.nullable(),
  159. peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
  160. attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
  161. productName: DbSchema.shop.products.name,
  162. productType: DbSchema.shop.products.productType,
  163. stockQuantity: DbSchema.shop.products.stockQuantity,
  164. actPublicName: DbSchema.act.activities.publicName.nullable(),
  165. peMemberFields: CustomFieldWithValue.extend({
  166. userEfId: z.string().uuid(),
  167. }).nullable(),
  168. }),
  169. ),
  170. }),
  171. )`
  172. select
  173. order_id as "orderId",
  174. order_number as "orderNumber",
  175. user_id as "userId",
  176. status,
  177. total_amount::float as "totalAmount",
  178. currency_code as "currencyCode",
  179. billing_data_snapshot as "billingDataSnapshot",
  180. created_at as "createdAt",
  181. payment_due_date as "paymentDueDate",
  182. items
  183. from
  184. shop.orders_with_items
  185. where
  186. ${"orderId" in order ? sql.fragment`order_id = ${order.orderId}` : sql.fragment`order_number = ${order.orderNumber}`}
  187. `);
  188. }
  189. async cancelOrder(orderId: string) {
  190. await updPool.query(sql.unsafe`
  191. update shop.orders set status = 'CANCELLED' where order_id = ${orderId}
  192. `);
  193. }
  194. async getLastPaymentByOrderId(orderId: string) {
  195. return await selPool.maybeOne(sql.type(
  196. z.object({
  197. paymentId: DbSchema.shop.payments.paymentId,
  198. orderId: DbSchema.shop.payments.orderId,
  199. userId: DbSchema.shop.payments.userId,
  200. amount: DbSchema.shop.payments.amount,
  201. currencyCode: DbSchema.shop.payments.currencyCode,
  202. paymentMethod: DbSchema.shop.payments.paymentMethod,
  203. bank: DbSchema.shop.payments.bank,
  204. status: DbSchema.shop.payments.status,
  205. externalTransactionId: DbSchema.shop.payments.externalTransactionId,
  206. paymentGatewayDetails: DbSchema.shop.payments.paymentGatewayDetails,
  207. createdAt: DbSchema.shop.payments.createdAt,
  208. updatedAt: DbSchema.shop.payments.updatedAt,
  209. confirmation: DbSchema.shop.payments.confirmation,
  210. }),
  211. )`
  212. select
  213. payment_id as "paymentId",
  214. order_id as "orderId",
  215. user_id as "userId",
  216. amount::float as "amount",
  217. currency_code as "currencyCode",
  218. payment_method as "paymentMethod",
  219. bank,
  220. status,
  221. external_transaction_id "externalTransactionId",
  222. payment_gateway_details "paymentGatewayDetails",
  223. created_at "createdAt",
  224. updated_at "updatedAt",
  225. confirmation
  226. from
  227. shop.payments
  228. where
  229. order_id = ${orderId}
  230. order by
  231. created_at desc
  232. limit 1;
  233. `);
  234. }
  235. async waitPandingOrder(orderId: string) {
  236. const order = await this.getOrder({ orderId });
  237. const payment = await this.getLastPaymentByOrderId(orderId);
  238. if (!order || !payment) {
  239. logger.error("Order or payment not found");
  240. return;
  241. }
  242. // Инициализация провайдера
  243. const yookassaProvider = new YooKassaProvider();
  244. // Инициализация сервиса с конкретным провайдером
  245. const paymentService = new PaymentService(yookassaProvider);
  246. const status = await paymentService.waitPayment(
  247. payment.paymentId,
  248. order.paymentDueDate,
  249. );
  250. if (status === "succeeded") {
  251. await updPool.query(sql.unsafe`
  252. update shop.orders
  253. set status = 'PAID'
  254. where order_id = ${orderId}
  255. `);
  256. await updPool.query(sql.unsafe`
  257. update shop.order_items
  258. set status = 'PAID'
  259. where order_id = ${orderId}
  260. `);
  261. // статус для регистраций
  262. for (const item of order.items) {
  263. if (
  264. item.productType === "ACTIVITY_REGISTRATION" ||
  265. item.productType === "ACTIVITY_PARTICIPANT"
  266. ) {
  267. if (!item.activityRegId) throw new Error("activityRegId not found");
  268. await cActService.updateActRegPaymentStatus(item.activityRegId);
  269. }
  270. }
  271. // удаляем из корзины
  272. await updPool.query(sql.unsafe`
  273. delete from shop.cart_items where order_id = ${orderId}
  274. `);
  275. }
  276. if (status === "canceled") {
  277. await updPool.query(sql.unsafe`
  278. update shop.orders
  279. set status = 'CANCELLED'
  280. where order_id = ${orderId}
  281. `);
  282. await updPool.query(sql.unsafe`
  283. update shop.order_items
  284. set status = 'CANCELLED'
  285. where order_id = ${orderId}
  286. `);
  287. // возвращаем в корзину
  288. await updPool.query(sql.unsafe`
  289. update shop.cart_items set order_id = null where order_id = ${orderId}
  290. `);
  291. }
  292. if (status === "failed") {
  293. await updPool.query(sql.unsafe`
  294. update shop.orders
  295. set status = 'FAILED'
  296. where order_id = ${orderId}
  297. `);
  298. // возвращаем в корзину
  299. await updPool.query(sql.unsafe`
  300. update shop.cart_items set order_id = null where order_id = ${orderId}
  301. `);
  302. }
  303. }
  304. async waitAllPendingOrders() {
  305. logger.info("Запуск проверки всех ожидающих заказов...");
  306. const orders = await this.getAllPendingOrders();
  307. for (const orderId of orders) {
  308. await this.waitPandingOrder(orderId);
  309. }
  310. }
  311. async getAllPendingOrders() {
  312. return await selPool.anyFirst(sql.type(
  313. z.object({
  314. orderId: z.string().uuid(),
  315. }),
  316. )`
  317. select order_id "orderId" from shop.orders where status = 'PENDING_PAYMENT'
  318. `);
  319. }
  320. async getOrders(userId: string) {
  321. return selPool.any(sql.type(
  322. z.object({
  323. orderId: DbSchema.shop.orders.orderId,
  324. orderNumber: DbSchema.shop.orders.orderNumber,
  325. userId: DbSchema.shop.orders.userId,
  326. status: DbSchema.shop.orders.status,
  327. totalAmount: DbSchema.shop.orders.totalAmount,
  328. currencyCode: DbSchema.shop.orders.currencyCode,
  329. billingDataSnapshot: DbSchema.shop.orders.billingDataSnapshot,
  330. createdAt: DbSchema.shop.orders.createdAt,
  331. paymentDueDate: DbSchema.shop.orders.paymentDueDate,
  332. items: z.array(
  333. z.object({
  334. orderItemId: DbSchema.shop.orderItems.orderItemId,
  335. orderId: DbSchema.shop.orderItems.orderId,
  336. productId: DbSchema.shop.orderItems.productId,
  337. quantity: DbSchema.shop.orderItems.quantity,
  338. unitPrice: DbSchema.shop.orderItems.unitPrice,
  339. totalPrice: DbSchema.shop.orderItems.totalPrice,
  340. activityRegId: DbSchema.shop.orderItems.activityRegId.nullable(),
  341. peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
  342. attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
  343. productName: DbSchema.shop.products.name,
  344. productType: DbSchema.shop.products.productType,
  345. stockQuantity: DbSchema.shop.products.stockQuantity,
  346. actPublicName: DbSchema.act.activities.publicName.nullable(),
  347. peMemberFields: CustomFieldWithValue.extend({
  348. userEfId: z.string().uuid(),
  349. }).nullable(),
  350. }),
  351. ),
  352. }),
  353. )`
  354. select
  355. order_id "orderId",
  356. order_number "orderNumber",
  357. user_id "userId",
  358. status,
  359. total_amount::float "totalAmount",
  360. currency_code "currencyCode",
  361. billing_data_snapshot "billingDataSnapshot",
  362. created_at "createdAt",
  363. payment_due_date "paymentDueDate",
  364. items
  365. from shop.orders_with_items
  366. where user_id = ${userId}
  367. `);
  368. }
  369. }
  370. export const ordersService = new OrdersService();