Sfoglia il codice sorgente

feat(core): Add option to CancelOrderInput to cancel of shipping

Relates to #1414. With this input being passed, we can now correctly cancel the entire
order including shipping charges, and re-evaluate the order totals to bring the
new total to zero.
Michael Bromley 3 anni fa
parent
commit
9eebae3734

+ 3 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -290,6 +291,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 

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

@@ -59,6 +59,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */

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

@@ -73,7 +73,8 @@ export type Adjustment = {
 
 export enum AdjustmentType {
   PROMOTION = 'PROMOTION',
-  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION'
+  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+  OTHER = 'OTHER'
 }
 
 export type Administrator = Node & {
@@ -293,6 +294,8 @@ export type CancelOrderInput = {
   orderId: Scalars['ID'];
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   lines?: Maybe<Array<OrderLineInput>>;
+  /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+  cancelShipping?: Maybe<Scalars['Boolean']>;
   reason?: Maybe<Scalars['String']>;
 };
 

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

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -290,6 +291,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 

+ 2 - 3
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -57,6 +57,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */
@@ -3439,9 +3440,7 @@ export type GetOrderByCodeWithPaymentsQueryVariables = Exact<{
 
 export type GetOrderByCodeWithPaymentsQuery = { orderByCode?: Maybe<TestOrderWithPaymentsFragment> };
 
-export type GetActiveCustomerOrderWithItemFulfillmentsQueryVariables = Exact<{
-    code: Scalars['String'];
-}>;
+export type GetActiveCustomerOrderWithItemFulfillmentsQueryVariables = Exact<{ [key: string]: never }>;
 
 export type GetActiveCustomerOrderWithItemFulfillmentsQuery = {
     activeCustomer?: Maybe<{

+ 25 - 1
packages/core/e2e/order.e2e-spec.ts

@@ -1055,7 +1055,7 @@ describe('Orders resolver', () => {
             await assertNoStockMovementsCreated(testOrder.product.id);
         });
 
-        it('cancel from PaymentAuthorized state', async () => {
+        it('cancel from PaymentAuthorized state with cancelShipping: true', async () => {
             const testOrder = await createTestOrder(
                 adminClient,
                 shopClient,
@@ -1087,6 +1087,7 @@ describe('Orders resolver', () => {
                 {
                     input: {
                         orderId: testOrder.orderId,
+                        cancelShipping: true,
                     },
                 },
             );
@@ -1107,6 +1108,8 @@ describe('Orders resolver', () => {
             });
             expect(order2!.active).toBe(false);
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.totalWithTax).toBe(0);
+            expect(order2!.shippingWithTax).toBe(0);
 
             const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -1339,6 +1342,7 @@ describe('Orders resolver', () => {
                     orderId,
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     reason: 'cancel reason 2',
+                    cancelShipping: true,
                 },
             });
 
@@ -1346,6 +1350,8 @@ describe('Orders resolver', () => {
                 id: orderId,
             });
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.shippingWithTax).toBe(0);
+            expect(order2!.totalWithTax).toBe(0);
 
             const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -1367,6 +1373,22 @@ describe('Orders resolver', () => {
             ]);
         });
 
+        it('cancelled OrderLine.unitPrice is not zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            expect(order?.lines[0].unitPrice).toEqual(order?.lines[0].items[0].unitPrice);
+        });
+
+        it('cancelled OrderLine.unitPrice is not zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            expect(order?.lines[0].unitPrice).toEqual(order?.lines[0].items[0].unitPrice);
+        });
+
         it('order history contains expected entries', async () => {
             const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
                 GET_ORDER_HISTORY,
@@ -1412,6 +1434,7 @@ describe('Orders resolver', () => {
                     data: {
                         orderItemIds: ['T_13'],
                         reason: 'cancel reason 1',
+                        shippingCancelled: false,
                     },
                 },
                 {
@@ -1419,6 +1442,7 @@ describe('Orders resolver', () => {
                     data: {
                         orderItemIds: ['T_14'],
                         reason: 'cancel reason 2',
+                        shippingCancelled: true,
                     },
                 },
                 {

+ 2 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -63,6 +63,8 @@ input CancelOrderInput {
     orderId: ID!
     "Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled"
     lines: [OrderLineInput!]
+    "Specify whether the shipping charges should also be cancelled. Defaults to false"
+    cancelShipping: Boolean
     reason: String
 }
 

+ 1 - 0
packages/core/src/api/schema/common/common-enums.graphql

@@ -7,6 +7,7 @@ enum GlobalFlag {
 enum AdjustmentType {
     PROMOTION
     DISTRIBUTED_ORDER_PROMOTION
+    OTHER
 }
 
 enum DeletionResult {

+ 11 - 1
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -416,7 +416,17 @@ export class OrderCalculator {
         }
     }
 
-    private calculateOrderTotals(order: Order) {
+    /**
+     * @description
+     * Sets the totals properties on an Order by summing each OrderLine, and taking
+     * into account any Surcharges and ShippingLines. Does not save the Order, so
+     * the entity must be persisted to the DB after calling this method.
+     *
+     * Note that this method does *not* evaluate any taxes or promotions. It assumes
+     * that has already been done and is solely responsible for summing the
+     * totals.
+     */
+    public calculateOrderTotals(order: Order) {
         let totalPrice = 0;
         let totalPriceWithTax = 0;
 

+ 1 - 0
packages/core/src/service/services/history.service.ts

@@ -86,6 +86,7 @@ export type OrderHistoryEntryData = {
     };
     [HistoryEntryType.ORDER_CANCELLATION]: {
         orderItemIds: ID[];
+        shippingCancelled: boolean;
         reason?: string;
     };
     [HistoryEntryType.ORDER_REFUND_TRANSITION]: {

+ 18 - 1
packages/core/src/service/services/order.service.ts

@@ -1260,8 +1260,23 @@ export class OrderService {
         await this.connection.getRepository(ctx, OrderItem).save(items, { reload: false });
 
         const orderWithItems = await this.connection.getEntityOrThrow(ctx, Order, order.id, {
-            relations: ['lines', 'lines.items'],
+            relations: ['lines', 'lines.items', 'surcharges', 'shippingLines'],
         });
+        if (input.cancelShipping === true) {
+            for (const shippingLine of orderWithItems.shippingLines) {
+                shippingLine.adjustments.push({
+                    adjustmentSource: 'CANCEL_ORDER',
+                    type: AdjustmentType.OTHER,
+                    description: 'shipping cancellation',
+                    amount: -shippingLine.discountedPriceWithTax,
+                });
+                this.connection.getRepository(ctx, ShippingLine).save(shippingLine, { reload: false });
+            }
+        }
+        // Update totals after cancellation
+        this.orderCalculator.calculateOrderTotals(orderWithItems);
+        await this.connection.getRepository(ctx, Order).save(orderWithItems, { reload: false });
+
         await this.historyService.createHistoryEntryForOrder({
             ctx,
             orderId: order.id,
@@ -1269,8 +1284,10 @@ export class OrderService {
             data: {
                 orderItemIds: items.map(i => i.id),
                 reason: input.reason || undefined,
+                shippingCancelled: !!input.cancelShipping,
             },
         });
+
         return orderItemsAreAllCancelled(orderWithItems);
     }
 

+ 3 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -290,6 +291,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 

+ 3 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -290,6 +291,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 

+ 1 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -57,6 +57,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff