Просмотр исходного кода

feat(server): implement setCustomerForOrder mutation

Michael Bromley 7 лет назад
Родитель
Сommit
169f709961

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
schema.json


+ 21 - 1
server/src/api/resolvers/order.resolver.ts

@@ -8,6 +8,7 @@ import {
     OrdersQueryArgs,
     OrdersQueryArgs,
     Permission,
     Permission,
     RemoveItemFromOrderMutationArgs,
     RemoveItemFromOrderMutationArgs,
+    SetCustomerForOrderMutationArgs,
     SetOrderShippingAddressMutationArgs,
     SetOrderShippingAddressMutationArgs,
     SetOrderShippingMethodMutationArgs,
     SetOrderShippingMethodMutationArgs,
     ShippingMethodQuote,
     ShippingMethodQuote,
@@ -19,6 +20,7 @@ import { Order } from '../../entity/order/order.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { I18nError } from '../../i18n/i18n-error';
 import { OrderState } from '../../service/helpers/order-state-machine/order-state';
 import { OrderState } from '../../service/helpers/order-state-machine/order-state';
 import { AuthService } from '../../service/services/auth.service';
 import { AuthService } from '../../service/services/auth.service';
+import { CustomerService } from '../../service/services/customer.service';
 import { OrderService } from '../../service/services/order.service';
 import { OrderService } from '../../service/services/order.service';
 import { ShippingMethodService } from '../../service/services/shipping-method.service';
 import { ShippingMethodService } from '../../service/services/shipping-method.service';
 import { IdCodecService } from '../common/id-codec.service';
 import { IdCodecService } from '../common/id-codec.service';
@@ -32,6 +34,7 @@ export class OrderResolver {
     constructor(
     constructor(
         private orderService: OrderService,
         private orderService: OrderService,
         private shippingMethodService: ShippingMethodService,
         private shippingMethodService: ShippingMethodService,
+        private customerService: CustomerService,
         private authService: AuthService,
         private authService: AuthService,
         private idCodecService: IdCodecService,
         private idCodecService: IdCodecService,
     ) {}
     ) {}
@@ -95,7 +98,12 @@ export class OrderResolver {
     ): Promise<Order | undefined> {
     ): Promise<Order | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
         if (ctx.authorizedAsOwnerOnly) {
             const order = await this.orderService.findOneByCode(ctx, args.code);
             const order = await this.orderService.findOneByCode(ctx, args.code);
-            if (order && order.customer.user && order.customer.user.id === ctx.activeUserId) {
+            if (
+                order &&
+                order.customer &&
+                order.customer.user &&
+                order.customer.user.id === ctx.activeUserId
+            ) {
                 return this.orderService.findOne(ctx, order.id);
                 return this.orderService.findOne(ctx, order.id);
             } else {
             } else {
                 throw new I18nError(`error.forbidden`);
                 throw new I18nError(`error.forbidden`);
@@ -222,6 +230,18 @@ export class OrderResolver {
         }
         }
     }
     }
 
 
+    @Mutation()
+    @Allow(Permission.Owner)
+    async setCustomerForOrder(@Ctx() ctx: RequestContext, @Args() args: SetCustomerForOrderMutationArgs) {
+        if (ctx.authorizedAsOwnerOnly) {
+            const sessionOrder = await this.getOrderFromContext(ctx);
+            if (sessionOrder) {
+                const customer = await this.customerService.createOrUpdate(args.input);
+                return this.orderService.addCustomerToOrder(ctx, sessionOrder.id, customer);
+            }
+        }
+    }
+
     private async getOrderFromContext(ctx: RequestContext): Promise<Order | undefined>;
     private async getOrderFromContext(ctx: RequestContext): Promise<Order | undefined>;
     private async getOrderFromContext(ctx: RequestContext, createIfNotExists: true): Promise<Order>;
     private async getOrderFromContext(ctx: RequestContext, createIfNotExists: true): Promise<Order>;
     private async getOrderFromContext(
     private async getOrderFromContext(

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

@@ -15,6 +15,7 @@ type Mutation {
     setOrderShippingAddress(input: CreateAddressInput!): Order
     setOrderShippingAddress(input: CreateAddressInput!): Order
     setOrderShippingMethod(shippingMethodId: ID!): Order
     setOrderShippingMethod(shippingMethodId: ID!): Order
     addPaymentToOrder(input: PaymentInput!): Order
     addPaymentToOrder(input: PaymentInput!): Order
+    setCustomerForOrder(input: CreateCustomerInput!): Order
 }
 }
 
 
 type OrderList implements PaginatedList {
 type OrderList implements PaginatedList {

+ 11 - 0
server/src/common/utils.ts

@@ -51,3 +51,14 @@ export function getAssetType(mimeType: string): AssetType {
             return AssetType.BINARY;
             return AssetType.BINARY;
     }
     }
 }
 }
+
+/**
+ * A simple normalization for email addresses. Lowercases the whole address,
+ * even though technically the local part (before the '@') is case-sensitive
+ * per the spec. In practice, however, it seems safe to treat emails as
+ * case-insensitive to allow for users who might vary the usage of
+ * upper/lower case. See more discussion here: https://ux.stackexchange.com/a/16849
+ */
+export function normalizeEmailAddress(input: string): string {
+    return input.trim().toLowerCase();
+}

+ 1 - 1
server/src/entity/order/order.entity.ts

@@ -26,7 +26,7 @@ export class Order extends VendureEntity {
     active: boolean;
     active: boolean;
 
 
     @ManyToOne(type => Customer)
     @ManyToOne(type => Customer)
-    customer: Customer;
+    customer?: Customer;
 
 
     @OneToMany(type => OrderLine, line => line.order)
     @OneToMany(type => OrderLine, line => line.order)
     lines: OrderLine[];
     lines: OrderLine[];

+ 1 - 0
server/src/i18n/messages/en.json

@@ -3,6 +3,7 @@
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
     "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-to-shipping-when-order-is-empty": "Cannot transition Order to the ArrangingShipping state when it is empty",
     "cannot-transition-to-shipping-when-order-is-empty": "Cannot transition Order to the ArrangingShipping state when it is empty",
+    "cannot-transition-to-payment-without-customer": "Cannot transition Order to the ArrangingShipping without Customer details",
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",

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

@@ -39,6 +39,9 @@ export class OrderStateMachine {
             if (data.order.lines.length === 0) {
             if (data.order.lines.length === 0) {
                 return `error.cannot-transition-to-payment-when-order-is-empty`;
                 return `error.cannot-transition-to-payment-when-order-is-empty`;
             }
             }
+            if (!data.order.customer) {
+                return `error.cannot-transition-to-payment-without-customer`;
+            }
         }
         }
     }
     }
 
 

