Procházet zdrojové kódy

feat(core): Implement bulk versions of order operations

These new methods can bulk add, update, and delete order lines in a much
more efficient way than calling the single version multiple times.

This is because they only need to perform the expensive order fetch & price
adjustment a single time.
Michael Bromley před 1 rokem
rodič
revize
8d65219539
1 změnil soubory, kde provedl 248 přidání a 109 odebrání
  1. 248 109
      packages/core/src/service/services/order.service.ts

+ 248 - 109
packages/core/src/service/services/order.service.ts

@@ -47,7 +47,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
 import { CacheKey } from '../../common/constants';
-import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
+import { ErrorResultUnion, isGraphQlErrorResult, JustErrorResults } from '../../common/error/error-result';
 import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
 import {
     CancelPaymentError,
@@ -532,6 +532,8 @@ export class OrderService {
      * @description
      * Adds an item to the Order, either creating a new OrderLine or
      * incrementing an existing one.
+     *
+     * If you need to add multiple items to an Order, use `addItemsToOrder()` instead.
      */
     async addItemToOrder(
         ctx: RequestContext,
@@ -541,74 +543,135 @@ export class OrderService {
         customFields?: { [key: string]: any },
         relations?: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
-        const order = await this.getOrderOrThrow(ctx, orderId);
-        const existingOrderLine = await this.orderModifier.getExistingOrderLine(
-            ctx,
-            order,
-            productVariantId,
-            customFields,
-        );
-        const validationError =
-            this.assertQuantityIsPositive(quantity) ||
-            this.assertAddingItemsState(order) ||
-            this.assertNotOverOrderItemsLimit(order, quantity) ||
-            this.assertNotOverOrderLineItemsLimit(existingOrderLine, quantity);
-        if (validationError) {
-            return validationError;
-        }
-        const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId, {
-            relations: ['product'],
-            where: {
-                enabled: true,
-                deletedAt: IsNull(),
-            },
-            loadEagerRelations: false,
-        });
-        if (variant.product.enabled === false) {
-            throw new EntityNotFoundError('ProductVariant', productVariantId);
-        }
-        const existingQuantityInOtherLines = summate(
-            order.lines.filter(
-                l =>
-                    idsAreEqual(l.productVariantId, productVariantId) &&
-                    !idsAreEqual(l.id, existingOrderLine?.id),
-            ),
-            'quantity',
-        );
-        const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
-            ctx,
-            variant,
-            quantity,
-            existingOrderLine?.quantity,
-            existingQuantityInOtherLines,
-        );
-        if (correctedQuantity === 0) {
-            return new InsufficientStockError({ order, quantityAvailable: correctedQuantity });
-        }
-        const orderLine = await this.orderModifier.getOrCreateOrderLine(
+        const result = await this.addItemsToOrder(
             ctx,
-            order,
-            productVariantId,
-            customFields,
+            orderId,
+            [{ productVariantId, quantity, customFields }],
+            relations,
         );
-        if (correctedQuantity < quantity) {
-            const newQuantity = (existingOrderLine ? existingOrderLine?.quantity : 0) + correctedQuantity;
-            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, newQuantity, order);
+        if (result.errorResults.length) {
+            return result.errorResults[0];
         } else {
-            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+            return result.order;
         }
-        const quantityWasAdjustedDown = correctedQuantity < quantity;
-        const updatedOrder = await this.applyPriceAdjustments(ctx, order, [orderLine], relations);
-        if (quantityWasAdjustedDown) {
-            return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
-        } else {
-            return updatedOrder;
+    }
+
+    /**
+     * @description
+     * Adds multiple items to an Order. This method is more efficient than calling `addItemToOrder`
+     * multiple times, as it only needs to fetch the entire Order once, and only performs
+     * price adjustments once at the end.
+     *
+     * Since this method can return multiple error results, it is recommended to check the `errorResults`
+     * array to determine if any errors occurred.
+     *
+     * @since 3.1.0
+     */
+    async addItemsToOrder(
+        ctx: RequestContext,
+        orderId: ID,
+        items: Array<{
+            productVariantId: ID;
+            quantity: number;
+            customFields?: { [key: string]: any };
+        }>,
+        relations?: RelationPaths<Order>,
+    ): Promise<{ order: Order; errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> }> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        const errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> = [];
+        const updatedOrderLines: OrderLine[] = [];
+        for (const item of items) {
+            const { productVariantId, quantity, customFields } = item;
+            const existingOrderLine = await this.orderModifier.getExistingOrderLine(
+                ctx,
+                order,
+                productVariantId,
+                customFields,
+            );
+            const validationError =
+                this.assertQuantityIsPositive(quantity) ||
+                this.assertAddingItemsState(order) ||
+                this.assertNotOverOrderItemsLimit(order, quantity) ||
+                this.assertNotOverOrderLineItemsLimit(existingOrderLine, quantity);
+            if (validationError) {
+                errorResults.push(validationError);
+                continue;
+            }
+            const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId, {
+                relations: ['product'],
+                where: {
+                    enabled: true,
+                    deletedAt: IsNull(),
+                },
+                loadEagerRelations: false,
+            });
+            if (variant.product.enabled === false) {
+                throw new EntityNotFoundError('ProductVariant', productVariantId);
+            }
+            const existingQuantityInOtherLines = summate(
+                order.lines.filter(
+                    l =>
+                        idsAreEqual(l.productVariantId, productVariantId) &&
+                        !idsAreEqual(l.id, existingOrderLine?.id),
+                ),
+                'quantity',
+            );
+            const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
+                ctx,
+                variant,
+                quantity,
+                existingOrderLine?.quantity,
+                existingQuantityInOtherLines,
+            );
+            if (correctedQuantity === 0) {
+                errorResults.push(
+                    new InsufficientStockError({ order, quantityAvailable: correctedQuantity }),
+                );
+                continue;
+            }
+            const orderLine = await this.orderModifier.getOrCreateOrderLine(
+                ctx,
+                order,
+                productVariantId,
+                customFields,
+            );
+            if (correctedQuantity < quantity) {
+                const newQuantity = (existingOrderLine ? existingOrderLine?.quantity : 0) + correctedQuantity;
+                await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, newQuantity, order);
+            } else {
+                await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+            }
+            updatedOrderLines.push(orderLine);
+            const quantityWasAdjustedDown = correctedQuantity < quantity;
+            if (quantityWasAdjustedDown) {
+                errorResults.push(
+                    new InsufficientStockError({ quantityAvailable: correctedQuantity, order }),
+                );
+                continue;
+            }
         }
