Browse Source

feat(server): Implement setting of shipping method on an Order

Relates to #35
Michael Bromley 7 năm trước cách đây
mục cha
commit
f52e2e1566

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
schema.json


+ 29 - 0
server/src/api/resolvers/order.resolver.ts

@@ -8,6 +8,8 @@ import {
     Permission,
     RemoveItemFromOrderMutationArgs,
     SetOrderShippingAddressMutationArgs,
+    SetOrderShippingMethodMutationArgs,
+    ShippingMethodQuote,
     TransitionOrderToStateMutationArgs,
 } from 'shared/generated-types';
 import { PaginatedList } from 'shared/shared-types';
@@ -75,6 +77,33 @@ export class OrderResolver {
         }
     }
 
+    @Query()
+    @Allow(Permission.Owner)
+    async eligibleShippingMethods(@Ctx() ctx: RequestContext): Promise<ShippingMethodQuote[]> {
+        if (ctx.authorizedAsOwnerOnly) {
+            const sessionOrder = await this.getOrderFromContext(ctx);
+            if (sessionOrder) {
+                return this.orderService.getEligibleShippingMethods(ctx, sessionOrder.id);
+            }
+        }
+        return [];
+    }
+
+    @Mutation()
+    @Allow(Permission.Owner)
+    @Decode('shippingMethodId')
+    async setOrderShippingMethod(
+        @Ctx() ctx: RequestContext,
+        @Args() args: SetOrderShippingMethodMutationArgs,
+    ): Promise<Order | undefined> {
+        if (ctx.authorizedAsOwnerOnly) {
+            const sessionOrder = await this.getOrderFromContext(ctx);
+            if (sessionOrder) {
+                return this.orderService.setShippingMethod(ctx, sessionOrder.id, args.shippingMethodId);
+            }
+        }
+    }
+
     @Query()
     @Allow(Permission.Owner)
     async nextOrderStates(@Ctx() ctx: RequestContext): Promise<string[]> {

+ 8 - 0
server/src/api/types/order.api.graphql

@@ -3,6 +3,7 @@ type Query {
     activeOrder: Order
     nextOrderStates: [String!]!
     orders(options: OrderListOptions): OrderList!
+    eligibleShippingMethods: [ShippingMethodQuote!]!
 }
 
 type Mutation {
@@ -11,6 +12,7 @@ type Mutation {
     adjustItemQuantity(orderItemId: ID!, quantity: Int!): Order
     transitionOrderToState(state: String!): Order
     setOrderShippingAddress(input: CreateAddressInput!): Order
+    setOrderShippingMethod(shippingMethodId: ID!): Order
 }
 
 type OrderList implements PaginatedList {
@@ -18,6 +20,12 @@ type OrderList implements PaginatedList {
     totalItems: Int!
 }
 
+type ShippingMethodQuote {
+    shippingMethodId: ID!
+    price: Int!
+    description: String!
+}
+
 input OrderListOptions {
     take: Int
     skip: Int

+ 2 - 2
server/src/common/types/adjustment-source.ts

@@ -9,6 +9,6 @@ export abstract class AdjustmentSource extends VendureEntity {
         return `${this.type}:${this.id}`;
     }
 
-    abstract test(...args: any[]): boolean;
-    abstract apply(...args: any[]): Adjustment | undefined;
+    abstract test(...args: any[]): boolean | Promise<boolean>;
+    abstract apply(...args: any[]): Adjustment | undefined | Promise<Adjustment | undefined>;
 }

+ 2 - 2
server/src/config/shipping-method/shipping-calculator.ts

@@ -8,7 +8,7 @@ export type ShippingCalculatorArgs = AdjustmentArgs<ShippingCalculatorArgType>;
 export type CalculateShippingFn<T extends ShippingCalculatorArgs> = (
     order: Order,
     args: ArgumentValues<T>,
-) => number;
+) => number | Promise<number>;
 
 export class ShippingCalculator<T extends ShippingCalculatorArgs = {}> {
     readonly code: string;
@@ -23,7 +23,7 @@ export class ShippingCalculator<T extends ShippingCalculatorArgs = {}> {
         this.calculateFn = config.calculate;
     }
 
-    calculate(order: Order, args: AdjustmentArg[]): number {
+    calculate(order: Order, args: AdjustmentArg[]): number | Promise<number> {
         return this.calculateFn(order, argsArrayToHash(args));
     }
 }

+ 1 - 1
server/src/config/shipping-method/shipping-eligibility-checker.ts

@@ -8,7 +8,7 @@ export type ShippingEligibilityCheckerArgs = AdjustmentArgs<ShippingEligibilityC
 export type CheckShippingEligibilityCheckerFn<T extends ShippingEligibilityCheckerArgs> = (
     order: Order,
     args: ArgumentValues<T>,
-) => boolean;
+) => boolean | Promise<boolean>;
 
 export class ShippingEligibilityChecker<T extends ShippingEligibilityCheckerArgs = {}> {
     readonly code: string;

+ 3 - 3
server/src/entity/shipping-method/shipping-method.entity.ts

@@ -36,10 +36,10 @@ export class ShippingMethod extends AdjustmentSource implements ChannelAware {
     @JoinTable()
     channels: Channel[];
 
-    apply(order: Order): Adjustment | undefined {
+    async apply(order: Order): Promise<Adjustment | undefined> {
         const calculator = this.allCalculators[this.calculator.code];
         if (calculator) {
-            const amount = calculator.calculate(order, this.calculator.args);
+            const amount = await calculator.calculate(order, this.calculator.args);
             return {
                 amount,
                 type: this.type,
@@ -49,7 +49,7 @@ export class ShippingMethod extends AdjustmentSource implements ChannelAware {
         }
     }
 
-    test(order: Order): boolean {
+    async test(order: Order): Promise<boolean> {
         const checker = this.allCheckers[this.checker.code];
         if (checker) {
             return checker.check(order, this.checker.args);

+ 8 - 8
server/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -63,47 +63,47 @@ describe('OrderCalculator', () => {
     }
 
     describe('taxes only', () => {
-        it('single line with taxes not included', () => {
+        it('single line with taxes not included', async () => {
             const ctx = createRequestContext(false, zoneDefault);
             const order = createOrder({
                 lines: [{ unitPrice: 123, taxCategory: taxCategoryStandard, quantity: 1 }],
             });
-            orderCalculator.applyPriceAdjustments(ctx, order, []);
+            await orderCalculator.applyPriceAdjustments(ctx, order, []);
 
             expect(order.subTotal).toBe(148);
             expect(order.subTotalBeforeTax).toBe(123);
         });
 
-        it('single line with taxes not included, multiple items', () => {
+        it('single line with taxes not included, multiple items', async () => {
             const ctx = createRequestContext(false, zoneDefault);
             const order = createOrder({
                 lines: [{ unitPrice: 123, taxCategory: taxCategoryStandard, quantity: 3 }],
             });
-            orderCalculator.applyPriceAdjustments(ctx, order, []);
+            await orderCalculator.applyPriceAdjustments(ctx, order, []);
 
             expect(order.subTotal).toBe(444);
             expect(order.subTotalBeforeTax).toBe(369);
         });
 
-        it('single line with taxes included', () => {
+        it('single line with taxes included', async () => {
             const ctx = createRequestContext(true, zoneDefault);
             const order = createOrder({
                 lines: [{ unitPrice: 123, taxCategory: taxCategoryStandard, quantity: 1 }],
             });
-            orderCalculator.applyPriceAdjustments(ctx, order, []);
+            await orderCalculator.applyPriceAdjustments(ctx, order, []);
 
             expect(order.subTotal).toBe(123);
             expect(order.subTotalBeforeTax).toBe(102);
         });
 
-        it('resets totals when lines array is empty', () => {
+        it('resets totals when lines array is empty', async () => {
             const ctx = createRequestContext(true, zoneDefault);
             const order = createOrder({
                 lines: [],
                 subTotal: 148,
                 subTotalBeforeTax: 123,
             });
-            orderCalculator.applyPriceAdjustments(ctx, order, []);
+            await orderCalculator.applyPriceAdjustments(ctx, order, []);
 
             expect(order.subTotal).toBe(0);
             expect(order.subTotalBeforeTax).toBe(0);

+ 21 - 6
server/src/service/helpers/order-calculator/order-calculator.ts

@@ -1,9 +1,12 @@
 import { Injectable } from '@nestjs/common';
 import { AdjustmentType } from 'shared/generated-types';
+import { ID } from 'shared/shared-types';
 
 import { RequestContext } from '../../../api/common/request-context';
+import { idsAreEqual } from '../../../common/utils';
 import { Order } from '../../../entity/order/order.entity';
 import { Promotion } from '../../../entity/promotion/promotion.entity';
+import { ShippingMethod } from '../../../entity/shipping-method/shipping-method.entity';
 import { Zone } from '../../../entity/zone/zone.entity';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
@@ -20,7 +23,12 @@ export class OrderCalculator {
     /**
      * Applies taxes and promotions to an Order. Mutates the order object.
      */
-    applyPriceAdjustments(ctx: RequestContext, order: Order, promotions: Promotion[]): Order {
+    async applyPriceAdjustments(
+        ctx: RequestContext,
+        order: Order,
+        promotions: Promotion[],
+        preferredShippingMethod?: ID,
+    ): Promise<Order> {
         const activeZone = ctx.channel.defaultTaxZone;
         order.clearAdjustments();
         if (order.lines.length) {
@@ -31,7 +39,7 @@ export class OrderCalculator {
             // Finally, re-calculate taxes because the promotions may have
             // altered the unit prices, which in turn will alter the tax payable.
             this.applyTaxes(ctx, order, activeZone);
-            this.applyShipping(ctx, order);
+            await this.applyShipping(ctx, order, preferredShippingMethod);
         } else {
             this.calculateOrderTotals(order);
         }
@@ -101,11 +109,18 @@ export class OrderCalculator {
         }
     }
 
-    private applyShipping(ctx: RequestContext, order: Order) {
-        const results = this.shippingCalculator.getEligibleShippingMethods(ctx, order);
+    private async applyShipping(ctx: RequestContext, order: Order, preferredShippingMethod?: ID) {
+        const results = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         if (results && results.length) {
-            order.shipping = results[0].price;
-            order.shippingMethod = results[0].method.description;
+            let selected: { method: ShippingMethod; price: number } | undefined;
+            if (preferredShippingMethod) {
+                selected = results.find(r => idsAreEqual(r.method.id, preferredShippingMethod));
+            }
+            if (!selected) {
+                selected = results[0];
+            }
+            order.shipping = selected.price;
+            order.shippingMethod = selected.method.description;
         }
     }
 

+ 2 - 2
server/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -35,9 +35,9 @@ export class OrderStateMachine {
      * Specific business logic to be executed on Order state transitions.
      */
     private onTransitionStart(fromState: OrderState, toState: OrderState, data: OrderTransitionData) {
-        if (toState === 'ArrangingShipping') {
+        if (toState === 'ArrangingPayment') {
             if (data.order.lines.length === 0) {
-                return `error.cannot-transition-to-shipping-when-order-is-empty`;
+                return `error.cannot-transition-to-payment-when-order-is-empty`;
             }
         }
     }

+ 13 - 15
server/src/service/helpers/shipping-calculator/shipping-calculator.ts

@@ -14,23 +14,21 @@ export class ShippingCalculator {
      * Returns an array of each eligible ShippingMethod for the given Order and sorts them by
      * price, with the cheapest first.
      */
-    getEligibleShippingMethods(
+    async getEligibleShippingMethods(
         ctx: RequestContext,
         order: Order,
-    ): Array<{ method: ShippingMethod; price: number }> {
+    ): Promise<Array<{ method: ShippingMethod; price: number }>> {
         const shippingMethods = this.shippingMethodService.getActiveShippingMethods(ctx.channel);
-        return shippingMethods
-            .filter(sm => sm.test(order))
-            .map(sm => {
-                const adjustment = sm.apply(order);
-                if (adjustment) {
-                    return {
-                        method: sm,
-                        price: adjustment.amount,
-                    };
-                }
-            })
-            .filter(notNullOrUndefined)
-            .sort((a, b) => a.price - b.price);
+        const methodsPromiseArray = shippingMethods.filter(async sm => await sm.test(order)).map(async sm => {
+            const adjustment = await sm.apply(order);
+            if (adjustment) {
+                return {
+                    method: sm,
+                    price: adjustment.amount,
+                };
+            }
+        });
+        const methods = await Promise.all(methodsPromiseArray);
+        return methods.filter(notNullOrUndefined).sort((a, b) => a.price - b.price);
     }
 }

+ 30 - 4
server/src/service/services/order.service.ts

@@ -1,5 +1,5 @@
 import { InjectConnection } from '@nestjs/typeorm';
-import { CreateAddressInput } from 'shared/generated-types';
+import { CreateAddressInput, ShippingMethodQuote } from 'shared/generated-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
@@ -19,6 +19,7 @@ import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
+import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { CustomerService } from './customer.service';
@@ -30,6 +31,7 @@ export class OrderService {
         private productVariantService: ProductVariantService,
         private customerService: CustomerService,
         private orderCalculator: OrderCalculator,
+        private shippingCalculator: ShippingCalculator,
         private orderStateMachine: OrderStateMachine,
         private orderMerger: OrderMerger,
         private listQueryBuilder: ListQueryBuilder,
@@ -172,7 +174,22 @@ export class OrderService {
     async setShippingAddress(ctx: RequestContext, orderId: ID, input: CreateAddressInput): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         order.shippingAddress = input;
-        await this.applyPriceAdjustments(ctx, order);
+        return this.connection.getRepository(Order).save(order);
+    }
+
+    async getEligibleShippingMethods(ctx: RequestContext, orderId: ID): Promise<ShippingMethodQuote[]> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
+        return eligibleMethods.map(result => ({
+            shippingMethodId: result.method.id as string,
+            price: result.price,
+            description: result.method.description,
+        }));
+    }
+
+    async setShippingMethod(ctx: RequestContext, orderId: ID, shippingMethodId: ID): Promise<Order> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        await this.applyPriceAdjustments(ctx, order, shippingMethodId);
         return this.connection.getRepository(Order).save(order);
     }
 
@@ -262,12 +279,21 @@ export class OrderService {
     /**
      * Applies promotions, taxes and shipping to the Order.
      */
-    private async applyPriceAdjustments(ctx: RequestContext, order: Order): Promise<Order> {
+    private async applyPriceAdjustments(
+        ctx: RequestContext,
+        order: Order,
+        preferredShippingMethod?: ID,
+    ): Promise<Order> {
         const promotions = await this.connection.getRepository(Promotion).find({
             where: { enabled: true },
             order: { priorityScore: 'ASC' },
         });
-        order = this.orderCalculator.applyPriceAdjustments(ctx, order, promotions);
+        order = await this.orderCalculator.applyPriceAdjustments(
+            ctx,
+            order,
+            promotions,
+            preferredShippingMethod,
+        );
         await this.connection.getRepository(Order).save(order);
         await this.connection.getRepository(OrderItem).save(order.getOrderItems());
         await this.connection.getRepository(OrderLine).save(order.lines);

+ 44 - 0
shared/generated-types.ts

@@ -62,6 +62,7 @@ export interface Query {
     activeOrder?: Order | null;
     nextOrderStates: string[];
     orders: OrderList;
+    eligibleShippingMethods: ShippingMethodQuote[];
     productOptionGroups: ProductOptionGroup[];
     productOptionGroup?: ProductOptionGroup | null;
     products: ProductList;
@@ -418,6 +419,12 @@ export interface OrderList extends PaginatedList {
     totalItems: number;
 }
 
+export interface ShippingMethodQuote {
+    shippingMethodId: string;
+    price: number;
+    description: string;
+}
+
 export interface ProductOptionGroup extends Node {
     id: string;
     createdAt: DateTime;
@@ -579,6 +586,7 @@ export interface Mutation {
     adjustItemQuantity?: Order | null;
     transitionOrderToState?: Order | null;
     setOrderShippingAddress?: Order | null;
+    setOrderShippingMethod?: Order | null;
     createProductOptionGroup: ProductOptionGroup;
     updateProductOptionGroup: ProductOptionGroup;
     createProduct: Product;
@@ -1456,6 +1464,9 @@ export interface TransitionOrderToStateMutationArgs {
 export interface SetOrderShippingAddressMutationArgs {
     input: CreateAddressInput;
 }
+export interface SetOrderShippingMethodMutationArgs {
+    shippingMethodId: string;
+}
 export interface CreateProductOptionGroupMutationArgs {
     input: CreateProductOptionGroupInput;
 }
@@ -1801,6 +1812,7 @@ export namespace QueryResolvers {
         activeOrder?: ActiveOrderResolver<Order | null, any, Context>;
         nextOrderStates?: NextOrderStatesResolver<string[], any, Context>;
         orders?: OrdersResolver<OrderList, any, Context>;
+        eligibleShippingMethods?: EligibleShippingMethodsResolver<ShippingMethodQuote[], any, Context>;
         productOptionGroups?: ProductOptionGroupsResolver<ProductOptionGroup[], any, Context>;
         productOptionGroup?: ProductOptionGroupResolver<ProductOptionGroup | null, any, Context>;
         products?: ProductsResolver<ProductList, any, Context>;
@@ -2008,6 +2020,11 @@ export namespace QueryResolvers {
         options?: OrderListOptions | null;
     }
 
+    export type EligibleShippingMethodsResolver<
+        R = ShippingMethodQuote[],
+        Parent = any,
+        Context = any
+    > = Resolver<R, Parent, Context>;
     export type ProductOptionGroupsResolver<R = ProductOptionGroup[], Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -3141,6 +3158,22 @@ export namespace OrderListResolvers {
     export type TotalItemsResolver<R = number, Parent = any, Context = any> = Resolver<R, Parent, Context>;
 }
 
+export namespace ShippingMethodQuoteResolvers {
+    export interface Resolvers<Context = any> {
+        shippingMethodId?: ShippingMethodIdResolver<string, any, Context>;
+        price?: PriceResolver<number, any, Context>;
+        description?: DescriptionResolver<string, any, Context>;
+    }
+
+    export type ShippingMethodIdResolver<R = string, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+    export type PriceResolver<R = number, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type DescriptionResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+}
+
 export namespace ProductOptionGroupResolvers {
     export interface Resolvers<Context = any> {
         id?: IdResolver<string, any, Context>;
@@ -3540,6 +3573,7 @@ export namespace MutationResolvers {
         adjustItemQuantity?: AdjustItemQuantityResolver<Order | null, any, Context>;
         transitionOrderToState?: TransitionOrderToStateResolver<Order | null, any, Context>;
         setOrderShippingAddress?: SetOrderShippingAddressResolver<Order | null, any, Context>;
+        setOrderShippingMethod?: SetOrderShippingMethodResolver<Order | null, any, Context>;
         createProductOptionGroup?: CreateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         updateProductOptionGroup?: UpdateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         createProduct?: CreateProductResolver<Product, any, Context>;
@@ -3844,6 +3878,16 @@ export namespace MutationResolvers {
         input: CreateAddressInput;
     }
 
+    export type SetOrderShippingMethodResolver<R = Order | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        SetOrderShippingMethodArgs
+    >;
+    export interface SetOrderShippingMethodArgs {
+        shippingMethodId: string;
+    }
+
     export type CreateProductOptionGroupResolver<
         R = ProductOptionGroup,
         Parent = any,

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác