Bladeren bron

Улучшение регистрации и оплаты

Vadim 2 maanden geleden
bovenliggende
commit
7b29a7049d

+ 22 - 1
src/api/v_0.1.0/client/client-activities-api.ts

@@ -5,6 +5,7 @@ import {
   CustomFieldWithValidatorsAndValue,
   InputFieldValue,
 } from "../types/custom-fields-types.js";
+import { PeMemberWithIdentityShema } from "../types/pe-types.js";
 
 class ClientActivitiesApi {
   GET_EventActivities = {
@@ -99,7 +100,16 @@ class ClientActivitiesApi {
       code: z.enum(["success"]),
 
       actReg: z.discriminatedUnion("role", [
-        actTypes.ActivityRegWithFields.extend({ role: z.literal("owner") }),
+        actTypes.ActivityRegWithFields.extend({
+          role: z.literal("owner"),
+          peMembers: z
+            .array(
+              PeMemberWithIdentityShema.extend({
+                isPaid: z.boolean(),
+              }),
+            )
+            .optional(),
+        }),
         actTypes.ActivityReg.extend({ role: z.literal("member") }),
       ]),
     }),
@@ -180,6 +190,17 @@ class ClientActivitiesApi {
       code: z.enum(["success"]),
     }),
   };
+
+  DELETE_ActivityReg = {
+    req: {
+      params: z.object({
+        activityRegId: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
 }
 
 export const clientActivitiesApi = new ClientActivitiesApi();

+ 55 - 0
src/api/v_0.1.0/client/client-pe-api.ts

@@ -106,6 +106,15 @@ class ClientPartEntitiesApi {
               expirationDate: z.string().datetime().nullable(),
             }),
           ),
+          peMembersRequests: z.array(
+            z.object({
+              peMemberRequestId: z.string().uuid(),
+              peInviteId: z.string().uuid(),
+              userId: z.string().uuid(),
+              userIdentity: z.string(),
+              status: z.enum(["PENDING", "ACCEPTED", "REJECTED"]),
+            }),
+          ),
         }),
 
         z.object({
@@ -239,6 +248,7 @@ class ClientPartEntitiesApi {
         peName: z.string(),
         peOwnerId: z.string().uuid(),
         expirationDate: z.string().datetime().nullable(),
+        peOwnerIdentity: z.string(),
       }),
     }),
   };
@@ -263,12 +273,29 @@ class ClientPartEntitiesApi {
       z.object({
         code: z.literal("peMemberAlreadyExists"),
       }),
+      z.object({
+        code: z.literal("peMemberInviteAlreadyExists"),
+      }),
       z.object({
         code: z.literal("inviteExpired"),
       }),
     ]),
   };
 