+        const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines, relations);
+        // for any InsufficientStockError errors, we want to make sure we use the final updatedOrder
+        // after having applied all price adjustments
+        for (const [i, errorResult] of Object.entries(errorResults)) {
+            if (errorResult.__typename === 'InsufficientStockError') {
+                errorResults[+i] = new InsufficientStockError({
+                    quantityAvailable: errorResult.quantityAvailable,
+                    order: updatedOrder,
+                });
+            }
+        }
+        return {
+            order: updatedOrder,
+            errorResults,
+        };
     }
 
     /**
      * @description
      * Adjusts the quantity and/or custom field values of an existing OrderLine.
+     *
+     * If you need to adjust multiple OrderLines, use `adjustOrderLines()` instead.
      */
     async adjustOrderLine(
         ctx: RequestContext,
@@ -618,84 +681,160 @@ export class OrderService {
         customFields?: { [key: string]: any },
         relations?: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
-        const order = await this.getOrderOrThrow(ctx, orderId);
-        const orderLine = this.getOrderLineOrThrow(order, orderLineId);
-        const validationError =
-            this.assertAddingItemsState(order) ||
-            this.assertQuantityIsPositive(quantity) ||
-            this.assertNotOverOrderItemsLimit(order, quantity - orderLine.quantity) ||
-            this.assertNotOverOrderLineItemsLimit(orderLine, quantity - orderLine.quantity);
-        if (validationError) {
-            return validationError;
-        }
-        if (customFields != null) {
-            orderLine.customFields = customFields;
-            await this.customFieldRelationService.updateRelations(
-                ctx,
-                OrderLine,
-                { customFields },
-                orderLine,
-            );
-        }
-        const existingQuantityInOtherLines = summate(
-            order.lines.filter(
-                l =>
-                    idsAreEqual(l.productVariantId, orderLine.productVariantId) &&
-                    !idsAreEqual(l.id, orderLineId),
-            ),
-            'quantity',
-        );
-        const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
+        const result = await this.adjustOrderLines(
             ctx,
-            orderLine.productVariant,
-            quantity,
-            0,
-            existingQuantityInOtherLines,
+            orderId,
+            [{ orderLineId, quantity, customFields }],
+            relations,
         );
-        let updatedOrderLines = [orderLine];
-        if (correctedQuantity === 0) {
-            order.lines = order.lines.filter(l => !idsAreEqual(l.id, orderLine.id));
-            const deletedOrderLine = new OrderLine(orderLine);
-            await this.connection.getRepository(ctx, OrderLine).remove(orderLine);
-            await this.eventBus.publish(new OrderLineEvent(ctx, order, deletedOrderLine, 'deleted'));
-            updatedOrderLines = [];
+        if (result.errorResults.length) {
+            return result.errorResults[0];
         } else {
-            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+            return result.order;
+        }
+    }
+
+    /**
+     * @description
+     * Adjusts the quantity and/or custom field values of existing OrderLines.
+     * This method is more efficient than calling `adjustOrderLine` multiple times, as it only needs to fetch
+     * the entire Order once, and only performs price adjustments once at the end.
+     * Since this method can return multiple error results, it is recommended to check the `errorResults`
+     * array to determine if any errors occurred.
+     *
+     * @since 3.1.0
+     */
+    async adjustOrderLines(
+        ctx: RequestContext,
+        orderId: ID,
+        lines: Array<{ orderLineId: ID; quantity: number; customFields?: { [key: string]: any } }>,
+        relations?: RelationPaths<Order>,
+    ): Promise<{ order: Order; errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> }> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        const errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> = [];
+        const updatedOrderLines: OrderLine[] = [];
+        for (const line of lines) {
+            const { orderLineId, quantity, customFields } = line;
+            const orderLine = this.getOrderLineOrThrow(order, orderLineId);
+            const validationError =
+                this.assertAddingItemsState(order) ||
+                this.assertQuantityIsPositive(quantity) ||
+                this.assertNotOverOrderItemsLimit(order, quantity - orderLine.quantity) ||
+                this.assertNotOverOrderLineItemsLimit(orderLine, quantity - orderLine.quantity);
+            if (validationError) {
+                errorResults.push(validationError);
+                continue;
+            }
+            if (customFields != null) {
+                orderLine.customFields = customFields;
+                await this.customFieldRelationService.updateRelations(
+                    ctx,
+                    OrderLine,
+                    { customFields },
+                    orderLine,
+                );
+            }
+            const existingQuantityInOtherLines = summate(
+                order.lines.filter(
+                    l =>
+                        idsAreEqual(l.productVariantId, orderLine.productVariantId) &&
+                        !idsAreEqual(l.id, orderLineId),
+                ),
+                'quantity',
+            );
+            const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
+                ctx,
+                orderLine.productVariant,
+                quantity,
+                0,
+                existingQuantityInOtherLines,
+            );
+            if (correctedQuantity === 0) {
+                order.lines = order.lines.filter(l => !idsAreEqual(l.id, orderLine.id));
+                const deletedOrderLine = new OrderLine(orderLine);
+                await this.connection.getRepository(ctx, OrderLine).remove(orderLine);
+                await this.eventBus.publish(new OrderLineEvent(ctx, order, deletedOrderLine, 'deleted'));
+            } else {
+                await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+                updatedOrderLines.push(orderLine);
+            }
+            const quantityWasAdjustedDown = correctedQuantity < quantity;
+
+            if (quantityWasAdjustedDown) {
+                errorResults.push(
+                    new InsufficientStockError({
+                        quantityAvailable: correctedQuantity,
+                        order,
+                    }),
+                );
+            }
         }
-        const quantityWasAdjustedDown = correctedQuantity < quantity;
         const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines, relations);