+ 31 - 1
server/src/service/services/customer.service.ts

@@ -10,7 +10,7 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
-import { assertFound } from '../../common/utils';
+import { assertFound, normalizeEmailAddress } from '../../common/utils';
 import { Address } from '../../entity/address/address.entity';
 import { Address } from '../../entity/address/address.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
@@ -59,8 +59,19 @@ export class CustomerService {
     }
     }
 
 
     async create(input: CreateCustomerInput, password?: string): Promise<Customer> {
     async create(input: CreateCustomerInput, password?: string): Promise<Customer> {
+        input.emailAddress = normalizeEmailAddress(input.emailAddress);
         const customer = new Customer(input);
         const customer = new Customer(input);
 
 
+        const existing = await this.connection.getRepository(Customer).findOne({
+            where: {
+                emailAddress: input.emailAddress,
+            },
+        });
+
+        if (existing) {
+            throw new I18nError(`error.email-address-must-be-unique`);
+        }
+
         if (password) {
         if (password) {
             const user = new User();
             const user = new User();
             user.passwordHash = await this.passwordCipher.hash(password);
             user.passwordHash = await this.passwordCipher.hash(password);
@@ -80,6 +91,25 @@ export class CustomerService {
         return assertFound(this.findOne(customer.id));
         return assertFound(this.findOne(customer.id));
     }
     }
 
 
+    /**
+     * For guest checkouts, we assume that a matching email address is the same customer.
+     */
+    async createOrUpdate(input: CreateCustomerInput): Promise<Customer> {
+        input.emailAddress = normalizeEmailAddress(input.emailAddress);
+        let customer: Customer;
+        const existing = await this.connection.getRepository(Customer).findOne({
+            where: {
+                emailAddress: input.emailAddress,
+            },
+        });
+        if (existing) {
+            customer = patchEntity(existing, input);
+        } else {
+            customer = new Customer(input);
+        }
+        return this.connection.getRepository(Customer).save(customer);
+    }
+
     async createAddress(customerId: string, input: CreateAddressInput): Promise<Address> {
     async createAddress(customerId: string, input: CreateAddressInput): Promise<Address> {
         const customer = await this.connection.manager.findOne(Customer, customerId, {
         const customer = await this.connection.manager.findOne(Customer, customerId, {
             relations: ['addresses'],
             relations: ['addresses'],

+ 10 - 0
server/src/service/services/order.service.ts

@@ -7,6 +7,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { generatePublicId } from '../../common/generate-public-id';
 import { generatePublicId } from '../../common/generate-public-id';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { idsAreEqual } from '../../common/utils';
 import { idsAreEqual } from '../../common/utils';
+import { Customer } from '../../entity/customer/customer.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
@@ -255,6 +256,15 @@ export class OrderService {
         return order;
         return order;
     }
     }
 
 
+    async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        if (order.customer) {
+            throw new I18nError(`error.order-already-has-customer`);
+        }
+        order.customer = customer;
+        return this.connection.getRepository(Order).save(order);
+    }
+
     /**
     /**
      * When a guest user with an anonymous Order signs in and has an existing Order associated with that Customer,
      * When a guest user with an anonymous Order signs in and has an existing Order associated with that Customer,
      * we need to reconcile the contents of the two orders.
      * we need to reconcile the contents of the two orders.

+ 15 - 0
shared/generated-types.ts

@@ -598,6 +598,7 @@ export interface Mutation {
     setOrderShippingAddress?: Order | null;
     setOrderShippingAddress?: Order | null;
     setOrderShippingMethod?: Order | null;
     setOrderShippingMethod?: Order | null;
     addPaymentToOrder?: Order | null;
     addPaymentToOrder?: Order | null;
+    setCustomerForOrder?: Order | null;
     updatePaymentMethod: PaymentMethod;
     updatePaymentMethod: PaymentMethod;
     createProductOptionGroup: ProductOptionGroup;
     createProductOptionGroup: ProductOptionGroup;
     updateProductOptionGroup: ProductOptionGroup;
     updateProductOptionGroup: ProductOptionGroup;
@@ -1483,6 +1484,9 @@ export interface SetOrderShippingMethodMutationArgs {
 export interface AddPaymentToOrderMutationArgs {
 export interface AddPaymentToOrderMutationArgs {
     input: PaymentInput;
     input: PaymentInput;
 }
 }
+export interface SetCustomerForOrderMutationArgs {
+    input: CreateCustomerInput;
+}
 export interface UpdatePaymentMethodMutationArgs {
 export interface UpdatePaymentMethodMutationArgs {
     input: UpdatePaymentMethodInput;
     input: UpdatePaymentMethodInput;
 }
 }
@@ -3625,6 +3629,7 @@ export namespace MutationResolvers {
         setOrderShippingAddress?: SetOrderShippingAddressResolver<Order | null, any, Context>;
         setOrderShippingAddress?: SetOrderShippingAddressResolver<Order | null, any, Context>;
         setOrderShippingMethod?: SetOrderShippingMethodResolver<Order | null, any, Context>;
         setOrderShippingMethod?: SetOrderShippingMethodResolver<Order | null, any, Context>;
         addPaymentToOrder?: AddPaymentToOrderResolver<Order | null, any, Context>;
         addPaymentToOrder?: AddPaymentToOrderResolver<Order | null, any, Context>;
+        setCustomerForOrder?: SetCustomerForOrderResolver<Order | null, any, Context>;
         updatePaymentMethod?: UpdatePaymentMethodResolver<PaymentMethod, any, Context>;
         updatePaymentMethod?: UpdatePaymentMethodResolver<PaymentMethod, any, Context>;
         createProductOptionGroup?: CreateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         createProductOptionGroup?: CreateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         updateProductOptionGroup?: UpdateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         updateProductOptionGroup?: UpdateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
@@ -3950,6 +3955,16 @@ export namespace MutationResolvers {
         input: PaymentInput;
         input: PaymentInput;
     }
     }
 
 
+    export type SetCustomerForOrderResolver<R = Order | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        SetCustomerForOrderArgs
+    >;
+    export interface SetCustomerForOrderArgs {
+        input: CreateCustomerInput;
+    }
+
     export type UpdatePaymentMethodResolver<R = PaymentMethod, Parent = any, Context = any> = Resolver<
     export type UpdatePaymentMethodResolver<R = PaymentMethod, Parent = any, Context = any> = Resolver<
         R,
         R,
         Parent,
         Parent,

Некоторые файлы не были показаны из-за большого количества измененных файлов