+  PATCH_PeMembersRequests = {
+    req: {
+      body: z.array(
+        z.object({
+          peMemberRequestId: z.string().uuid(),
+          status: z.enum(["ACCEPTED", "REJECTED"]),
+        }),
+      ),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
   GET_MyPesForActivity = {
     res: z.object({
       code: z.enum(["success"]),
@@ -302,6 +329,34 @@ class ClientPartEntitiesApi {
       ),
     }),
   };
+
+  DELETE_PeMember = {
+    req: {
+      params: z.object({
+        peMemberId: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  GET_RelatedActivityRegsToPe = {
+    req: {
+      params: z.object({
+        peId: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+      activityRegs: z.array(
+        z.object({
+          activityRegId: z.string().uuid(),
+          activityRegNumber: z.string(),
+        }),
+      ),
+    }),
+  };
 }
 
 export const clientPartEntitiesApi = new ClientPartEntitiesApi();

+ 20 - 18
src/api/v_0.1.0/client/client-shop-api.ts

@@ -1,5 +1,4 @@
 import { z } from "zod";
-import { CustomFieldWithValue } from "../types/custom-fields-types.js";
 import { OrderShema, PaymentShema } from "../types/shop-types.js";
 
 class ClientShopApi {
@@ -19,7 +18,6 @@ class ClientShopApi {
               cartItemId: z.string().uuid(),
               productId: z.string().uuid(),
               productName: z.string(),
-              quantity: z.number(),
               productType: z.enum([
                 "SHOP_ORDER",
                 "TICKET",
@@ -37,9 +35,8 @@ class ClientShopApi {
               activityRegNumber: z.string().nullable(),
 
               peMemberId: z.string().uuid().nullable(),
-              peMemberFields: CustomFieldWithValue.extend({
-                userEfId: z.string().uuid(),
-              }).nullable(),
+              peMemberIdentity: z.string().nullable(),
+
               addedAt: z.string().datetime(),
             }),
           ),
@@ -63,11 +60,16 @@ class ClientShopApi {
           productType: z.literal("ACTIVITY_PARTICIPANT"),
           productId: z.string(),
           peMemberId: z.string(),
+          activityRegId: z.string(),
         }),
         z.object({
           productType: z.literal("SHOP_ORDER"),
           productId: z.string(),
         }),
+        z.object({
+          productType: z.literal("TICKET"),
+          productId: z.string(),
+        }),
       ]),
     },
     res: z.object({
@@ -87,19 +89,19 @@ class ClientShopApi {
     }),
   };
 
-  PUT_CartItemQuantity = {
-    req: {
-      params: z.object({
-        cartItemId: z.string(),
-      }),
-      body: z.object({
-        quantity: z.number(),
-      }),
-    },
-    res: z.object({
-      code: z.enum(["success"]),
-    }),
-  };
+  // PUT_CartItemQuantity = {
+  //   req: {
+  //     params: z.object({
+  //       cartItemId: z.string(),
+  //     }),
+  //     body: z.object({
+  //       quantity: z.number(),
+  //     }),
+  //   },
+  //   res: z.object({
+  //     code: z.enum(["success"]),
+  //   }),
+  // };
 
   //
   //

+ 1 - 0
src/api/v_0.1.0/types/act-types.ts

@@ -79,6 +79,7 @@ class ActTypes {
     peOwnerId: z.string().uuid().nullable(),
     userId: z.string().uuid().nullable(),
     isPaid: z.boolean(),
+    isCanceled: z.boolean(),
     statusHistory: z.array(
       z.object({
         statusHistoryId: z.string().uuid(),

+ 6 - 0
src/api/v_0.1.0/types/pe-types.ts

@@ -9,3 +9,9 @@ export const PeTypeWithFields = z.object({
   eventInstId: z.string(),
   fields: z.array(CustomFieldWithValidators),
 });
+
+export const PeMemberWithIdentityShema = z.object({
+  peMemberId: z.string().uuid(),
+  userId: z.string().uuid(),
+  identity: z.string(),
+});

+ 113 - 26
src/api/v_0.1.0/types/shop-types.ts

@@ -1,5 +1,110 @@
 import { z } from "zod";
-import { CustomFieldWithValue } from "./custom-fields-types.js";
+
+const BaseCartItem = z.object({
+  cartId: z.string().uuid(),
+  cartItemId: z.string().uuid(),
+  productId: z.string().uuid(),
+  priceAtAddition: z.number(),
+  name: z.string(),
+  orderId: z.string().uuid().nullable(),
+  activityRegId: z.string().uuid().nullable(),
+  peMemberId: z.string().uuid().nullable(),
+  productType: z.enum([
+    "SHOP_ORDER",
+    "TICKET",
+    "ACTIVITY_REGISTRATION",
+    "ACTIVITY_PARTICIPANT",
+  ]),
+});
+
+const CartItemShema = z.discriminatedUnion("productType", [
+  BaseCartItem.extend({
+    activityRegId: z.null(),
+    peMemberId: z.null(),
+    productType: z.literal("SHOP_ORDER"),
+  }),
+  BaseCartItem.extend({
+    activityRegId: z.null(),
+    peMemberId: z.null(),
+    productType: z.literal("TICKET"),
+  }),
+  BaseCartItem.extend({
+    activityRegId: z.string().uuid(),
+    peMemberId: z.null(),
+    productType: z.literal("ACTIVITY_REGISTRATION"),
+  }),
+  BaseCartItem.extend({
+    activityRegId: z.string().uuid(),
+    peMemberId: z.string().uuid(),
+    productType: z.literal("ACTIVITY_PARTICIPANT"),
+  }),
+]);
+
+export const CartShema = z.object({
+  cartId: z.string().uuid(),
+  createdAt: z.string().datetime(),
+  updatedAt: z.string().datetime(),
+  items: z.array(CartItemShema),
+});
+
+//
+//
+// order
+//
+//
+
+const BaseOrderItem = z.object({
+  orderItemId: z.string().uuid(),
+  orderId: z.string().uuid(),
+  productId: z.string().uuid(),
+  unitPrice: z.number(),
+
+  activityRegNumber: z.string().nullable(),
+  peMemberId: z.string().uuid().nullable(),
+  peMemberIdentity: z.any().nullable(),
+
+  attributesSnapshot: z.any().nullable(),
+  productName: z.string(),
+  productType: z.enum([
+    "SHOP_ORDER",
+    "TICKET",
+    "ACTIVITY_REGISTRATION",
+    "ACTIVITY_PARTICIPANT",
+  ]),
+  stockQuantity: z.number().nullable(),
+  actPublicName: z.string().nullable(),
+  status: z.enum([
+    "PENDING_PAYMENT",
+    "PAID",
+    "CANCELLED",
+    "FAILED",
+    "REFUNDED",
+  ]),
+});
+
+const OrderItemShema = z.discriminatedUnion("productType", [
+  BaseOrderItem.extend({
+    activityRegId: z.null(),
+    peMemberId: z.null(),
+    peMemberIdentity: z.null(),
+    productType: z.literal("SHOP_ORDER"),
+  }),
+  BaseOrderItem.extend({
+    activityRegId: z.null(),
+    peMemberId: z.null(),
+    productType: z.literal("TICKET"),
+  }),
+  BaseOrderItem.extend({
+    activityRegId: z.string().uuid(),
+    peMemberId: z.null(),
+    productType: z.literal("ACTIVITY_REGISTRATION"),
+  }),
+  BaseOrderItem.extend({
+    activityRegId: z.string().uuid(),
+    peMemberId: z.string().uuid(),
+    productType: z.literal("ACTIVITY_PARTICIPANT"),
+  }),
+]);
 
 export const OrderShema = z.object({
   orderId: z.string().uuid(),
@@ -19,33 +124,15 @@ export const OrderShema = z.object({
   billingDataSnapshot: z.any().nullable(),
   createdAt: z.string().datetime(),
   paymentDueDate: z.string().datetime(),
-  items: z.array(
-    z.object({
-      orderItemId: z.string().uuid(),
-      orderId: z.string().uuid(),
-      productId: z.string().uuid(),
-      quantity: z.number(),
-      unitPrice: z.number(),
-      totalPrice: z.number(),
-      activityRegId: z.string().uuid().nullable(),
-      peMemberId: z.string().uuid().nullable(),
-      attributesSnapshot: z.any().nullable(),
-      productName: z.string(),
-      productType: z.enum([
-        "SHOP_ORDER",
-        "TICKET",
-        "ACTIVITY_REGISTRATION",
-        "ACTIVITY_PARTICIPANT",
-      ]),
-      stockQuantity: z.number().nullable(),
-      actPublicName: z.string().nullable(),
-      peMemberFields: CustomFieldWithValue.extend({
-        userEfId: z.string().uuid(),
-      }).nullable(),
-    }),
-  ),
+  items: z.array(OrderItemShema),
 });
 
+//
+//
+// payment
+//
+//
+
 export const PaymentShema = z.object({
   paymentId: z.string().uuid(),
   status: z.enum(["PENDING", "SUCCEEDED", "FAILED", "REFUNDED", "CANCELED"]),

+ 13 - 3
src/db/db-schema.ts

@@ -85,6 +85,7 @@ const DbSchema = {
       eventId: z.string().uuid(),
       fieldDefinitionId: z.string().uuid(),
       fieldTitleOverride: z.string().nullable(),
+      userIdentityOrdinalNumber: z.number().int().nullable(),
     },
     userEventFieldValues: {
       // Таблица user_event_fields_values из новой БД
@@ -194,10 +195,19 @@ const DbSchema = {
       name: z.string(),
       expirationDate: z.string().datetime().nullable(),
     },
+    peMembersRequests: {
+      peMemberRequestId: z.string().uuid(),
+      peInviteId: z.string().uuid(),
+      userId: z.string().uuid(),
+      status: z.enum(["PENDING", "ACCEPTED", "REJECTED"]),
+      created_at: z.string().datetime(),
+      updated_at: z.string().datetime(),
+    },
     peMembers: {
       peMemberId: z.string().uuid(),
       userId: z.string().uuid(),
       peId: z.string().uuid(),
+      isActive: z.boolean(),
     },
     activityRegFormFields: {
       // Таблица activity_reg_form_fields из новой БД
@@ -215,6 +225,7 @@ const DbSchema = {
       userId: z.string().uuid().nullable(),
       isPaid: z.boolean(),
       number: z.string(),
+      isCanceled: z.boolean(),
     },
     actRegStatuses: {
       actRegStatusId: z.string().uuid(),
@@ -317,7 +328,7 @@ const DbSchema = {
       cartItemId: z.string().uuid(),
       cartId: z.string().uuid(),
       productId: z.string().uuid(),
-      quantity: z.number(),
+      // quantity: z.number(),
       priceAtAddition: z.number(),
       activityRegId: z.string().uuid().nullable(),
       peMemberId: z.string().uuid().nullable(),
@@ -350,9 +361,8 @@ const DbSchema = {
       orderItemId: z.string().uuid(),
       orderId: z.string().uuid(),
       productId: z.string().uuid(),
-      quantity: z.number(),
+      // quantity: z.number(),
       unitPrice: z.number(),
-      totalPrice: z.number(),
       activityRegId: z.string().uuid().nullable(),
       peMemberId: z.string().uuid().nullable(),
       attributesSnapshot: z.any().nullable(),

+ 60 - 2
src/modules/client/activities/c-act-controller.ts

@@ -24,6 +24,7 @@ import { cPeService } from "./participant-entities/c-pe-service.js";
 import { cActService } from "./c-act-service.js";
 import { validatePeForAct } from "./validators/act-pe-validators.js";
 import { generateRandomNumber } from "#utils/other-utils.js";
+import { PeMemberWithIdentityShema } from "#api/v_0.1.0/types/pe-types.js";
 
 class ClientActivitiesController {
   async getEventActivities(
@@ -284,7 +285,7 @@ class ClientActivitiesController {
           "ID сущности участия не передан",
         );
       const pe = await cPeService.getPeWithValues(peId);
-      const members = await cPeService.getMembers(peId);
+      const members = await cPeService.getPeMembersWithFields(peId);
       if (!pe)
         throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
 
@@ -424,8 +425,28 @@ class ClientActivitiesController {
           "actRegNotFound",
           "Регистрация на мероприятии не найдена",
         );
+
+      let peMembers:
+        | (z.infer<typeof PeMemberWithIdentityShema> & { isPaid: boolean })[]
+        | undefined = undefined;
+
+      if (r.peId) {
+        const m = [...(await cPeService.getPeMembersWithIdentity(r.peId))];
+        peMembers = await Promise.all(
+          m.map(async (m) => ({
+            ...m,
+            // TODO: слишком много операций, проще одним запросом вместо getPeMembersWithIdentity
+            isPaid: await cActService.checkPeMemberActivityRegPayment({
+              peMemberId: m.peMemberId,
+              activityRegId: activityRegId,
+            }),
+          })),
+        );
+      }
+
       actReg = {
         ...r,
+        peMembers,
         role,
       };
     } else {
@@ -562,7 +583,7 @@ class ClientActivitiesController {
       peId // может не быть потому что patch
     ) {
       const pe = await cPeService.getPeWithValues(peId);
-      const members = await cPeService.getMembers(peId);
+      const members = await cPeService.getPeMembersWithFields(peId);
       if (!pe)
         throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
 
@@ -658,6 +679,43 @@ class ClientActivitiesController {
       },
     );
   }
+
+  async cancelActivityReg(req: Request, res: Response) {
+    const { activityRegId } =
+      api.client.activities.DELETE_ActivityReg.req.params.parse(req.params);
+
+    const user = sessionService.getUserFromReq(req);
+    const role = await cActService.checkActRegOwner(user.userId, activityRegId);
+    if (role !== "owner") throw ApiError.ForbiddenError();
+
+    await updPool.transaction(async (tr) => {
+      await tr.query(sql.unsafe`
+          update act.activity_regs
+          set
+            is_canceled = true
+          where
+            activity_reg_id = ${activityRegId}
+        `);
+
+      await tr.query(sql.unsafe`
+        insert into act.act_reg_status_history
+          (status_history_id, activity_reg_id, act_reg_status_id, note)
+        values
+          (${v7()}, ${activityRegId}, 'e324fddb-81fe-45f4-90fe-074c0beaae45', 'Заявка отменена')
+      `);
+    });
+
+    // возвраты
+    await cActService.refundByActivityRegId(activityRegId);
+
+    RouterUtils.validAndSendResponse(
+      api.client.activities.DELETE_ActivityReg.res,
+      res,
+      {
+        code: "success",
+      },
+    );
+  }
 }
 
 export const clientActController = new ClientActivitiesController();

+ 5 - 0
src/modules/client/activities/c-act-router.ts

@@ -30,6 +30,11 @@ router.patch(
   RouterUtils.asyncHandler(clientActController.patchActReg),
 );
 
+router.delete(
+  "/reg/:activityRegId",
+  RouterUtils.asyncHandler(clientActController.cancelActivityReg),
+);
+
 router.get(
   "/cat/:categoryCode",
   RouterUtils.asyncHandler(clientActController.getCategory),

+ 184 - 12
src/modules/client/activities/c-act-service.ts

@@ -8,8 +8,10 @@ import {
 import { DbSchema } from "#db/db-schema.js";
 import { selPool, updPool } from "#db/db.js";
 import { ApiError } from "#exceptions/api-error.js";
+import { logger } from "#plugins/logger.js";
 import { sql } from "slonik";
 import { z } from "zod";
+import { ordersService } from "../shop/orders-service.js";
 
 class CActService {
   async addDataToActValidator(
@@ -57,6 +59,7 @@ class CActService {
         peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
         userId: DbSchema.act.activityRegs.userId.nullable(),
         isPaid: DbSchema.act.activityRegs.isPaid,
+        isCanceled: DbSchema.act.activityRegs.isCanceled,
         statusHistory: z.array(
           z.object({
             statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
@@ -83,7 +86,8 @@ class CActService {
         ar.user_id "userId",
         ar.is_paid "isPaid",
         ar.status_history "statusHistory",
-        ar.is_user_reg "isUserReg"
+        ar.is_user_reg "isUserReg",
+        ar.is_canceled "isCanceled"
       from
         act.act_regs_with_status ar
       where
@@ -96,6 +100,7 @@ class CActService {
               from act.pe_members pm_check
               where pm_check.pe_id = ar.pe_id
               and pm_check.user_id = ${userId}
+              and pm_check.is_active = true
           );
     `);
     return actRegs;
@@ -113,6 +118,7 @@ class CActService {
         peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
         userId: DbSchema.act.activityRegs.userId.nullable(),
         isPaid: DbSchema.act.activityRegs.isPaid,
+        isCanceled: DbSchema.act.activityRegs.isCanceled,
         statusHistory: z.array(
           z.object({
             statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
@@ -147,7 +153,8 @@ class CActService {
         ar.is_paid "isPaid",
         ar.status_history "statusHistory",
         ar.fields "fields",
-        ar.is_user_reg "isUserReg"
+        ar.is_user_reg "isUserReg",
+        ar.is_canceled "isCanceled"
       from
         act.act_regs_with_values ar
       where
@@ -168,6 +175,7 @@ class CActService {
         peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
         userId: DbSchema.act.activityRegs.userId.nullable(),
         isPaid: DbSchema.act.activityRegs.isPaid,
+        isCanceled: DbSchema.act.activityRegs.isCanceled,
         statusHistory: z.array(
           z.object({
             statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
@@ -194,7 +202,8 @@ class CActService {
         ar.user_id "userId",
         ar.is_paid "isPaid",
         ar.status_history "statusHistory",
-        ar.is_user_reg "isUserReg"
+        ar.is_user_reg "isUserReg",
+        ar.is_canceled "isCanceled"
       from
         act.act_regs_with_values ar
       where
@@ -266,7 +275,7 @@ class CActService {
   }
 
   async updateActRegPaymentStatus(activityRegId: string) {
-    const actReg = await this.getActRegWithValues(activityRegId);
+    const actReg = await this.getActRegForPeMember(activityRegId);
     if (!actReg) {
       throw ApiError.BadRequest("actRegNotFound", "Не найдена регистрация");
     }
@@ -281,14 +290,16 @@ class CActService {
       const isPaid = await this.checkActivityRegPayment({
         activityRegId,
       });
-      if (isPaid) {
-        await updPool.query(sql.unsafe`
+      // если надо поменять
+      if (actReg.isPaid !== isPaid) {
+        if (isPaid) {
+          await updPool.query(sql.unsafe`
           update act.activity_regs
           set is_paid = true
           where activity_reg_id = ${activityRegId}
         `);
 
-        await updPool.query(sql.unsafe`
+          await updPool.query(sql.unsafe`
           insert into act.act_reg_status_history (
             activity_reg_id,
             act_reg_status_id,
@@ -300,6 +311,14 @@ class CActService {
             'Оплачено'
           )
         `);
+        } else {
+          // такого по идее не должно быть
+          logger.error("isPaid !== actReg.isPaid", {
+            isPaid,
+            actReg,
+            activity,
+          });
+        }
       }
 
       // TODO: QR
@@ -316,19 +335,35 @@ class CActService {
         peId: actReg.peId,
       });
 
-      if (isAllPaid) {
-        await updPool.query(sql.unsafe`
+      // если надо поменять
+      if (isAllPaid !== actReg.isPaid) {
+        if (!isAllPaid) {
+          await updPool.query(sql.unsafe`
           update act.activity_regs
           set is_paid = false
           where activity_reg_id = ${activityRegId}
         `);
-      } else {
-        await updPool.query(sql.unsafe`
+
+          // TODO: Возможно стоит добавить в act.activities поле payment_status_id
+          await updPool.query(sql.unsafe`
+          insert into act.act_reg_status_history (
+            activity_reg_id,
+            act_reg_status_id,
+            note
+          )
+          values (
+            ${activityRegId},
+            'd6d27702-cded-4625-be07-e339c4003c2f',
+            'Оплачены не все участники'
+          )
+        `);
+        } else {
+          await updPool.query(sql.unsafe`
           update act.activity_regs
           set is_paid = true
           where activity_reg_id = ${activityRegId}
         `);
-        await updPool.query(sql.unsafe`
+          await updPool.query(sql.unsafe`
           insert into act.act_reg_status_history (
             activity_reg_id,
             act_reg_status_id,
@@ -340,6 +375,7 @@ class CActService {
             'Оплачены все участники'
           )
         `);
+        }
       }
     }
   }
@@ -364,6 +400,7 @@ class CActService {
         act.pe_members pm
       where
         pm.pe_id = ${peId}
+        and pm.is_active = true
     `);
 
     const memberIds = members.map((member) => member.peMemberId);
@@ -408,6 +445,7 @@ class CActService {
       from act.pe_members
       where pe_id = ${actReg.peId}
       and user_id = ${userId}
+      and is_active = true
     `);
 
     if (isMemeber) return "member";
@@ -415,6 +453,24 @@ class CActService {
     return undefined;
   }
 
+  async checkPeMemberActivityRegPayment({
+    peMemberId,
+    activityRegId,
+  }: {
+    peMemberId: string;
+    activityRegId: string;
+  }) {
+    const isPaid = await selPool.exists(sql.unsafe`
+      select 1
+      from shop.order_items oi
+      where oi.pe_member_id = ${peMemberId}
+      and oi.activity_reg_id = ${activityRegId}
+      and oi.status = 'PAID'
+    `);
+
+    return !!isPaid;
+  }
+
   async getActRegDataWithUserCopyValuesAndActValidators(
     userId: string,
     eventId: string,
@@ -564,6 +620,7 @@ class CActService {
         peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
         userId: DbSchema.act.activityRegs.userId.nullable(),
         isPaid: DbSchema.act.activityRegs.isPaid,
+        isCanceled: DbSchema.act.activityRegs.isCanceled,
         validators: z.array(apiTypes.activities.ActValidator),
         statusHistory: z.array(
           z.object({
@@ -604,6 +661,7 @@ class CActService {
         ar.pe_owner_id "peOwnerId",
         ar.user_id "userId",
         ar.is_paid "isPaid",
+        ar.is_canceled "isCanceled",
         ar.status_history "statusHistory",
         f.fields "fields",
         awv.validators,
@@ -645,6 +703,120 @@ class CActService {
       where ar.activity_reg_id = ${activityRegId}
     `);
   }
+
+  async refundByPeMemberId(peMember: {
+    peMemberId: string;
+    peId: string;
+    userId: string;
+  }) {
+    const actRegs = await selPool.any(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        activityId: DbSchema.act.activityRegs.activityId,
+      }),
+    )`
+      select
+        r.activity_reg_id "activityRegId",
+        r.activity_id "activityId"
+      from
+        act.activity_regs r
+      left join act.activities a on
+        a.activity_id = r.activity_id and
+        a.payment_config = 'PER_PARTICIPANT'
+      where
+        pe_id = ${peMember.peId}
+    `);
+
+    for (const actReg of actRegs) {
+      const orderItem = await selPool.maybeOne(sql.type(
+        z.object({
+          orderItemId: DbSchema.shop.orderItems.orderItemId,
+          orderId: DbSchema.shop.orderItems.orderId,
+          productId: DbSchema.shop.orderItems.productId,
+          unitPrice: DbSchema.shop.orderItems.unitPrice,
+        }),
+      )`
+      select
+        oi.order_item_id "orderItemId",
+        oi.order_id "orderId",
+        oi.product_id "productId",
+        oi.unit_price::float "unitPrice"
+      from
+        shop.order_items oi
+      where
+        oi.activity_reg_id = ${actReg.activityRegId} and
+        oi.pe_member_id = ${peMember.peMemberId}
+    `);
+
+      if (!orderItem) {
+        throw new Error("Order item not found");
+      }
+
+      await ordersService.refundOrderItem(orderItem.orderItemId);
+    }
+  }
+
+  async getActivityRegsByPeId(peId: string) {
+    return await selPool.any(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        activityId: DbSchema.act.activityRegs.activityId,
+        activityRegNumber: DbSchema.act.activityRegs.number,
+      }),
+    )`
+      select
+        r.activity_reg_id "activityRegId",
+        r.activity_id "activityId",
+        r.number "activityRegNumber"
+      from
+        act.activity_regs r
+      where
+        pe_id = ${peId}
+    `);
+  }
+
+  async refundByActivityRegId(activityRegId: string) {
+    const activityId = await selPool.maybeOneFirst(sql.type(
+      z.object({
+        activityId: DbSchema.act.activityRegs.activityId,
+      }),
+    )`
+      select
+        r.activity_id "activityId"
+      from
+        act.activity_regs r
+      left join act.activities a on
+        a.activity_id = r.activity_id
+      where
+        r.activity_reg_id = ${activityRegId}
+    `);
+
+    if (!activityId) {
+      throw new Error("Activity reg not found");
+    }
+
+    const orderItems = await selPool.any(sql.type(
+      z.object({
+        orderItemId: DbSchema.shop.orderItems.orderItemId,
+        orderId: DbSchema.shop.orderItems.orderId,
+        paymentId: DbSchema.shop.payments.paymentId,
+        paymentExternalTransactionId:
+          DbSchema.shop.payments.externalTransactionId,
+      }),
+    )`
+      select
+        oi.order_item_id "orderItemId",
+        oi.order_id "orderId"
+      from
+        shop.order_items oi
+      where
+        oi.activity_reg_id = ${activityRegId}
+    `);
+
+    for (const orderItem of orderItems) {
+      await ordersService.refundOrderItem(orderItem.orderItemId);
+    }
+  }
 }
 
 export const cActService = new CActService();

+ 172 - 10
src/modules/client/activities/participant-entities/c-pe-controller.ts

@@ -18,6 +18,7 @@ import { v4, v7 } from "uuid";
 import { cPeService } from "./c-pe-service.js";
 import { cCustomFieldsValidateService } from "#modules/client/custom-fields/c-cf-validate-service.js";
 import dayjs from "dayjs";
+import { cActService } from "../c-act-service.js";
 
 class ClientPeController {
   async getEventPeTypes(
@@ -304,6 +305,7 @@ class ClientPeController {
       where
         pe.event_inst_id = ${event.eventInstId}
         and pm.user_id = ${user.userId}
+        and pm.is_active = true 
     `);
 
     RouterUtils.validAndSendResponse(api.client.pe.GET_MyPes.res, res, {
@@ -332,8 +334,9 @@ class ClientPeController {
     // валделец
     if (isOwner) {
       const pe = await cPeService.getPeWithValues(peId);
-      const members = await cPeService.getMembers(peId);
+      const members = await cPeService.getPeMembersWithFields(peId);
       const invites = await cPeService.getInvites(peId);
+      const peMembersRequests = await cPeService.getPeMembersRequests(peId);
 
       if (!pe)
         throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
@@ -345,6 +348,7 @@ class ClientPeController {
           ...pe,
           members: [...members],
           invites: [...invites],
+          peMembersRequests: [...peMembersRequests],
           userRole: "owner",
         },
       });
@@ -361,6 +365,7 @@ class ClientPeController {
         where 
           pm.user_id = ${user.userId}
           and pm.pe_id = ${peId}
+          and pm.is_active = true   
       `,
     );
 
@@ -425,12 +430,21 @@ class ClientPeController {
       req.params,
     );
 
-    const invite = await cPeService.getInviteInfo(peInviteUuid);
+    const user = sessionService.getUserFromReq(req);
 
+    const invite = await cPeService.getInviteInfo(peInviteUuid);
     if (!invite)
       throw ApiError.BadRequest("inviteNotFound", "Приглашение не найдено");
+
+    const request = await cPeService.checkPeMemberInvite({
+      peInviteId: invite.peInviteId,
+      userId: user.userId,
+    });
+
     if (dayjs(invite.expirationDate).isBefore(dayjs()))
       throw ApiError.BadRequest("inviteExpired", "Приглашение истекло");
+    if (request)
+      throw ApiError.BadRequest("requestAlreadyExists", "Запрос уже отправлен");
 
     RouterUtils.validAndSendResponse(api.client.pe.GET_InviteInfo.res, res, {
       code: "success",
@@ -446,6 +460,7 @@ class ClientPeController {
 
     const invite = await cPeService.getInviteInfo(peInviteUuid);
 
+    // приглашение не найдено
     if (!invite) {
       RouterUtils.validAndSendResponse(
         api.client.pe.POST_AcceptInvite.res,
@@ -458,6 +473,7 @@ class ClientPeController {
       return;
     }
 
+    // лимит превышен
     if (invite.limitVal && invite.countVal >= invite.limitVal) {
       RouterUtils.validAndSendResponse(
         api.client.pe.POST_AcceptInvite.res,
@@ -470,6 +486,7 @@ class ClientPeController {
       return;
     }
 
+    // приглашение истекло
     if (
       invite.expirationDate &&
       dayjs(invite.expirationDate).isBefore(dayjs())
@@ -486,7 +503,8 @@ class ClientPeController {
     }
 
     // TODO: много лишних данных
-    const peMembers = await cPeService.getMembers(invite.peId);
+    // участник уже в pe
+    const peMembers = await cPeService.getPeMembersWithFields(invite.peId);
     const isFound = peMembers.find((m) => m.userId === user.userId);
     if (isFound) {
       RouterUtils.validAndSendResponse(
@@ -500,15 +518,33 @@ class ClientPeController {
       return;
     }
 
+    // запрос уже отправлен
+    const isPending = await cPeService.checkPeMemberInvite({
+      peInviteId: invite.peInviteId,
+      userId: user.userId,
+    });
+    if (isPending) {
+      RouterUtils.validAndSendResponse(
+        api.client.pe.POST_AcceptInvite.res,
+        res,
+        {
+          code: "peMemberInviteAlreadyExists",
+        },
+        400,
+      );
+      return;
+    }
+
     await updPool.transaction(async (t) => {
-      t.query(sql.unsafe`
-      insert into act.pe_members 
-        (pe_member_id, pe_id, user_id)
-      values
-        (${v7()}, ${invite.peId}, ${user.userId})
-    `);
+      const id = v7();
+      await t.query(sql.unsafe`
+        insert into act.pe_members_requests
+          (pe_member_request_id, pe_invite_id, user_id, status)
+        values
+          (${id}, ${invite.peInviteId}, ${user.userId}, 'PENDING')
+      `);
 
-      t.query(sql.unsafe`
+      await t.query(sql.unsafe`
       update act.pe_invites
       set
         count_val = count_val + 1
@@ -523,6 +559,72 @@ class ClientPeController {
     });
   }
 
+  async patchPeMembersRequests(req: Request, res: Response) {
+    const requests = api.client.pe.PATCH_PeMembersRequests.req.body.parse(
+      req.body,
+    );
+
+    const user = sessionService.getUserFromReq(req);
+    const peIdsToRecheck: Set<string> = new Set();
+    await updPool.transaction(async (t) => {
+      for (const r of requests) {
+        const request = await cPeService.getPeMemberRequest(
+          r.peMemberRequestId,
+        );
+        if (!request)
+          throw ApiError.BadRequest("requestNotFound", "Запрос не найден");
+
+        const pe = await cPeService.getPeForMember(request.peId);
+        if (!pe)
+          throw ApiError.BadRequest(
+            "peNotFound",
+            "Сущность участия не найдена",
+          );
+
+        if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError();
+
+        // участник уже в pe
+        const peMembers = await cPeService.getPeMembersWithFields(pe.peId);
+        const isFound = peMembers.find((m) => m.userId === request.userId);
+        if (isFound)
+          throw ApiError.BadRequest(
+            "memberAlreadyInPe",
+            "Участник уже добавлен",
+          );
+
+        peIdsToRecheck.add(pe.peId);
+        await t.query(sql.unsafe`
+          update act.pe_members_requests
+          set
+            status = ${r.status}
+          where
+            pe_member_request_id = ${r.peMemberRequestId}
+        `);
+
+        if (r.status === "ACCEPTED") {
+          await t.query(sql.unsafe`
+            insert into act.pe_members
+              (pe_id, user_id)
+            values
+              (${pe.peId}, ${request.userId})
+          `);
+        }
+      }
+    });
+
+    for (const peId of peIdsToRecheck) {
+      await cPeService.updateAllActRegStatusByPe(peId);
+    }
+
+    RouterUtils.validAndSendResponse(
+      api.client.pe.PATCH_PeMembersRequests.res,
+      res,
+      {
+        code: "success",
+      },
+    );
+  }
+
   async getMyPesForActivity(req: Request, res: Response) {
     const user = sessionService.getUserFromReq(req);
 
@@ -575,6 +677,66 @@ class ClientPeController {
       },
     );
   }
+
+  async excludeMemberFromPe(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const { peMemberId } = api.client.pe.DELETE_PeMember.req.params.parse(
+      req.params,
+    );
+
+    const peMember = await cPeService.getPeMember(peMemberId);
+    if (!peMember)
+      throw ApiError.BadRequest("peMemberNotFound", "Участник не найден");
+
+    const pe = await cPeService.getPeForMember(peMember.peId);
+    if (!pe)
+      throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
+
+    if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError();
+
+    await updPool.transaction(async (t) => {
+      await cActService.refundByPeMemberId({
+        peMemberId,
+        peId: pe.peId,
+        userId: user.userId,
+      });
+
+      await t.query(sql.unsafe`
+        update act.pe_members
+        set
+          is_active = false
+        where
+          pe_member_id = ${peMemberId}  
+      `);
+    });
+
+    RouterUtils.validAndSendResponse(api.client.pe.DELETE_PeMember.res, res, {
+      code: "success",
+    });
+  }
+
+  async getRelatedActivityRegsToPe(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const { peId } = api.client.pe.GET_RelatedActivityRegsToPe.req.params.parse(
+      req.params,
+    );
+
+    const pe = await cPeService.getPeForMember(peId);
+    if (!pe)
+      throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
+
+    if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError();
+
+    const activityRegs = await cActService.getActivityRegsByPeId(peId);
+    RouterUtils.validAndSendResponse(
+      api.client.pe.GET_RelatedActivityRegsToPe.res,
+      res,
+      {
+        code: "success",
+        activityRegs: [...activityRegs],
+      },
+    );
+  }
 }
 
 export const clientPeController = new ClientPeController();

+ 15 - 0
src/modules/client/activities/participant-entities/c-pe-router.ts

@@ -29,6 +29,16 @@ router.get(
   RouterUtils.asyncHandler(clientPeController.getMyPesForActivity),
 );
 
+router.patch(
+  "/peMembersRequests",
+  RouterUtils.asyncHandler(clientPeController.patchPeMembersRequests),
+);
+
+router.get(
+  "/relatedActivityRegs/:peId",
+  RouterUtils.asyncHandler(clientPeController.getRelatedActivityRegsToPe),
+);
+
 router.get("/:peId", RouterUtils.asyncHandler(clientPeController.getPe));
 
 router.patch(
@@ -61,3 +71,8 @@ router.post(
   "/invite/:peInviteUuid/accept",
   RouterUtils.asyncHandler(clientPeController.acceptInvite),
 );
+
+router.delete(
+  "/peMember/:peMemberId",
+  RouterUtils.asyncHandler(clientPeController.excludeMemberFromPe),
+);

+ 140 - 2
src/modules/client/activities/participant-entities/c-pe-service.ts

@@ -8,6 +8,7 @@ import { DbSchema } from "#db/db-schema.js";
 import { selPool } from "#db/db.js";
 import { sql } from "slonik";
 import { z } from "zod";
+import { cActService } from "../c-act-service.js";
 
 class CPeService {
   async checkPeOwner(userId: string, peId: string) {
@@ -171,7 +172,7 @@ class CPeService {
     `);
   }
 
-  async getMembers(peId: string) {
+  async getPeMembersWithFields(peId: string) {
     return await selPool.any(sql.type(
       z.object({
         peMemberId: z.string().uuid(),
@@ -207,6 +208,7 @@ class CPeService {
         limitVal: DbSchema.act.peInvites.limitVal,
         countVal: DbSchema.act.peInvites.countVal,
         expirationDate: DbSchema.act.peInvites.expirationDate,
+        peOwnerIdentity: z.string(),
       }),
     )`
       select
@@ -217,11 +219,14 @@ class CPeService {
         pe.owner_id "peOwnerId",
         i.limit_val "limitVal",
         i.count_val "countVal",
-        i.expiration_date "expirationDate"
+        i.expiration_date "expirationDate",
+        ui.identity "peOwnerIdentity"
       from
         act.pe_invites i
       join act.part_entities pe on
         pe.pe_id = i.pe_id
+      left join ev.users_identity ui on
+        ui.user_id = pe.owner_id
       where
         pe_invite_uuid = ${peInviteUuid}
     `);
@@ -352,6 +357,139 @@ class CPeService {
             pt.event_inst_id
         `);
   }
+
+  async getPeMembersRequests(peId: string) {
+    return await selPool.any(sql.type(
+      z.object({
+        peMemberRequestId: DbSchema.act.peMembersRequests.peMemberRequestId,
+        peInviteId: DbSchema.act.peMembersRequests.peInviteId,
+        userId: DbSchema.act.peMembersRequests.userId,
+        status: DbSchema.act.peMembersRequests.status,
+        userIdentity: z.string(),
+      }),
+    )`
+      select
+        r.pe_member_request_id "peMemberRequestId",
+        r.pe_invite_id "peInviteId",
+        r.user_id "userId",
+        r.status,
+        ui.identity "userIdentity"
+      from
+        act.pe_members_requests r
+      left join act.pe_invites i on
+        i.pe_invite_id = r.pe_invite_id
+      left join ev.users_identity ui on
+        ui.user_id = r.user_id
+      where
+        i.pe_id = ${peId} 
+    `);
+  }
+
+  async checkPeMemberInvite({
+    peInviteId,
+    userId,
+  }: {
+    peInviteId: string;
+    userId: string;
+  }) {
+    return await selPool.exists(sql.unsafe`
+      select
+        1
+      from
+        act.pe_members_requests r
+      where
+        r.pe_invite_id = ${peInviteId}
+        and r.user_id = ${userId}
+    `);
+  }
+
+  async getPeMemberRequest(peMemberRequestId: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        peMemberRequestId: DbSchema.act.peMembersRequests.peMemberRequestId,
+        peInviteId: DbSchema.act.peMembersRequests.peInviteId,
+        userId: DbSchema.act.peMembersRequests.userId,
+        status: DbSchema.act.peMembersRequests.status,
+        peId: DbSchema.act.peInvites.peId,
+      }),
+    )`
+      select
+        r.pe_member_request_id "peMemberRequestId",
+        r.pe_invite_id "peInviteId",
+        r.user_id "userId",
+        r.status,
+        i.pe_id "peId"
+      from
+        act.pe_members_requests r
+      left join act.pe_invites i on
+        i.pe_invite_id = r.pe_invite_id
+      where
+        r.pe_member_request_id = ${peMemberRequestId}
+    `);
+  }
+
+  async updateAllActRegStatusByPe(peId: string) {
+    const actRegs = await selPool.any(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        peId: DbSchema.act.activityRegs.peId,
+      }),
+    )`
+      select
+        activity_reg_id "activityRegId",
+        pe_id "peId"
+      from
+        act.activity_regs
+      where
+        pe_id = ${peId}
+    `);
+
+    // TODO: добавить в транзикции все функции, в которых что-то меняется
+    for (const actReg of actRegs) {
+      await cActService.updateActRegPaymentStatus(actReg.activityRegId);
+    }
+  }
+
+  async getPeMembersWithIdentity(peId: string) {
+    return await selPool.any(sql.type(
+      z.object({
+        peMemberId: DbSchema.act.peMembers.peMemberId,
+        userId: DbSchema.act.peMembers.userId,
+        identity: z.string(),
+      }),
+    )`
+      select
+        pm.pe_member_id "peMemberId",
+        pm.user_id "userId",
+        ui.identity "identity"
+      from
+        act.pe_members pm
+      left join ev.users_identity ui on
+        ui.user_id = pm.user_id
+      where
+        pm.pe_id = ${peId} and
+        pm.is_active = true
+    `);
+  }
+
+  async getPeMember(peMemberId: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        peMemberId: DbSchema.act.peMembers.peMemberId,
+        peId: DbSchema.act.peMembers.peId,
+        userId: DbSchema.act.peMembers.userId,
+      }),
+    )`
+      select
+        pm.pe_member_id "peMemberId",
+        pm.pe_id "peId",
+        pm.user_id "userId"
+      from
+        act.pe_members pm
+      where
+        pm.pe_member_id = ${peMemberId}
+    `);
+  }
 }
 
 export const cPeService = new CPeService();

+ 88 - 33
src/modules/client/shop/cart/c-cart-controller.ts

@@ -28,6 +28,11 @@ class ClientCartController {
     if (!realProduct) {
       throw ApiError.BadRequest("productNotFound", "Товар не найден");
     }
+    if (product.productType !== realProduct.productType)
+      throw ApiError.BadRequest(
+        "productTypeNotMatch",
+        "Тип товара не совпадает",
+      );
 
     // проверка на наличие корзины пользователя
 
@@ -53,14 +58,39 @@ class ClientCartController {
       }
       // в корзине нет такого товара
       else {
-        await cartService.addItemToCart({
-          cartId: cart.cartId,
-          productId: realProduct.productId,
-          priceAtAddition: realProduct.price,
-          activityRegId:
-            "activityRegId" in product ? product.activityRegId : null,
-          peMemberId: "peMemberId" in product ? product.peMemberId : null,
-        });
+        switch (product.productType) {
+          case "SHOP_ORDER":
+          case "TICKET": {
+            await cartService.addItemToCart({
+              productType: product.productType,
+              cartId: cart.cartId,
+              productId: realProduct.productId,
+              priceAtAddition: realProduct.price,
+            });
+            break;
+          }
+          case "ACTIVITY_REGISTRATION": {
+            await cartService.addItemToCart({
+              productType: product.productType,
+              cartId: cart.cartId,
+              productId: realProduct.productId,
+              priceAtAddition: realProduct.price,
+              activityRegId: product.activityRegId,
+            });
+            break;
+          }
+          case "ACTIVITY_PARTICIPANT": {
+            await cartService.addItemToCart({
+              productType: product.productType,
+              cartId: cart.cartId,
+              productId: realProduct.productId,
+              priceAtAddition: realProduct.price,
+              activityRegId: product.activityRegId,
+              peMemberId: product.peMemberId,
+            });
+            break;
+          }
+        }
       }
 
       RouterUtils.validAndSendResponse(api.client.shop.POST_CartItem.res, res, {
@@ -71,14 +101,39 @@ class ClientCartController {
     // корзины нет
     else {
       const cartId = await cartService.createCart(user?.userId);
-      await cartService.addItemToCart({
-        cartId: cartId,
-        productId: realProduct.productId,
-        priceAtAddition: realProduct.price,
-        activityRegId:
-          "activityRegId" in product ? product.activityRegId : null,
-        peMemberId: "peMemberId" in product ? product.peMemberId : null,
-      });
+      switch (product.productType) {
+        case "SHOP_ORDER":
+        case "TICKET": {
+          await cartService.addItemToCart({
+            productType: product.productType,
+            cartId: cartId,
+            productId: realProduct.productId,
+            priceAtAddition: realProduct.price,
+          });
+          break;
+        }
+        case "ACTIVITY_REGISTRATION": {
+          await cartService.addItemToCart({
+            productType: product.productType,
+            cartId: cartId,
+            productId: realProduct.productId,
+            priceAtAddition: realProduct.price,
+            activityRegId: product.activityRegId,
+          });
+          break;
+        }
+        case "ACTIVITY_PARTICIPANT": {
+          await cartService.addItemToCart({
+            productType: product.productType,
+            cartId: cartId,
+            productId: realProduct.productId,
+            priceAtAddition: realProduct.price,
+            activityRegId: product.activityRegId,
+            peMemberId: product.peMemberId,
+          });
+          break;
+        }
+      }
 
       RouterUtils.validAndSendResponse(api.client.shop.POST_CartItem.res, res, {
         code: "success",
@@ -100,23 +155,23 @@ class ClientCartController {
     });
   }
 
-  async updateCartItemQuantity(req: Request, res: Response) {
-    const { cartItemId } =
-      api.client.shop.PUT_CartItemQuantity.req.params.parse(req.params);
-    const { quantity } = api.client.shop.PUT_CartItemQuantity.req.body.parse(
-      req.body,
-    );
-
-    await cartService.updateCartItemQuantity(cartItemId, quantity);
-
-    RouterUtils.validAndSendResponse(
-      api.client.shop.PUT_CartItemQuantity.res,
-      res,
-      {
-        code: "success",
-      },
-    );
-  }
+  // async updateCartItemQuantity(req: Request, res: Response) {
+  //   const { cartItemId } =
+  //     api.client.shop.PUT_CartItemQuantity.req.params.parse(req.params);
+  //   const { quantity } = api.client.shop.PUT_CartItemQuantity.req.body.parse(
+  //     req.body,
+  //   );
+
+  //   await cartService.updateCartItemQuantity(cartItemId, quantity);
+
+  //   RouterUtils.validAndSendResponse(
+  //     api.client.shop.PUT_CartItemQuantity.res,
+  //     res,
+  //     {
+  //       code: "success",
+  //     },
+  //   );
+  // }
 
   async getCart(req: Request, res: Response) {
     const { cartId } = api.client.shop.GET_Cart.req.params.parse(req.params);

+ 4 - 4
src/modules/client/shop/cart/c-cart-router.ts

@@ -16,7 +16,7 @@ router.delete(
   RouterUtils.asyncHandler(clientCartController.deleteCartItem),
 );
 
-router.put(
-  "/:cartItemId/quantity",
-  RouterUtils.asyncHandler(clientCartController.updateCartItemQuantity),
-);
+// router.put(
+//   "/:cartItemId/quantity",
+//   RouterUtils.asyncHandler(clientCartController.updateCartItemQuantity),
+// );

+ 68 - 27
src/modules/client/shop/cart/cart-service.ts

@@ -1,15 +1,15 @@
-import { CustomFieldWithValue } from "#api/v_0.1.0/types/custom-fields-types.js";
+import { CartShema } from "#api/v_0.1.0/types/shop-types.js";
 import { DbSchema } from "#db/db-schema.js";
 import { selPool, updPool } from "#db/db.js";
 import { sql } from "slonik";
+import { v7 } from "uuid";
 import { z } from "zod";
-import { Cart } from "./cart-types.js";
 
 class CartService {
   async getCart(
     entity: { userId: string } | { cartId: string },
     withItemsInOrder: boolean,
-  ): Promise<Cart | null> {
+  ): Promise<z.infer<typeof CartShema> | null> {
     const cart = await selPool.maybeOne(sql.type(
       z.object({
         cartId: DbSchema.shop.carts.cartId,
@@ -20,7 +20,6 @@ class CartService {
             cartItemId: DbSchema.shop.cartItems.cartItemId,
             cartId: DbSchema.shop.cartItems.cartId,
             productId: DbSchema.shop.cartItems.productId,
-            quantity: DbSchema.shop.cartItems.quantity,
             priceAtAddition: DbSchema.shop.cartItems.priceAtAddition,
             activityRegId: DbSchema.shop.cartItems.activityRegId,
             peMemberId: DbSchema.shop.cartItems.peMemberId,
@@ -44,7 +43,6 @@ class CartService {
               'cartItemId', ci.cart_item_id,
               'cartId', ci.cart_id,
               'productId', ci.product_id,
-              'quantity', ci.quantity,
               'priceAtAddition', ci.price_at_addition,
               'activityRegId', ci.activity_reg_id,
               'peMemberId', ci.pe_member_id,
@@ -64,7 +62,7 @@ class CartService {
         where
             ${"cartId" in entity ? sql.fragment`c.cart_id = ${entity.cartId}` : sql.fragment`c.user_id = ${entity.userId}`}
         `);
-    return cart;
+    return CartShema.parse(cart);
   }
 
   async getFullCart(
@@ -81,7 +79,6 @@ class CartService {
             cartItemId: DbSchema.shop.cartItems.cartItemId,
             productId: DbSchema.shop.cartItems.productId,
             productName: DbSchema.shop.products.name,
-            quantity: DbSchema.shop.cartItems.quantity,
             productType: DbSchema.shop.products.productType,
             priceAtAddition: DbSchema.shop.cartItems.priceAtAddition,
             realPrice: DbSchema.shop.products.price,
@@ -94,9 +91,7 @@ class CartService {
             activityRegNumber: DbSchema.act.activityRegs.number.nullable(),
 
             peMemberId: DbSchema.shop.cartItems.peMemberId.nullable(),
-            peMemberFields: CustomFieldWithValue.extend({
-              userEfId: z.string().uuid(),
-            }).nullable(),
+            peMemberIdentity: z.string().nullable(),
 
             name: DbSchema.shop.products.name,
             addedAt: DbSchema.shop.cartItems.addedAt,
@@ -117,7 +112,6 @@ class CartService {
               'cartItemId', ci.cart_item_id,
               'productId', ci.product_id,
               'productName', p.name,
-              'quantity', ci.quantity,
 			        'productType', p.product_type,
               'priceAtAddition', ci.price_at_addition,
               'realPrice', p.price,
@@ -130,7 +124,7 @@ class CartService {
               'activityRegNumber', ar.number,
 
               'peMemberId', ci.pe_member_id,
-              'peMemberFields', pm.fields,
+              'peMemberIdentity', ui.identity,
 
               'name', p."name",
               'addedAt', to_char(ci.added_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS') || 'Z'
@@ -143,8 +137,11 @@ class CartService {
             ar.activity_reg_id = ci.activity_reg_id
           left join act.activities a on
             a.activity_id = ar.activity_id
-          left join act.pe_members_with_fields_and_values pm on
-            pm.pe_member_id = ci.pe_member_id
+          left join act.pe_members pm on
+            pm.pe_member_id = ci.pe_member_id and
+            pm.is_active = true 
+          left join ev.users_identity ui on
+            pm.user_id = ui.user_id
           where
             ci.cart_id = c.cart_id
             ${withItemsInOrder ? sql.fragment`and ci.order_id is null` : sql.fragment``}
@@ -155,6 +152,7 @@ class CartService {
         `);
     return cart;
   }
+  // TODO: удалить везде act.pe_members_with_fields_and_values но проверить валидацию активности
 
   async createCart(userId?: string) {
     const cartId = await updPool.one(sql.unsafe`
@@ -174,16 +172,59 @@ class CartService {
   // }
 
   async addItemToCart(item: {
+    productType: "SHOP_ORDER" | "TICKET";
+    cartId: string;
+    productId: string;
+    priceAtAddition: number;
+  }): Promise<void>;
+  async addItemToCart(item: {
+    productType: "ACTIVITY_REGISTRATION";
+    cartId: string;
+    productId: string;
+    priceAtAddition: number;
+    activityRegId: string;
+  }): Promise<void>;
+  async addItemToCart(item: {
+    productType: "ACTIVITY_PARTICIPANT";
+    cartId: string;
+    productId: string;
+    priceAtAddition: number;
+    activityRegId: string;
+    peMemberId: string;
+  }): Promise<void>;
+  async addItemToCart(item: {
+    productType:
+      | "SHOP_ORDER"
+      | "TICKET"
+      | "ACTIVITY_REGISTRATION"
+      | "ACTIVITY_PARTICIPANT";
     cartId: string;
     productId: string;
     priceAtAddition: number;
-    activityRegId: string | null;
-    peMemberId: string | null;
-  }) {
+    activityRegId?: string;
+    peMemberId?: string;
+  }): Promise<void> {
+    const id = v7();
     await updPool.query(sql.unsafe`
-            insert into shop.cart_items (cart_id, product_id, quantity, price_at_addition, activity_reg_id, pe_member_id)
-            values (${item.cartId}, ${item.productId}, 1, ${item.priceAtAddition}, ${item.activityRegId}, ${item.peMemberId})
-        `);
+      insert into shop.cart_items 
+      (
+        cart_item_id, 
+        cart_id, 
+        product_id, 
+        price_at_addition, 
+        activity_reg_id, 
+        pe_member_id
+      )
+      values 
+      (
+        ${id},
+        ${item.cartId}, 
+        ${item.productId}, 
+        ${item.priceAtAddition}, 
+        ${item.activityRegId || null}, 
+        ${item.peMemberId || null}
+      )
+    `);
   }
 
   async deleteCartItem(cartItemId: string) {
@@ -193,13 +234,13 @@ class CartService {
         `);
   }
 
-  async updateCartItemQuantity(cartItemId: string, quantity: number) {
-    await updPool.query(sql.unsafe`
-            update shop.cart_items
-            set quantity = ${quantity}
-            where cart_item_id = ${cartItemId}
-        `);
-  }
+  // async updateCartItemQuantity(cartItemId: string, quantity: number) {
+  //   await updPool.query(sql.unsafe`
+  //           update shop.cart_items
+  //           set quantity = ${quantity}
+  //           where cart_item_id = ${cartItemId}
+  //       `);
+  // }
 
   async clearCart(cartId: string) {
     await updPool.query(sql.unsafe`

+ 0 - 21
src/modules/client/shop/cart/cart-types.ts

@@ -1,21 +0,0 @@
-export type Cart = {
-  cartId: string;
-  createdAt: string;
-  updatedAt: string;
-  items: {
-    cartId: string;
-    cartItemId: string;
-    productId: string;
-    quantity: number;
-    priceAtAddition: number;
-    activityRegId: string | null;
-    peMemberId: string | null;
-    name: string;
-    productType:
-      | "SHOP_ORDER"
-      | "TICKET"
-      | "ACTIVITY_REGISTRATION"
-      | "ACTIVITY_PARTICIPANT";
-    orderId: string | null;
-  }[];
-};

+ 93 - 23
src/modules/client/shop/orders-service.ts

@@ -5,14 +5,14 @@ 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";
+import { CartShema, OrderShema } from "#api/v_0.1.0/types/shop-types.js";
+import { UnexpectedError } from "#exceptions/unexpected-errors.js";
 
 type OrderUserData = Record<string, SerializableValue> & {
   firstName: string;
@@ -30,7 +30,7 @@ class OrdersService {
       userId,
       userData,
     }: {
-      cart: Cart;
+      cart: z.infer<typeof CartShema>;
       userId?: string;
       userData: OrderUserData;
     },
@@ -52,7 +52,6 @@ class OrdersService {
         }
         return {
           ...p,
-          quantity: item.quantity,
           activityRegId: item.activityRegId,
           peMemberId: item.peMemberId,
           cartItemId: item.cartItemId,
@@ -70,7 +69,7 @@ class OrdersService {
     }
 
     const totalAmount = products.reduce((acc, product) => {
-      return acc + product.price * product.quantity;
+      return acc + product.price;
     }, 0);
 
     const paymentDueDate = dayjs().add(1, "hour").toDate();
@@ -104,9 +103,7 @@ class OrdersService {
         order_item_id, 
         order_id, 
         product_id, 
-        quantity, 
         unit_price, 
-        total_price,
         activity_reg_id,
         pe_member_id,
         attributes_snapshot
@@ -115,9 +112,7 @@ class OrdersService {
         ${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)}
@@ -149,8 +144,10 @@ class OrdersService {
     return !!order;
   }
 
-  async getOrder(order: { orderId: string } | { orderNumber: string }) {
-    return await selPool.maybeOne(sql.type(
+  async getOrder(
+    order: { orderId: string } | { orderNumber: string },
+  ): Promise<z.infer<typeof OrderShema>> {
+    const orderData = await selPool.maybeOne(sql.type(
       z.object({
         orderId: DbSchema.shop.orders.orderId,
         orderNumber: DbSchema.shop.orders.orderNumber,
@@ -166,19 +163,19 @@ class OrdersService {
             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(),
+            activityRegNumber: DbSchema.act.activityRegs.number.nullable(),
             peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
+            peMemberIdentity: z.string().nullable(),
+
             attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
+            status: DbSchema.shop.orderItems.status,
             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(),
           }),
         ),
       }),
@@ -199,6 +196,8 @@ class OrdersService {
     where
         ${"orderId" in order ? sql.fragment`order_id = ${order.orderId}` : sql.fragment`order_number = ${order.orderNumber}`}
     `);
+
+    return OrderShema.parse(orderData);
   }
 
   async cancelOrder(orderId: string) {
@@ -348,8 +347,8 @@ class OrdersService {
     `);
   }
 
-  async getOrders(userId: string) {
-    return selPool.any(sql.type(
+  async getOrders(userId: string): Promise<z.infer<typeof OrderShema>[]> {
+    const orders = await selPool.any(sql.type(
       z.object({
         orderId: DbSchema.shop.orders.orderId,
         orderNumber: DbSchema.shop.orders.orderNumber,
@@ -365,19 +364,19 @@ class OrdersService {
             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(),
+            activityRegNumber: DbSchema.act.activityRegs.number.nullable(),
             peMemberId: DbSchema.shop.orderItems.peMemberId.nullable(),
+            peMemberIdentity: z.string().nullable(),
+
             attributesSnapshot: DbSchema.shop.orderItems.attributesSnapshot,
+            status: DbSchema.shop.orderItems.status,
             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(),
           }),
         ),
       }),
@@ -396,6 +395,77 @@ class OrdersService {
       from shop.orders_with_items
       where user_id = ${userId}
     `);
+
+    return z.array(OrderShema).parse(orders);
+  }
+
+  async getOrderItem(orderItemId: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        orderItemId: DbSchema.shop.orderItems.orderItemId,
+        orderId: DbSchema.shop.orderItems.orderId,
+        productId: DbSchema.shop.orderItems.productId,
+        unitPrice: DbSchema.shop.orderItems.unitPrice,
+        paymentId: DbSchema.shop.payments.paymentId,
+        paymentExternalTransactionId:
+          DbSchema.shop.payments.externalTransactionId,
+      }),
+    )`
+      select
+        oi.order_item_id "orderItemId",
+        oi.order_id "orderId",
+        oi.product_id "productId",
+        oi.unit_price::float "unitPrice",
+        p.payment_id "paymentId",
+        p.external_transaction_id "paymentExternalTransactionId"
+      from
+        shop.order_items oi
+      left join shop.payments p on
+        p.order_id = oi.order_id  
+      where
+        oi.order_item_id = ${orderItemId} and
+        oi.status = 'PAID'
+    `);
+  }
+
+  async refundOrderItem(orderItemId: string) {
+    const orderItem = await this.getOrderItem(orderItemId);
+
+    if (!orderItem) {
+      throw new Error("Order item not found");
+    }
+
+    if (!orderItem.paymentExternalTransactionId) {
+      throw new Error("Payment external transaction id not found");
+    }
+
+    // Инициализация провайдера
+    const yookassaProvider = new YooKassaProvider();
+    // Инициализация сервиса с конкретным провайдером
+    const paymentService = new PaymentService(yookassaProvider);
+
+    const refund = await paymentService.refundPayment({
+      paymentId: orderItem.paymentExternalTransactionId,
+      amount: {
+        value: orderItem.unitPrice.toFixed(2),
+        currency: "RUB",
+      },
+    });
+
+    if (refund.status !== "succeeded") {
+      logger.error("Payment refund failed", refund);
+      throw new UnexpectedError(
+        500,
+        "Payment refund failed",
+        "Payment refund failed",
+      );
+    }
+
+    await updPool.query(sql.unsafe`
+          update shop.order_items
+          set status = 'REFUNDED'
+          where order_item_id = ${orderItem.orderItemId}
+        `);
   }
 }
 

+ 17 - 1
src/modules/client/shop/payment/payment-service.ts

@@ -116,7 +116,23 @@ export class PaymentService {
     console.log(
       `Creating refund for payment ${params.paymentId} with idempotency key: ${idempotencyKey}`,
     );
-    return this.provider.createRefund(params, 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;
   }
 
   // Новая функция для отмены платежа