-        if (quantityWasAdjustedDown) {
-            return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
-        } else {
-            return updatedOrder;
+        for (const [i, errorResult] of Object.entries(errorResults)) {
+            if (errorResult.__typename === 'InsufficientStockError') {
+                errorResults[+i] = new InsufficientStockError({
+                    quantityAvailable: errorResult.quantityAvailable,
+                    order: updatedOrder,
+                });
+            }
         }
+        return {
+            order: updatedOrder,
+            errorResults,
+        };
     }
 
     /**
      * @description
      * Removes the specified OrderLine from the Order.
+     *
+     * If you need to remove multiple OrderLines, use `removeItemsFromOrder()` instead.
      */
     async removeItemFromOrder(
         ctx: RequestContext,
         orderId: ID,
         orderLineId: ID,
+    ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
+        return this.removeItemsFromOrder(ctx, orderId, [orderLineId]);
+    }
+
+    /**
+     * @description
+     * Removes the specified OrderLines from the Order.
+     * This method is more efficient than calling `removeItemFromOrder` multiple times, as it only needs to fetch
+     * the entire Order once, and only performs price adjustments once at the end.
+     *
+     * @since 3.1.0
+     */
+    async removeItemsFromOrder(
+        ctx: RequestContext,
+        orderId: ID,
+        orderLineIds: ID[],
     ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const validationError = this.assertAddingItemsState(order);
         if (validationError) {
             return validationError;
         }
-        const orderLine = this.getOrderLineOrThrow(order, orderLineId);
-        order.lines = order.lines.filter(line => !idsAreEqual(line.id, orderLineId));
+        const orderLinesToDelete: OrderLine[] = [];
+        for (const orderLineId of orderLineIds) {
+            // Validation check to ensure that the OrderLine exists on the Order
+            const orderLine = this.getOrderLineOrThrow(order, orderLineId);
+            orderLinesToDelete.push(orderLine);
+        }
+
+        order.lines = order.lines.filter(line => !orderLineIds.find(olId => idsAreEqual(line.id, olId)));
         // Persist the orderLine removal before applying price adjustments
         // so that any hydration of the Order entity during the course of the
         // `applyPriceAdjustments()` (e.g. in a ShippingEligibilityChecker etc)
         // will not re-add the OrderLine.
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         const updatedOrder = await this.applyPriceAdjustments(ctx, order);
-        const deletedOrderLine = new OrderLine(orderLine);
-        await this.connection.getRepository(ctx, OrderLine).remove(orderLine);
-        await this.eventBus.publish(new OrderLineEvent(ctx, order, deletedOrderLine, 'deleted'));
+        for (const orderLine of orderLinesToDelete) {
+            const deletedOrderLine = new OrderLine(orderLine);
+            await this.connection.getRepository(ctx, OrderLine).remove(orderLine);
+            await this.eventBus.publish(new OrderLineEvent(ctx, order, deletedOrderLine, 'deleted'));
+        }
         return updatedOrder;
     }