123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- import { selPool, updPool } from "#db/db.js";
- import { DatabaseTransactionConnection, SerializableValue, sql } from "slonik";
- import { v7 as uuidv7 } from "uuid";
- import { cartService } from "./cart/cart-service.js";
- import { ApiError } from "#exceptions/api-error.js";
- import { z } from "zod";
- import { DbSchema } from "#db/db-schema.js";
- import { CustomFieldWithValue } from "#api/v_0.1.0/types/custom-fields-types.js";
- import { generateRandomNumber } from "#utils/other-utils.js";
- import { Cart } from "./cart/cart-types.js";
- import { dayjs } from "#plugins/dayjs.js";
- import { PaymentService } from "./payment/payment-service.js";
- import { logger } from "#plugins/logger.js";
- import { YooKassaProvider } from "./payment/yookassa-provider.js";
- import { cActService } from "../activities/c-act-service.js";
- type OrderUserData = Record<string, SerializableValue> & {
- firstName: string;
- lastName: string;
- patronymic: string | null;
- email: string;
- phone: string;
- };
- class OrdersService {
- async createOrder(
- tr: DatabaseTransactionConnection,
- {
- cart,
- userId,
- userData,
- }: {
- cart: Cart;
- userId?: string;
- userData: OrderUserData;
- },
- ) {
- const products = await Promise.all(
- cart.items.map(async (item) => {
- const p = await cartService.getProductById(item.productId);
- if (!p) {
- throw ApiError.BadRequest(
- "productNotFound",
- "Не найден товар из корзины",
- );
- }
- if (item.priceAtAddition !== p.price) {
- throw ApiError.BadRequest(
- "productPriceChanged",
- "Цена товара изменилась",
- );
- }
- return {
- ...p,
- quantity: item.quantity,
- activityRegId: item.activityRegId,
- peMemberId: item.peMemberId,
- cartItemId: item.cartItemId,
- };
- }),
- );
- // order
- const orderId = uuidv7();
- let orderNumber = generateRandomNumber();
- while (await this.checkOrderNumber(orderNumber)) {
- orderNumber = generateRandomNumber();
- }
- const totalAmount = products.reduce((acc, product) => {
- return acc + product.price * product.quantity;
- }, 0);
- const paymentDueDate = dayjs().add(1, "hour").toDate();
- await tr.query(sql.unsafe`
- insert into shop.orders (
- order_id,
- order_number,
- user_id,
- status,
- total_amount,
- currency_code,
- billing_data_snapshot,
- payment_due_date
- )
- values (
- ${orderId},
- ${orderNumber},
- ${userId || null},
- 'PENDING_PAYMENT',
- ${totalAmount},
- 'RUB',
- ${sql.jsonb(userData)},
- ${paymentDueDate.toISOString()}
- )
- `);
- for (const product of products) {
- await tr.query(sql.unsafe`
- insert into shop.order_items (
- order_item_id,
- order_id,
- product_id,
- quantity,
- unit_price,
- total_price,
- activity_reg_id,
- pe_member_id,
- attributes_snapshot
- )
- values (
- ${uuidv7()},
- ${orderId},
- ${product.productId},
- ${product.quantity},
- ${product.price},
- ${product.price * product.quantity},
- ${"activityRegId" in product ? product.activityRegId : null},
- ${"peMemberId" in product ? product.peMemberId : null},
- ${sql.jsonb(product.attributes)}
- )
- `);
- // чтобы скрыть из корзины
- await tr.query(sql.unsafe`
- update shop.cart_items set order_id = ${orderId} where cart_item_id = ${product.cartItemId}
- `);
- }
- return {
- orderId,
- orderNumber,
- totalAmount,
- currencyCode: "RUB",
- billingDataSnapshot: userData,
- paymentDueDate,
- items: products,
- };
- }
- private async checkOrderNumber(orderNumber: string) {
- const order = await selPool.exists(sql.unsafe`
- select 1 from shop.orders where order_number = ${orderNumber}
- `);
- return !!order;
- }
- async getOrder(order: { orderId: string } | { orderNumber: string }) {
- return await selPool.maybeOne(sql.type(
- z.object({
- orderId: DbSchema.shop.orders.orderId,
- orderNumber: DbSchema.shop.orders.orderNumber,
- userId: DbSchema.shop.orders.userId,
- status: DbSchema.shop.orders.status,
- totalAmount: DbSchema.shop.orders.totalAmount,
- currencyCode: DbSchema.shop.orders.currencyCode,
- billingDataSnapshot: DbSchema.shop.orders.billingDataSnapshot,
- createdAt: DbSchema.shop.orders.createdAt,
- paymentDueDate: DbSchema.shop.orders.paymentDueDate,
- items: z.array(
- z.object({
- orderItemId: DbSchema.shop.orderItems.orderItemId,
- orderId: DbSchema.shop.orderItems.orderId,
- productId: DbSchema.shop.orderItems.productId,
- quantity: DbSchema.shop.orderItems.quantity,
- unitPrice: DbSchema.shop.orderItems.unitPrice,
- totalPrice: DbSchema.shop.orderItems.totalPrice,
- activityRegId: DbSchema.shop.orderItems.activityRegId.nullable(),
- peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
- attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
- productName: DbSchema.shop.products.name,
- productType: DbSchema.shop.products.productType,
- stockQuantity: DbSchema.shop.products.stockQuantity,
- actPublicName: DbSchema.act.activities.publicName.nullable(),
- peMemberFields: CustomFieldWithValue.extend({
- userEfId: z.string().uuid(),
- }).nullable(),
- }),
- ),
- }),
- )`
- select
- order_id as "orderId",
- order_number as "orderNumber",
- user_id as "userId",
- status,
- total_amount::float as "totalAmount",
- currency_code as "currencyCode",
- billing_data_snapshot as "billingDataSnapshot",
- created_at as "createdAt",
- payment_due_date as "paymentDueDate",
- items
- from
- shop.orders_with_items
- where
- ${"orderId" in order ? sql.fragment`order_id = ${order.orderId}` : sql.fragment`order_number = ${order.orderNumber}`}
- `);
- }
- async cancelOrder(orderId: string) {
- await updPool.query(sql.unsafe`
- update shop.orders set status = 'CANCELLED' where order_id = ${orderId}
- `);
- }
- async getLastPaymentByOrderId(orderId: string) {
- return await selPool.maybeOne(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,
- }),
- )`
- select
- 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 "externalTransactionId",
- payment_gateway_details "paymentGatewayDetails",
- created_at "createdAt",
- updated_at "updatedAt",
- confirmation
- from
- shop.payments
- where
- order_id = ${orderId}
- order by
- created_at desc
- limit 1;
- `);
- }
- async waitPandingOrder(orderId: string) {
- const order = await this.getOrder({ orderId });
- const payment = await this.getLastPaymentByOrderId(orderId);
- if (!order || !payment) {
- logger.error("Order or payment not found");
- return;
- }
- // Инициализация провайдера
- const yookassaProvider = new YooKassaProvider();
- // Инициализация сервиса с конкретным провайдером
- const paymentService = new PaymentService(yookassaProvider);
- const status = await paymentService.waitPayment(
- payment.paymentId,
- order.paymentDueDate,
- );
- if (status === "succeeded") {
- await updPool.query(sql.unsafe`
- update shop.orders
- set status = 'PAID'
- where order_id = ${orderId}
- `);
- await updPool.query(sql.unsafe`
- update shop.order_items
- set status = 'PAID'
- where order_id = ${orderId}
- `);
- // статус для регистраций
- for (const item of order.items) {
- if (
- item.productType === "ACTIVITY_REGISTRATION" ||
- item.productType === "ACTIVITY_PARTICIPANT"
- ) {
- if (!item.activityRegId) throw new Error("activityRegId not found");
- await cActService.updateActRegPaymentStatus(item.activityRegId);
- }
- }
- // удаляем из корзины
- await updPool.query(sql.unsafe`
- delete from shop.cart_items where order_id = ${orderId}
- `);
- }
- if (status === "canceled") {
- await updPool.query(sql.unsafe`
- update shop.orders
- set status = 'CANCELLED'
- where order_id = ${orderId}
- `);
- await updPool.query(sql.unsafe`
- update shop.order_items
- set status = 'CANCELLED'
- where order_id = ${orderId}
- `);
- // возвращаем в корзину
- await updPool.query(sql.unsafe`
- update shop.cart_items set order_id = null where order_id = ${orderId}
- `);
- }
- if (status === "failed") {
- await updPool.query(sql.unsafe`
- update shop.orders
- set status = 'FAILED'
- where order_id = ${orderId}
- `);
- // возвращаем в корзину
- await updPool.query(sql.unsafe`
- update shop.cart_items set order_id = null where order_id = ${orderId}
- `);
- }
- }
- async waitAllPendingOrders() {
- logger.info("Запуск проверки всех ожидающих заказов...");
- const orders = await this.getAllPendingOrders();
- for (const orderId of orders) {
- await this.waitPandingOrder(orderId);
- }
- }
- async getAllPendingOrders() {
- return await selPool.anyFirst(sql.type(
- z.object({
- orderId: z.string().uuid(),
- }),
- )`
- select order_id "orderId" from shop.orders where status = 'PENDING_PAYMENT'
- `);
- }
- async getOrders(userId: string) {
- return selPool.any(sql.type(
- z.object({
- orderId: DbSchema.shop.orders.orderId,
- orderNumber: DbSchema.shop.orders.orderNumber,
- userId: DbSchema.shop.orders.userId,
- status: DbSchema.shop.orders.status,
- totalAmount: DbSchema.shop.orders.totalAmount,
- currencyCode: DbSchema.shop.orders.currencyCode,
- billingDataSnapshot: DbSchema.shop.orders.billingDataSnapshot,
- createdAt: DbSchema.shop.orders.createdAt,
- paymentDueDate: DbSchema.shop.orders.paymentDueDate,
- items: z.array(
- z.object({
- orderItemId: DbSchema.shop.orderItems.orderItemId,
- orderId: DbSchema.shop.orderItems.orderId,
- productId: DbSchema.shop.orderItems.productId,
- quantity: DbSchema.shop.orderItems.quantity,
- unitPrice: DbSchema.shop.orderItems.unitPrice,
- totalPrice: DbSchema.shop.orderItems.totalPrice,
- activityRegId: DbSchema.shop.orderItems.activityRegId.nullable(),
- peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
- attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
- productName: DbSchema.shop.products.name,
- productType: DbSchema.shop.products.productType,
- stockQuantity: DbSchema.shop.products.stockQuantity,
- actPublicName: DbSchema.act.activities.publicName.nullable(),
- peMemberFields: CustomFieldWithValue.extend({
- userEfId: z.string().uuid(),
- }).nullable(),
- }),
- ),
- }),
- )`
- select
- order_id "orderId",
- order_number "orderNumber",
- user_id "userId",
- status,
- total_amount::float "totalAmount",
- currency_code "currencyCode",
- billing_data_snapshot "billingDataSnapshot",
- created_at "createdAt",
- payment_due_date "paymentDueDate",
- items
- from shop.orders_with_items
- where user_id = ${userId}
- `);
- }
- }
- export const ordersService = new OrdersService();
|