Pārlūkot izejas kodu

feat(core): Add history entry to Order when vouchers applied/removed

Michael Bromley 6 gadi atpakaļ
vecāks
revīzija
887cc6cf51

+ 2 - 0
packages/common/src/generated-shop-types.ts

@@ -874,6 +874,8 @@ export enum HistoryEntryType {
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_NOTE = 'ORDER_NOTE',
     ORDER_NOTE = 'ORDER_NOTE',
+    ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
+    ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
 }
 }
 
 
 export type ImportInfo = {
 export type ImportInfo = {

+ 3 - 1
packages/common/src/generated-types.ts

@@ -1205,7 +1205,9 @@ export enum HistoryEntryType {
   ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
   ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
   ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
-  ORDER_NOTE = 'ORDER_NOTE'
+  ORDER_NOTE = 'ORDER_NOTE',
+  ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
+  ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED'
 }
 }
 
 
 export type ImportInfo = {
 export type ImportInfo = {

+ 2 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1210,6 +1210,8 @@ export enum HistoryEntryType {
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_NOTE = 'ORDER_NOTE',
     ORDER_NOTE = 'ORDER_NOTE',
+    ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
+    ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
 }
 }
 
 
 export type ImportInfo = {
 export type ImportInfo = {

+ 7 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -874,6 +874,8 @@ export enum HistoryEntryType {
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_CANCELLATION = 'ORDER_CANCELLATION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
     ORDER_NOTE = 'ORDER_NOTE',
     ORDER_NOTE = 'ORDER_NOTE',
+    ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
+    ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
 }
 }
 
 
 export type ImportInfo = {
 export type ImportInfo = {
@@ -2233,6 +2235,9 @@ export type TestOrderFragmentFragment = { __typename?: 'Order' } & Pick<
             { __typename?: 'ShippingMethod' } & Pick<ShippingMethod, 'id' | 'code' | 'description'>
             { __typename?: 'ShippingMethod' } & Pick<ShippingMethod, 'id' | 'code' | 'description'>
         >;
         >;
         customer: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>;
         customer: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>;
+        history: { __typename?: 'HistoryEntryList' } & {
+            items: Array<{ __typename?: 'HistoryEntry' } & Pick<HistoryEntry, 'id' | 'type' | 'data'>>;
+        };
     };
     };
 
 
 export type AddItemToOrderMutationVariables = {
 export type AddItemToOrderMutationVariables = {
@@ -2595,6 +2600,8 @@ export namespace TestOrderFragment {
     export type ProductVariant = (NonNullable<TestOrderFragmentFragment['lines'][0]>)['productVariant'];
     export type ProductVariant = (NonNullable<TestOrderFragmentFragment['lines'][0]>)['productVariant'];
     export type ShippingMethod = NonNullable<TestOrderFragmentFragment['shippingMethod']>;
     export type ShippingMethod = NonNullable<TestOrderFragmentFragment['shippingMethod']>;
     export type Customer = NonNullable<TestOrderFragmentFragment['customer']>;
     export type Customer = NonNullable<TestOrderFragmentFragment['customer']>;
+    export type History = TestOrderFragmentFragment['history'];
+    export type Items = NonNullable<TestOrderFragmentFragment['history']['items'][0]>;
 }
 }
 
 
 export namespace AddItemToOrder {
 export namespace AddItemToOrder {

+ 7 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -30,6 +30,13 @@ export const TEST_ORDER_FRAGMENT = gql`
         customer {
         customer {
             id
             id
         }
         }
+        history {
+            items {
+                id
+                type
+                data
+            }
+        }
     }
     }
 `;
 `;
 
 

+ 65 - 0
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -15,6 +15,7 @@ import {
     CreatePromotionInput,
     CreatePromotionInput,
     GetFacetList,
     GetFacetList,
     GetPromoProducts,
     GetPromoProducts,
+    HistoryEntryType,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
     AddItemToOrder,
     AddItemToOrder,
@@ -163,6 +164,21 @@ describe('Promotions applied to Orders', () => {
             expect(applyCouponCode!.total).toBe(0);
             expect(applyCouponCode!.total).toBe(0);
         });
         });
 
 
+        it('order history records application', async () => {
+            const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
+
+            expect(activeOrder!.history.items).toEqual([
+                {
+                    id: 'T_1',
+                    type: HistoryEntryType.ORDER_COUPON_APPLIED,
+                    data: {
+                        couponCode: TEST_COUPON_CODE,
+                        promotionId: 'T_3',
+                    },
+                },
+            ]);
+        });
+
         it('de-duplicates existing codes', async () => {
         it('de-duplicates existing codes', async () => {
             const { applyCouponCode } = await shopClient.query<
             const { applyCouponCode } = await shopClient.query<
                 ApplyCouponCode.Mutation,
                 ApplyCouponCode.Mutation,
@@ -185,6 +201,55 @@ describe('Promotions applied to Orders', () => {
             expect(removeCouponCode!.adjustments.length).toBe(0);
             expect(removeCouponCode!.adjustments.length).toBe(0);
             expect(removeCouponCode!.total).toBe(6000);
             expect(removeCouponCode!.total).toBe(6000);
         });
         });
+
+        it('order history records removal', async () => {
+            const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
+
+            expect(activeOrder!.history.items).toEqual([
+                {
+                    id: 'T_1',
+                    type: HistoryEntryType.ORDER_COUPON_APPLIED,
+                    data: {
+                        couponCode: TEST_COUPON_CODE,
+                        promotionId: 'T_3',
+                    },
+                },
+                {
+                    id: 'T_2',
+                    type: HistoryEntryType.ORDER_COUPON_REMOVED,
+                    data: {
+                        couponCode: TEST_COUPON_CODE,
+                    },
+                },
+            ]);
+        });
+
+        it('does not record removal of coupon code that was not added', async () => {
+            const { removeCouponCode } = await shopClient.query<
+                RemoveCouponCode.Mutation,
+                RemoveCouponCode.Variables
+            >(REMOVE_COUPON_CODE, {
+                couponCode: 'NOT_THERE',
+            });
+
+            expect(removeCouponCode!.history.items).toEqual([
+                {
+                    id: 'T_1',
+                    type: HistoryEntryType.ORDER_COUPON_APPLIED,
+                    data: {
+                        couponCode: TEST_COUPON_CODE,
+                        promotionId: 'T_3',
+                    },
+                },
+                {
+                    id: 'T_2',
+                    type: HistoryEntryType.ORDER_COUPON_REMOVED,
+                    data: {
+                        couponCode: TEST_COUPON_CODE,
+                    },
+                },
+            ]);
+        });
     });
     });
 
 
     describe('default PromotionConditions', () => {
     describe('default PromotionConditions', () => {

+ 1 - 0
packages/core/src/api/middleware/id-codec-plugin.ts

@@ -41,6 +41,7 @@ export class IdCodecPlugin implements ApolloServerPlugin {
                     'paymentId',
                     'paymentId',
                     'fulfillmentId',
                     'fulfillmentId',
                     'orderItemIds',
                     'orderItemIds',
+                    'promotionId',
                     'refundId',
                     'refundId',
                     'groupId',
                     'groupId',
                 ]);
                 ]);

+ 2 - 0
packages/core/src/api/schema/type/history-entry.type.graphql

@@ -14,6 +14,8 @@ enum HistoryEntryType {
     ORDER_CANCELLATION
     ORDER_CANCELLATION
     ORDER_REFUND_TRANSITION
     ORDER_REFUND_TRANSITION
     ORDER_NOTE
     ORDER_NOTE
+    ORDER_COUPON_APPLIED
+    ORDER_COUPON_REMOVED
 }
 }
 
 
 type HistoryEntryList implements PaginatedList {
 type HistoryEntryList implements PaginatedList {

+ 31 - 13
packages/core/src/service/services/history.service.ts

@@ -40,6 +40,13 @@ export type OrderHistoryEntryData = {
     [HistoryEntryType.ORDER_NOTE]: {
     [HistoryEntryType.ORDER_NOTE]: {
         note: string;
         note: string;
     };
     };
+    [HistoryEntryType.ORDER_COUPON_APPLIED]: {
+        couponCode: string;
+        promotionId: ID;
+    };
+    [HistoryEntryType.ORDER_COUPON_REMOVED]: {
+        couponCode: string;
+    };
 };
 };
 
 
 export interface CreateOrderHistoryEntryArgs<T extends keyof OrderHistoryEntryData> {
 export interface CreateOrderHistoryEntryArgs<T extends keyof OrderHistoryEntryData> {
@@ -54,26 +61,37 @@ export interface CreateOrderHistoryEntryArgs<T extends keyof OrderHistoryEntryDa
  */
  */
 @Injectable()
 @Injectable()
 export class HistoryService {
 export class HistoryService {
-    constructor(@InjectConnection() private connection: Connection,
-                private administratorService: AdministratorService,
-                private listQueryBuilder: ListQueryBuilder) {}
+    constructor(
+        @InjectConnection() private connection: Connection,
+        private administratorService: AdministratorService,
+        private listQueryBuilder: ListQueryBuilder,
+    ) {}
 
 
-    async getHistoryForOrder(orderId: ID, options?: HistoryEntryListOptions): Promise<PaginatedList<OrderHistoryEntry>> {
-        return this.listQueryBuilder.build(HistoryEntry as any as Type<OrderHistoryEntry>, options, {
-            where: {
-                order: { id: orderId } as any,
-            },
-            relations: ['administrator'],
-        }).getManyAndCount()
+    async getHistoryForOrder(
+        orderId: ID,
+        options?: HistoryEntryListOptions,
+    ): Promise<PaginatedList<OrderHistoryEntry>> {
+        return this.listQueryBuilder
+            .build((HistoryEntry as any) as Type<OrderHistoryEntry>, options, {
+                where: {
+                    order: { id: orderId } as any,
+                },
+                relations: ['administrator'],
+            })
+            .getManyAndCount()
             .then(([items, totalItems]) => ({
             .then(([items, totalItems]) => ({
                 items,
                 items,
                 totalItems,
                 totalItems,
             }));
             }));
     }
     }
 
 
-    async createHistoryEntryForOrder<T extends keyof OrderHistoryEntryData>(args: CreateOrderHistoryEntryArgs<T>): Promise<OrderHistoryEntry> {
-        const {ctx, data, orderId, type} = args;
-        const administrator = ctx.activeUserId ? await this.administratorService.findOneByUserId(ctx.activeUserId) : undefined;
+    async createHistoryEntryForOrder<T extends keyof OrderHistoryEntryData>(
+        args: CreateOrderHistoryEntryArgs<T>,
+    ): Promise<OrderHistoryEntry> {
+        const { ctx, data, orderId, type } = args;
+        const administrator = ctx.activeUserId
+            ? await this.administratorService.findOneByUserId(ctx.activeUserId)
+            : undefined;
         const entry = new OrderHistoryEntry({
         const entry = new OrderHistoryEntry({
             type,
             type,
             // TODO: figure out which should be public and not
             // TODO: figure out which should be public and not

+ 22 - 3
packages/core/src/service/services/order.service.ts

@@ -299,15 +299,34 @@ export class OrderService {
         if (order.couponCodes.includes(couponCode)) {
         if (order.couponCodes.includes(couponCode)) {
             return order;
             return order;
         }
         }
-        await this.promotionService.validateCouponCode(couponCode, order.customer && order.customer.id);
+        const promotion = await this.promotionService.validateCouponCode(
+            couponCode,
+            order.customer && order.customer.id,
+        );
         order.couponCodes.push(couponCode);
         order.couponCodes.push(couponCode);
+        await this.historyService.createHistoryEntryForOrder({
+            ctx,
+            orderId: order.id,
+            type: HistoryEntryType.ORDER_COUPON_APPLIED,
+            data: { couponCode, promotionId: promotion.id },
+        });
         return this.applyPriceAdjustments(ctx, order);
         return this.applyPriceAdjustments(ctx, order);
     }
     }
 
 
     async removeCouponCode(ctx: RequestContext, orderId: ID, couponCode: string) {
     async removeCouponCode(ctx: RequestContext, orderId: ID, couponCode: string) {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
-        order.couponCodes = order.couponCodes.filter(cc => cc !== couponCode);
-        return this.applyPriceAdjustments(ctx, order);
+        if (order.couponCodes.includes(couponCode)) {
+            order.couponCodes = order.couponCodes.filter(cc => cc !== couponCode);
+            await this.historyService.createHistoryEntryForOrder({
+                ctx,
+                orderId: order.id,
+                type: HistoryEntryType.ORDER_COUPON_REMOVED,
+                data: { couponCode },
+            });
+            return this.applyPriceAdjustments(ctx, order);
+        } else {
+            return order;
+        }
     }
     }
 
 
     async getOrderPromotions(orderId: ID): Promise<Promotion[]> {
     async getOrderPromotions(orderId: ID): Promise<Promotion[]> {

+ 2 - 2
packages/core/src/service/services/promotion.service.ts

@@ -134,7 +134,7 @@ export class PromotionService {
         };
         };
     }
     }
 
 
-    async validateCouponCode(couponCode: string, customerId?: ID): Promise<boolean> {
+    async validateCouponCode(couponCode: string, customerId?: ID): Promise<Promotion> {
         const promotion = await this.connection.getRepository(Promotion).findOne({
         const promotion = await this.connection.getRepository(Promotion).findOne({
             where: {
             where: {
                 couponCode,
                 couponCode,
@@ -154,7 +154,7 @@ export class PromotionService {
                 throw new CouponCodeLimitError(promotion.perCustomerUsageLimit);
                 throw new CouponCodeLimitError(promotion.perCustomerUsageLimit);
             }
             }
         }
         }
-        return true;
+        return promotion;
     }
     }
 
 
     async addPromotionsToOrder(order: Order): Promise<Order> {
     async addPromotionsToOrder(order: Order): Promise<Order> {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
schema-admin.json


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
schema-shop.json


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels