Parcourir la source

feat(core): Allow order shipping method to be modified

Closes #978
Michael Bromley il y a 2 ans
Parent
commit
400d78aa8b

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

@@ -2499,6 +2499,8 @@ export type ModifyOrderInput = {
    */
   refund?: InputMaybe<AdministratorRefundInput>;
   refunds?: InputMaybe<Array<AdministratorRefundInput>>;
+  /** Added in v2.2 */
+  shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   surcharges?: InputMaybe<Array<SurchargeInput>>;
   updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
   updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
@@ -2509,7 +2511,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
-export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
+export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID']['input'];

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

@@ -2580,6 +2580,8 @@ export type ModifyOrderInput = {
    */
   refund?: InputMaybe<AdministratorRefundInput>;
   refunds?: InputMaybe<Array<AdministratorRefundInput>>;
+  /** Added in v2.2 */
+  shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   surcharges?: InputMaybe<Array<SurchargeInput>>;
   updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
   updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
@@ -2590,7 +2592,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
-export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
+export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID']['input'];

Fichier diff supprimé car celui-ci est trop grand
+ 5 - 3
packages/core/e2e/graphql/generated-e2e-admin-types.ts


+ 85 - 4
packages/core/e2e/order-modification.e2e-spec.ts

@@ -70,18 +70,24 @@ const SHIPPING_OTHER = 750;
 const testCalculator = new ShippingCalculator({
     code: 'test-calculator',
     description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
-    args: {},
+    args: {
+        surcharge: {
+            type: 'int',
+            defaultValue: 0,
+        },
+    },
     calculate: (ctx, order, args) => {
         let price;
+        const surcharge = args.surcharge || 0;
         switch (order.shippingAddress.countryCode) {
             case 'GB':
-                price = SHIPPING_GB;
+                price = SHIPPING_GB + surcharge;
                 break;
             case 'US':
-                price = SHIPPING_US;
+                price = SHIPPING_US + surcharge;
                 break;
             default:
-                price = SHIPPING_OTHER;
+                price = SHIPPING_OTHER + surcharge;
         }
         return {
             price,
@@ -113,6 +119,7 @@ describe('Order modification', () => {
 
     let orderId: string;
     let testShippingMethodId: string;
+    let testExpressShippingMethodId: string;
     const orderGuard: ErrorResultGuard<
         UpdatedOrderFragment | OrderWithModificationsFragment | OrderFragment
     > = createErrorResultGuard(input => !!input.id);
@@ -186,6 +193,38 @@ describe('Order modification', () => {
         });
         testShippingMethodId = createShippingMethod.id;
 
+        const { createShippingMethod: shippingMethod2 } = await adminClient.query<
+            Codegen.CreateShippingMethodMutation,
+            Codegen.CreateShippingMethodMutationVariables
+        >(CREATE_SHIPPING_METHOD, {
+            input: {
+                code: 'new-method-express',
+                fulfillmentHandler: manualFulfillmentHandler.code,
+                checker: {
+                    code: defaultShippingEligibilityChecker.code,
+                    arguments: [
+                        {
+                            name: 'orderMinimum',
+                            value: '0',
+                        },
+                    ],
+                },
+                calculator: {
+                    code: testCalculator.code,
+                    arguments: [
+                        {
+                            name: 'surcharge',
+                            value: '500',
+                        },
+                    ],
+                },
+                translations: [
+                    { languageCode: LanguageCode.en, name: 'test method express', description: '' },
+                ],
+            },
+        });
+        testExpressShippingMethodId = shippingMethod2.id;
+
         // create an order and check out
         await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
         await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
@@ -663,6 +702,40 @@ describe('Order modification', () => {
             await assertOrderIsUnchanged(order!);
         });
 
+        it('changing shipping method', async () => {
+            const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
+                GET_ORDER,
+                {
+                    id: orderId,
+                },
+            );
+            const { modifyOrder } = await adminClient.query<
+                Codegen.ModifyOrderMutation,
+                Codegen.ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: true,
+                    orderId,
+                    shippingMethodIds: [testExpressShippingMethodId],
+                },
+            });
+            orderGuard.assertSuccess(modifyOrder);
+
+            const expectedTotal = order!.totalWithTax + 500;
+            expect(modifyOrder.totalWithTax).toBe(expectedTotal);
+            expect(modifyOrder.shippingLines).toEqual([
+                {
+                    id: 'T_1',
+                    discountedPriceWithTax: 1500,
+                    shippingMethod: {
+                        id: testExpressShippingMethodId,
+                        name: 'test method express',
+                    },
+                },
+            ]);
+            await assertOrderIsUnchanged(order!);
+        });
+
         it('does not add a history entry', async () => {
             const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
                 GET_ORDER,
@@ -2511,6 +2584,14 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
             countryCode
             country
         }
+        shippingLines {
+            id
+            discountedPriceWithTax
+            shippingMethod {
+                id
+                name
+            }
+        }
     }
 `;
 

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

@@ -197,6 +197,10 @@ input ModifyOrderInput {
     refunds: [AdministratorRefundInput!]
     options: ModifyOrderOptions
     couponCodes: [String!]
+    """
+    Added in v2.2
+    """
+    shippingMethodIds: [ID!]
 }
 
 input AddItemInput {
@@ -452,5 +456,6 @@ union ModifyOrderResult =
     | CouponCodeExpiredError
     | CouponCodeInvalidError
     | CouponCodeLimitError
+    | IneligibleShippingMethodError
 union AddManualPaymentToOrderResult = Order | ManualPaymentStateError
 union SetCustomerForDraftOrderResult = Order | EmailAddressConflictError

+ 77 - 6
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -27,20 +27,21 @@ import {
     RefundPaymentIdMissingError,
 } from '../../../common/error/generated-graphql-admin-errors';
 import {
+    IneligibleShippingMethodError,
     InsufficientStockError,
     NegativeQuantityError,
     OrderLimitError,
 } from '../../../common/error/generated-graphql-shop-errors';
-import { assertFound, idsAreEqual } from '../../../common/utils';
+import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { CustomFieldConfig } from '../../../config/custom-field/custom-field-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { Order } from '../../../entity/order/order.entity';
-import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { FulfillmentLine } from '../../../entity/order-line-reference/fulfillment-line.entity';
 import { OrderModificationLine } from '../../../entity/order-line-reference/order-modification-line.entity';
+import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
+import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
@@ -58,8 +59,8 @@ import { ProductVariantService } from '../../services/product-variant.service';
 import { PromotionService } from '../../services/promotion.service';
 import { StockMovementService } from '../../services/stock-movement.service';
 import { CustomFieldRelationService } from '../custom-field-relation/custom-field-relation.service';
-import { EntityHydrator } from '../entity-hydrator/entity-hydrator.service';
 import { OrderCalculator } from '../order-calculator/order-calculator';
+import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
 import { TranslatorService } from '../translator/translator.service';
 import { getOrdersFromLines, orderLinesAreAllCancelled } from '../utils/order-utils';
 import { patchEntity } from '../utils/patch-entity';
@@ -90,7 +91,7 @@ export class OrderModifier {
         private customFieldRelationService: CustomFieldRelationService,
         private promotionService: PromotionService,
         private eventBus: EventBus,
-        private entityHydrator: EntityHydrator,
+        private shippingCalculator: ShippingCalculator,
         private historyService: HistoryService,
         private translator: TranslatorService,
     ) {}
@@ -597,6 +598,12 @@ export class OrderModifier {
         const updatedOrderLines = order.lines.filter(l => updatedOrderLineIds.includes(l.id));
         const promotions = await this.promotionService.getActivePromotionsInChannel(ctx);
         const activePromotionsPre = await this.promotionService.getActivePromotionsOnOrder(ctx, order.id);
+        if (input.shippingMethodIds) {
+            const result = await this.setShippingMethods(ctx, order, input.shippingMethodIds);
+            if (isGraphQlErrorResult(result)) {
+                return result;
+            }
+        }
         await this.orderCalculator.applyPriceAdjustments(ctx, order, promotions, updatedOrderLines, {
             recalculateShipping: input.options?.recalculateShipping,
         });
@@ -657,6 +664,69 @@ export class OrderModifier {
         return { order, modification: createdModification };
     }
 
+    async setShippingMethods(ctx: RequestContext, order: Order, shippingMethodIds: ID[]) {
+        for (const [i, shippingMethodId] of shippingMethodIds.entries()) {
+            const shippingMethod = await this.shippingCalculator.getMethodIfEligible(
+                ctx,
+                order,
+                shippingMethodId,
+            );
+            if (!shippingMethod) {
+                return new IneligibleShippingMethodError();
+            }
+            let shippingLine: ShippingLine | undefined = order.shippingLines[i];
+            if (shippingLine) {
+                shippingLine.shippingMethod = shippingMethod;
+                shippingLine.shippingMethodId = shippingMethod.id;
+            } else {
+                shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
+                    new ShippingLine({
+                        shippingMethod,
+                        order,
+                        adjustments: [],
+                        listPrice: 0,
+                        listPriceIncludesTax: ctx.channel.pricesIncludeTax,
+                        taxLines: [],
+                    }),
+                );
+                if (order.shippingLines) {
+                    order.shippingLines.push(shippingLine);
+                } else {
+                    order.shippingLines = [shippingLine];
+                }
+            }
+
+            await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
+        }
+        // remove any now-unused ShippingLines
+        if (shippingMethodIds.length < order.shippingLines.length) {
+            const shippingLinesToDelete = order.shippingLines.splice(shippingMethodIds.length - 1);
+            await this.connection.getRepository(ctx, ShippingLine).remove(shippingLinesToDelete);
+        }
+        // assign the ShippingLines to the OrderLines
+        await this.connection
+            .getRepository(ctx, OrderLine)
+            .createQueryBuilder('line')
+            .update({ shippingLine: undefined })
+            .whereInIds(order.lines.map(l => l.id))
+            .execute();
+        const { shippingLineAssignmentStrategy } = this.configService.shippingOptions;
+        for (const shippingLine of order.shippingLines) {
+            const orderLinesForShippingLine =
+                await shippingLineAssignmentStrategy.assignShippingLineToOrderLines(ctx, shippingLine, order);
+            await this.connection
+                .getRepository(ctx, OrderLine)
+                .createQueryBuilder('line')
+                .update({ shippingLineId: shippingLine.id })
+                .whereInIds(orderLinesForShippingLine.map(l => l.id))
+                .execute();
+            orderLinesForShippingLine.forEach(line => {
+                line.shippingLine = shippingLine;
+            });
+        }
+        return order;
+    }
+
     private noChangesSpecified(input: ModifyOrderInput): boolean {
         const noChanges =
             !input.adjustOrderLines?.length &&
@@ -665,7 +735,8 @@ export class OrderModifier {
             !input.updateShippingAddress &&
             !input.updateBillingAddress &&
             !input.couponCodes &&
-            !(input as any).customFields;
+            !(input as any).customFields &&
+            (!input.shippingMethodIds || input.shippingMethodIds.length === 0);
         return noChanges;
     }
 

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

@@ -62,7 +62,6 @@ import {
     SettlePaymentError,
 } from '../../common/error/generated-graphql-admin-errors';
 import {
-    IneligibleShippingMethodError,
     InsufficientStockError,
     NegativeQuantityError,
     OrderLimitError,
@@ -110,7 +109,6 @@ import { OrderModifier } from '../helpers/order-modifier/order-modifier';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
 import { PaymentState } from '../helpers/payment-state-machine/payment-state';
-import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { TranslatorService } from '../helpers/translator/translator.service';
@@ -127,7 +125,6 @@ import { PaymentService } from './payment.service';
 import { ProductVariantService } from './product-variant.service';
 import { PromotionService } from './promotion.service';
 import { StockLevelService } from './stock-level.service';
-import { StockMovementService } from './stock-movement.service';
 
 /**
  * @description
@@ -148,11 +145,9 @@ export class OrderService {
         private orderStateMachine: OrderStateMachine,
         private orderMerger: OrderMerger,
         private paymentService: PaymentService,
-        private paymentStateMachine: PaymentStateMachine,
         private paymentMethodService: PaymentMethodService,
         private fulfillmentService: FulfillmentService,
         private listQueryBuilder: ListQueryBuilder,
-        private stockMovementService: StockMovementService,
         private refundStateMachine: RefundStateMachine,
         private historyService: HistoryService,
         private promotionService: PromotionService,
@@ -919,64 +914,9 @@ export class OrderService {
         if (validationError) {
             return validationError;
         }
-        for (const [i, shippingMethodId] of shippingMethodIds.entries()) {
-            const shippingMethod = await this.shippingCalculator.getMethodIfEligible(
-                ctx,
-                order,
-                shippingMethodId,
-            );
-            if (!shippingMethod) {
-                return new IneligibleShippingMethodError();
-            }
-            let shippingLine: ShippingLine | undefined = order.shippingLines[i];
-            if (shippingLine) {
-                shippingLine.shippingMethod = shippingMethod;
-                shippingLine.shippingMethodId = shippingMethod.id;
-            } else {
-                shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
-                    new ShippingLine({
-                        shippingMethod,
-                        order,
-                        adjustments: [],
-                        listPrice: 0,
-                        listPriceIncludesTax: ctx.channel.pricesIncludeTax,
-                        taxLines: [],
-                    }),
-                );
-                if (order.shippingLines) {
-                    order.shippingLines.push(shippingLine);
-                } else {
-                    order.shippingLines = [shippingLine];
-                }
-            }
-
-            await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
-        }
-        // remove any now-unused ShippingLines
-        if (shippingMethodIds.length < order.shippingLines.length) {
-            const shippingLinesToDelete = order.shippingLines.splice(shippingMethodIds.length - 1);
-            await this.connection.getRepository(ctx, ShippingLine).remove(shippingLinesToDelete);
-        }
-        // assign the ShippingLines to the OrderLines
-        await this.connection
-            .getRepository(ctx, OrderLine)
-            .createQueryBuilder('line')
-            .update({ shippingLine: undefined })
-            .whereInIds(order.lines.map(l => l.id))
-            .execute();
-        const { shippingLineAssignmentStrategy } = this.configService.shippingOptions;
-        for (const shippingLine of order.shippingLines) {
-            const orderLinesForShippingLine =
-                await shippingLineAssignmentStrategy.assignShippingLineToOrderLines(ctx, shippingLine, order);
-            await this.connection
-                .getRepository(ctx, OrderLine)
-                .createQueryBuilder('line')
-                .update({ shippingLineId: shippingLine.id })
-                .whereInIds(orderLinesForShippingLine.map(l => l.id))
-                .execute();
-            orderLinesForShippingLine.forEach(line => {
-                line.shippingLine = shippingLine;
-            });
+        const result = await this.orderModifier.setShippingMethods(ctx, order, shippingMethodIds);
+        if (isGraphQlErrorResult(result)) {
+            return result;
         }
         const updatedOrder = await this.getOrderOrThrow(ctx, orderId);
         await this.applyPriceAdjustments(ctx, updatedOrder);

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

@@ -2499,6 +2499,8 @@ export type ModifyOrderInput = {
    */
   refund?: InputMaybe<AdministratorRefundInput>;
   refunds?: InputMaybe<Array<AdministratorRefundInput>>;
+  /** Added in v2.2 */
+  shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   surcharges?: InputMaybe<Array<SurchargeInput>>;
   updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
   updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
@@ -2509,7 +2511,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
-export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
+export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID']['input'];

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

@@ -2499,6 +2499,8 @@ export type ModifyOrderInput = {
    */
   refund?: InputMaybe<AdministratorRefundInput>;
   refunds?: InputMaybe<Array<AdministratorRefundInput>>;
+  /** Added in v2.2 */
+  shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   surcharges?: InputMaybe<Array<SurchargeInput>>;
   updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
   updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
@@ -2509,7 +2511,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
-export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
+export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID']['input'];

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-admin.json


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff