UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

791 lines 44.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OrderModifier = void 0; const common_1 = require("@nestjs/common"); const generated_types_1 = require("@vendure/common/lib/generated-types"); const shared_utils_1 = require("@vendure/common/lib/shared-utils"); const typeorm_1 = require("typeorm"); const error_result_1 = require("../../../common/error/error-result"); const errors_1 = require("../../../common/error/errors"); const generated_graphql_admin_errors_1 = require("../../../common/error/generated-graphql-admin-errors"); const generated_graphql_shop_errors_1 = require("../../../common/error/generated-graphql-shop-errors"); const instrument_decorator_1 = require("../../../common/instrument-decorator"); const utils_1 = require("../../../common/utils"); const config_service_1 = require("../../../config/config.service"); const transactional_connection_1 = require("../../../connection/transactional-connection"); const fulfillment_line_entity_1 = require("../../../entity/order-line-reference/fulfillment-line.entity"); const order_modification_line_entity_1 = require("../../../entity/order-line-reference/order-modification-line.entity"); const order_line_entity_1 = require("../../../entity/order-line/order-line.entity"); const order_modification_entity_1 = require("../../../entity/order-modification/order-modification.entity"); const order_entity_1 = require("../../../entity/order/order.entity"); const payment_entity_1 = require("../../../entity/payment/payment.entity"); const product_variant_entity_1 = require("../../../entity/product-variant/product-variant.entity"); const shipping_line_entity_1 = require("../../../entity/shipping-line/shipping-line.entity"); const allocation_entity_1 = require("../../../entity/stock-movement/allocation.entity"); const cancellation_entity_1 = require("../../../entity/stock-movement/cancellation.entity"); const release_entity_1 = require("../../../entity/stock-movement/release.entity"); const sale_entity_1 = require("../../../entity/stock-movement/sale.entity"); const surcharge_entity_1 = require("../../../entity/surcharge/surcharge.entity"); const event_bus_1 = require("../../../event-bus"); const event_bus_2 = require("../../../event-bus/event-bus"); const order_line_event_1 = require("../../../event-bus/events/order-line-event"); const country_service_1 = require("../../services/country.service"); const history_service_1 = require("../../services/history.service"); const payment_service_1 = require("../../services/payment.service"); const product_variant_service_1 = require("../../services/product-variant.service"); const promotion_service_1 = require("../../services/promotion.service"); const stock_movement_service_1 = require("../../services/stock-movement.service"); const custom_field_relation_service_1 = require("../custom-field-relation/custom-field-relation.service"); const order_calculator_1 = require("../order-calculator/order-calculator"); const shipping_calculator_1 = require("../shipping-calculator/shipping-calculator"); const translator_service_1 = require("../translator/translator.service"); const coupon_codes_match_1 = require("../utils/coupon-codes-match"); const order_utils_1 = require("../utils/order-utils"); const patch_entity_1 = require("../utils/patch-entity"); /** * @description * This helper is responsible for modifying the contents of an Order. * * Note: * There is not a clear separation of concerns between the OrderService and this, since * the OrderService also contains some method which modify the Order (e.g. removeItemFromOrder). * So this helper was mainly extracted to isolate the huge `modifyOrder` method since the * OrderService was just growing too large. Future refactoring could improve the organization * of these Order-related methods into a more clearly-delineated set of classes. * * @docsCategory service-helpers */ let OrderModifier = class OrderModifier { constructor(connection, configService, orderCalculator, paymentService, countryService, stockMovementService, productVariantService, customFieldRelationService, promotionService, eventBus, shippingCalculator, historyService, translator) { this.connection = connection; this.configService = configService; this.orderCalculator = orderCalculator; this.paymentService = paymentService; this.countryService = countryService; this.stockMovementService = stockMovementService; this.productVariantService = productVariantService; this.customFieldRelationService = customFieldRelationService; this.promotionService = promotionService; this.eventBus = eventBus; this.shippingCalculator = shippingCalculator; this.historyService = historyService; this.translator = translator; } /** * @description * Ensure that the ProductVariant has sufficient saleable stock to add the given * quantity to an Order. * * - `existingOrderLineQuantity` is used when adding an item to the order, since if an OrderLine * already exists then we will be adding the new quantity to the existing quantity. * - `quantityInOtherOrderLines` is used when we have more than 1 OrderLine containing the same * ProductVariant. This occurs when there are custom fields defined on the OrderLine and the lines * have differing values for one or more custom fields. In this case, we need to take _all_ of these * OrderLines into account when constraining the quantity. See https://github.com/vendurehq/vendure/issues/2702 * for more on this. */ async constrainQuantityToSaleable(ctx, variant, quantity, existingOrderLineQuantity = 0, quantityInOtherOrderLines = 0) { let correctedQuantity = quantity + existingOrderLineQuantity; const saleableStockLevel = await this.productVariantService.getSaleableStockLevel(ctx, variant); if (saleableStockLevel < correctedQuantity + quantityInOtherOrderLines) { correctedQuantity = Math.max(saleableStockLevel - existingOrderLineQuantity - quantityInOtherOrderLines, 0); } return correctedQuantity; } /** * @description * Given a ProductVariant ID and optional custom fields, this method will return an existing OrderLine that * matches, or `undefined` if no match is found. */ async getExistingOrderLine(ctx, order, productVariantId, customFields) { for (const line of order.lines) { const match = (0, utils_1.idsAreEqual)(line.productVariantId, productVariantId) && (await this.customFieldsAreEqual(ctx, line, customFields, line.customFields)); if (match) { return line; } } } /** * @description * Returns the OrderLine containing the given {@link ProductVariant}, taking into account any custom field values. If no existing * OrderLine is found, a new OrderLine will be created. */ async getOrCreateOrderLine(ctx, order, productVariantId, customFields) { var _a; const existingOrderLine = await this.getExistingOrderLine(ctx, order, productVariantId, customFields); if (existingOrderLine) { return existingOrderLine; } const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId, order); const featuredAssetId = (_a = productVariant.featuredAssetId) !== null && _a !== void 0 ? _a : productVariant.product.featuredAssetId; const orderLine = await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).save(new order_line_entity_1.OrderLine({ productVariant, taxCategory: productVariant.taxCategory, featuredAsset: featuredAssetId ? { id: featuredAssetId } : undefined, listPrice: productVariant.listPrice, listPriceIncludesTax: productVariant.listPriceIncludesTax, adjustments: [], taxLines: [], customFields, quantity: 0, })); const { orderSellerStrategy } = this.configService.orderOptions; if (typeof orderSellerStrategy.setOrderLineSellerChannel === 'function') { orderLine.sellerChannel = await orderSellerStrategy.setOrderLineSellerChannel(ctx, orderLine); await this.connection .getRepository(ctx, order_line_entity_1.OrderLine) .createQueryBuilder() .relation('sellerChannel') .of(orderLine) .set(orderLine.sellerChannel); } await this.customFieldRelationService.updateRelations(ctx, order_line_entity_1.OrderLine, { customFields }, orderLine); order.lines.push(orderLine); await this.connection .getRepository(ctx, order_entity_1.Order) .createQueryBuilder() .relation('lines') .of(order) .add(orderLine); await this.eventBus.publish(new order_line_event_1.OrderLineEvent(ctx, order, orderLine, 'created')); return orderLine; } /** * @description * Updates the quantity of an OrderLine, taking into account the available saleable stock level. * Returns the actual quantity that the OrderLine was updated to (which may be less than the * `quantity` argument if insufficient stock was available. */ async updateOrderLineQuantity(ctx, orderLine, quantity, order) { const currentQuantity = orderLine.quantity; orderLine.quantity = quantity; if (currentQuantity < quantity) { if (!order.active && order.state !== 'Draft') { await this.stockMovementService.createAllocationsForOrderLines(ctx, [ { orderLineId: orderLine.id, quantity: quantity - currentQuantity, }, ]); } } else if (quantity < currentQuantity) { if (!order.active && order.state !== 'Draft') { // When an Order is not active (i.e. Customer checked out), then we don't want to just // delete the OrderItems - instead we will cancel them // const toSetAsCancelled = orderLine.items.filter(i => !i.cancelled).slice(quantity); // const fulfilledItems = toSetAsCancelled.filter(i => !!i.fulfillment); // const allocatedItems = toSetAsCancelled.filter(i => !i.fulfillment); await this.stockMovementService.createCancellationsForOrderLines(ctx, [ { orderLineId: orderLine.id, quantity }, ]); await this.stockMovementService.createReleasesForOrderLines(ctx, [ { orderLineId: orderLine.id, quantity }, ]); } } await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).save(orderLine); await this.eventBus.publish(new order_line_event_1.OrderLineEvent(ctx, order, orderLine, 'updated')); return orderLine; } async cancelOrderByOrderLines(ctx, input, lineInputs) { if (lineInputs.length === 0 || (0, shared_utils_1.summate)(lineInputs, 'quantity') === 0) { return new generated_graphql_admin_errors_1.EmptyOrderLineSelectionError(); } const orders = await (0, order_utils_1.getOrdersFromLines)(ctx, this.connection, lineInputs); if (1 < orders.length) { return new generated_graphql_admin_errors_1.MultipleOrderError(); } const order = orders[0]; if (!(0, utils_1.idsAreEqual)(order.id, input.orderId)) { return new generated_graphql_admin_errors_1.MultipleOrderError(); } if (order.active) { return new generated_graphql_admin_errors_1.CancelActiveOrderError({ orderState: order.state }); } const fullOrder = await this.connection.getEntityOrThrow(ctx, order_entity_1.Order, order.id, { relations: ['lines'], }); const allocatedLines = []; const fulfilledLines = []; for (const lineInput of lineInputs) { const orderLine = fullOrder.lines.find(l => (0, utils_1.idsAreEqual)(l.id, lineInput.orderLineId)); if (orderLine && orderLine.quantity < lineInput.quantity) { return new generated_graphql_admin_errors_1.QuantityTooGreatError(); } const allocationsForLine = await this.connection .getRepository(ctx, allocation_entity_1.Allocation) .createQueryBuilder('allocation') .leftJoinAndSelect('allocation.orderLine', 'orderLine') .where('orderLine.id = :orderLineId', { orderLineId: lineInput.orderLineId }) .getMany(); const salesForLine = await this.connection .getRepository(ctx, sale_entity_1.Sale) .createQueryBuilder('sale') .leftJoinAndSelect('sale.orderLine', 'orderLine') .where('orderLine.id = :orderLineId', { orderLineId: lineInput.orderLineId }) .getMany(); const releasesForLine = await this.connection .getRepository(ctx, release_entity_1.Release) .createQueryBuilder('release') .leftJoinAndSelect('release.orderLine', 'orderLine') .where('orderLine.id = :orderLineId', { orderLineId: lineInput.orderLineId }) .getMany(); const totalAllocated = (0, shared_utils_1.summate)(allocationsForLine, 'quantity') + (0, shared_utils_1.summate)(salesForLine, 'quantity') - (0, shared_utils_1.summate)(releasesForLine, 'quantity'); if (0 < totalAllocated) { allocatedLines.push({ orderLineId: lineInput.orderLineId, quantity: Math.min(totalAllocated, lineInput.quantity), }); } const fulfillmentsForLine = await this.connection .getRepository(ctx, fulfillment_line_entity_1.FulfillmentLine) .createQueryBuilder('fulfillmentLine') .leftJoinAndSelect('fulfillmentLine.orderLine', 'orderLine') .where('orderLine.id = :orderLineId', { orderLineId: lineInput.orderLineId }) .getMany(); const cancellationsForLine = await this.connection .getRepository(ctx, cancellation_entity_1.Cancellation) .createQueryBuilder('cancellation') .leftJoinAndSelect('cancellation.orderLine', 'orderLine') .where('orderLine.id = :orderLineId', { orderLineId: lineInput.orderLineId }) .getMany(); const totalFulfilled = (0, shared_utils_1.summate)(fulfillmentsForLine, 'quantity') - (0, shared_utils_1.summate)(cancellationsForLine, 'quantity'); if (0 < totalFulfilled) { fulfilledLines.push({ orderLineId: lineInput.orderLineId, quantity: Math.min(totalFulfilled, lineInput.quantity), }); } } await this.stockMovementService.createCancellationsForOrderLines(ctx, fulfilledLines); await this.stockMovementService.createReleasesForOrderLines(ctx, allocatedLines); for (const line of lineInputs) { const orderLine = fullOrder.lines.find(l => (0, utils_1.idsAreEqual)(l.id, line.orderLineId)); if (orderLine) { await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).update(line.orderLineId, { quantity: orderLine.quantity - line.quantity, }); await this.eventBus.publish(new order_line_event_1.OrderLineEvent(ctx, order, orderLine, 'cancelled')); } } const orderWithLines = await this.connection.getEntityOrThrow(ctx, order_entity_1.Order, order.id, { relations: ['lines', 'surcharges', 'shippingLines'], }); if (input.cancelShipping === true) { for (const shippingLine of orderWithLines.shippingLines) { shippingLine.adjustments.push({ adjustmentSource: 'CANCEL_ORDER', type: generated_types_1.AdjustmentType.OTHER, description: 'shipping cancellation', amount: -shippingLine.discountedPrice, data: {}, }); await this.connection.getRepository(ctx, shipping_line_entity_1.ShippingLine).save(shippingLine, { reload: false }); } } // Update totals after cancellation this.orderCalculator.calculateOrderTotals(orderWithLines); await this.connection.getRepository(ctx, order_entity_1.Order).save(orderWithLines, { reload: false }); await this.historyService.createHistoryEntryForOrder({ ctx, orderId: order.id, type: generated_types_1.HistoryEntryType.ORDER_CANCELLATION, data: { lines: lineInputs, reason: input.reason || undefined, shippingCancelled: !!input.cancelShipping, }, }); return (0, order_utils_1.orderLinesAreAllCancelled)(orderWithLines); } async modifyOrder(ctx, input, order) { var _a, _b, _c, _d, _e; const { dryRun } = input; const modification = new order_modification_entity_1.OrderModification({ order, note: input.note || '', lines: [], surcharges: [], }); const initialTotalWithTax = order.totalWithTax; const initialShippingWithTax = order.shippingWithTax; if (order.state !== 'Modifying') { return new generated_graphql_admin_errors_1.OrderModificationStateError(); } if (this.noChangesSpecified(input)) { return new generated_graphql_admin_errors_1.NoChangesSpecifiedError(); } const { orderItemsLimit } = this.configService.orderOptions; let currentItemsCount = (0, shared_utils_1.summate)(order.lines, 'quantity'); const updatedOrderLineIds = []; const refundInputArray = Array.isArray(input.refunds) ? input.refunds : input.refund ? [input.refund] : []; const refundInputs = refundInputArray.map(refund => ({ lines: [], adjustment: 0, shipping: 0, paymentId: refund.paymentId, amount: refund.amount, reason: refund.reason || input.note, })); for (const row of (_a = input.addItems) !== null && _a !== void 0 ? _a : []) { const { productVariantId, quantity } = row; if (quantity < 0) { return new generated_graphql_shop_errors_1.NegativeQuantityError(); } const customFields = row.customFields || {}; const orderLine = await this.getOrCreateOrderLine(ctx, order, productVariantId, customFields); const correctedQuantity = await this.constrainQuantityToSaleable(ctx, orderLine.productVariant, quantity); if (orderItemsLimit < currentItemsCount + correctedQuantity) { return new generated_graphql_shop_errors_1.OrderLimitError({ maxItems: orderItemsLimit }); } else { currentItemsCount += correctedQuantity; } if (correctedQuantity < quantity) { return new generated_graphql_shop_errors_1.InsufficientStockError({ quantityAvailable: correctedQuantity, order }); } updatedOrderLineIds.push(orderLine.id); const initialQuantity = orderLine.quantity; await this.updateOrderLineQuantity(ctx, orderLine, initialQuantity + correctedQuantity, order); const orderModificationLine = await this.connection .getRepository(ctx, order_modification_line_entity_1.OrderModificationLine) .save(new order_modification_line_entity_1.OrderModificationLine({ orderLine, quantity: quantity - initialQuantity })); modification.lines.push(orderModificationLine); } for (const row of (_b = input.adjustOrderLines) !== null && _b !== void 0 ? _b : []) { const { orderLineId, quantity } = row; if (quantity < 0) { return new generated_graphql_shop_errors_1.NegativeQuantityError(); } const orderLine = order.lines.find(line => (0, utils_1.idsAreEqual)(line.id, orderLineId)); if (!orderLine) { throw new errors_1.UserInputError('error.order-does-not-contain-line-with-id', { id: orderLineId }); } const initialLineQuantity = orderLine.quantity; let correctedQuantity = quantity; if (initialLineQuantity < quantity) { const additionalQuantity = await this.constrainQuantityToSaleable(ctx, orderLine.productVariant, quantity - initialLineQuantity); correctedQuantity = initialLineQuantity + additionalQuantity; } const resultingOrderTotalQuantity = currentItemsCount + correctedQuantity - orderLine.quantity; if (orderItemsLimit < resultingOrderTotalQuantity) { return new generated_graphql_shop_errors_1.OrderLimitError({ maxItems: orderItemsLimit }); } else { currentItemsCount += correctedQuantity; } if (correctedQuantity < quantity) { return new generated_graphql_shop_errors_1.InsufficientStockError({ quantityAvailable: correctedQuantity, order }); } else { const customFields = row.customFields; if (customFields) { (0, patch_entity_1.patchEntity)(orderLine, { customFields }); } if (quantity < initialLineQuantity) { const cancelLinesInput = [ { orderLineId, quantity: initialLineQuantity - quantity, }, ]; await this.cancelOrderByOrderLines(ctx, { orderId: order.id }, cancelLinesInput); orderLine.quantity = quantity; } else { await this.updateOrderLineQuantity(ctx, orderLine, quantity, order); } const orderModificationLine = await this.connection .getRepository(ctx, order_modification_line_entity_1.OrderModificationLine) .save(new order_modification_line_entity_1.OrderModificationLine({ orderLine, quantity: quantity - initialLineQuantity })); modification.lines.push(orderModificationLine); if (correctedQuantity < initialLineQuantity) { const qtyDelta = initialLineQuantity - correctedQuantity; refundInputs.forEach(ri => { var _a; (_a = ri.lines) === null || _a === void 0 ? void 0 : _a.push({ orderLineId: orderLine.id, quantity: qtyDelta, }); }); } } updatedOrderLineIds.push(orderLine.id); } for (const surchargeInput of (_c = input.surcharges) !== null && _c !== void 0 ? _c : []) { const taxLines = surchargeInput.taxRate != null ? [ { taxRate: surchargeInput.taxRate, description: surchargeInput.taxDescription || '', }, ] : []; const surcharge = await this.connection.getRepository(ctx, surcharge_entity_1.Surcharge).save(new surcharge_entity_1.Surcharge({ sku: surchargeInput.sku || '', description: surchargeInput.description, listPrice: surchargeInput.price, listPriceIncludesTax: surchargeInput.priceIncludesTax, taxLines, order, })); order.surcharges.push(surcharge); modification.surcharges.push(surcharge); if (surcharge.priceWithTax < 0) { refundInputs.forEach(ri => { if (ri.adjustment != null) { ri.adjustment += Math.abs(surcharge.priceWithTax); } }); } } if ((_d = input.surcharges) === null || _d === void 0 ? void 0 : _d.length) { await this.connection.getRepository(ctx, order_entity_1.Order).save(order, { reload: false }); } if (input.updateShippingAddress) { order.shippingAddress = Object.assign(Object.assign({}, order.shippingAddress), input.updateShippingAddress); if (input.updateShippingAddress.countryCode) { const country = await this.countryService.findOneByCode(ctx, input.updateShippingAddress.countryCode); order.shippingAddress.country = country.name; } await this.connection.getRepository(ctx, order_entity_1.Order).save(order, { reload: false }); modification.shippingAddressChange = input.updateShippingAddress; } if (input.updateBillingAddress) { order.billingAddress = Object.assign(Object.assign({}, order.billingAddress), input.updateBillingAddress); if (input.updateBillingAddress.countryCode) { const country = await this.countryService.findOneByCode(ctx, input.updateBillingAddress.countryCode); order.billingAddress.country = country.name; } await this.connection.getRepository(ctx, order_entity_1.Order).save(order, { reload: false }); modification.billingAddressChange = input.updateBillingAddress; } if (input.couponCodes) { const canonicalCouponCodes = []; for (const couponCode of input.couponCodes) { const validationResult = await this.promotionService.validateCouponCode(ctx, couponCode, order.customer && order.customer.id); if ((0, error_result_1.isGraphQlErrorResult)(validationResult)) { return validationResult; } const canonicalCode = validationResult.couponCode; if (!canonicalCouponCodes.some(cc => (0, coupon_codes_match_1.couponCodesMatch)(cc, canonicalCode))) { canonicalCouponCodes.push(canonicalCode); } if (!order.couponCodes.some(cc => (0, coupon_codes_match_1.couponCodesMatch)(cc, canonicalCode))) { // This is a new coupon code that hadn't been applied before await this.historyService.createHistoryEntryForOrder({ ctx, orderId: order.id, type: generated_types_1.HistoryEntryType.ORDER_COUPON_APPLIED, data: { couponCode: canonicalCode, promotionId: validationResult.id }, }); } } for (const existingCouponCode of order.couponCodes) { if (!canonicalCouponCodes.some(cc => (0, coupon_codes_match_1.couponCodesMatch)(cc, existingCouponCode))) { // An existing coupon code has been removed await this.historyService.createHistoryEntryForOrder({ ctx, orderId: order.id, type: generated_types_1.HistoryEntryType.ORDER_COUPON_REMOVED, data: { couponCode: existingCouponCode }, }); } } order.couponCodes = canonicalCouponCodes; } 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 ((0, error_result_1.isGraphQlErrorResult)(result)) { return result; } } const { orderItemPriceCalculationStrategy } = this.configService.orderOptions; for (const orderLine of updatedOrderLines) { const variant = await this.productVariantService.applyChannelPriceAndTax(orderLine.productVariant, ctx, order); const priceResult = await orderItemPriceCalculationStrategy.calculateUnitPrice(ctx, variant, orderLine.customFields || {}, order, orderLine.quantity); orderLine.listPrice = priceResult.price; orderLine.listPriceIncludesTax = priceResult.priceIncludesTax; } await this.orderCalculator.applyPriceAdjustments(ctx, order, promotions, updatedOrderLines, { recalculateShipping: (_e = input.options) === null || _e === void 0 ? void 0 : _e.recalculateShipping, }); await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre); await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).save(order.lines, { reload: false }); const orderCustomFields = input.customFields; if (orderCustomFields) { (0, patch_entity_1.patchEntity)(order, { customFields: orderCustomFields }); } if (dryRun) { return { order, modification }; } // Create the actual modification and commit all changes const newTotalWithTax = order.totalWithTax; const delta = newTotalWithTax - initialTotalWithTax; if (delta < 0) { if (refundInputs.length === 0) { return new generated_graphql_admin_errors_1.RefundPaymentIdMissingError(); } // If there are multiple refunds, we select the largest one as the // "primary" refund to associate with the OrderModification. const primaryRefund = refundInputs.slice().sort((a, b) => (b.amount || 0) - (a.amount || 0))[0]; // TODO: the following code can be removed once we remove the deprecated // support for "shipping" and "adjustment" input fields for refunds const shippingDelta = order.shippingWithTax - initialShippingWithTax; if (shippingDelta < 0) { primaryRefund.shipping = shippingDelta * -1; } if (primaryRefund.adjustment != null) { primaryRefund.adjustment += await this.calculateRefundAdjustment(ctx, delta, primaryRefund); } // end for (const refundInput of refundInputs) { const existingPayments = await this.getOrderPayments(ctx, order.id); const payment = existingPayments.find(p => (0, utils_1.idsAreEqual)(p.id, refundInput.paymentId)); if (payment) { const refund = await this.paymentService.createRefund(ctx, refundInput, order, payment); if (!(0, error_result_1.isGraphQlErrorResult)(refund)) { if ((0, utils_1.idsAreEqual)(payment.id, primaryRefund.paymentId)) { modification.refund = refund; } } else { throw new errors_1.InternalServerError(refund.message); } } } } modification.priceChange = delta; const createdModification = await this.connection .getRepository(ctx, order_modification_entity_1.OrderModification) .save(modification); await this.connection.getRepository(ctx, order_entity_1.Order).save(order); await this.connection.getRepository(ctx, shipping_line_entity_1.ShippingLine).save(order.shippingLines, { reload: false }); await this.eventBus.publish(new event_bus_1.OrderEvent(ctx, order, 'updated', input)); return { order, modification: createdModification }; } async setShippingMethods(ctx, order, shippingMethodIds) { for (const [i, shippingMethodId] of shippingMethodIds.entries()) { const shippingMethod = await this.shippingCalculator.getMethodIfEligible(ctx, order, shippingMethodId); if (!shippingMethod) { return new generated_graphql_shop_errors_1.IneligibleShippingMethodError(); } let shippingLine = order.shippingLines[i]; if (shippingLine) { shippingLine.shippingMethod = shippingMethod; shippingLine.shippingMethodId = shippingMethod.id; } else { shippingLine = await this.connection.getRepository(ctx, shipping_line_entity_1.ShippingLine).save(new shipping_line_entity_1.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, shipping_line_entity_1.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, shipping_line_entity_1.ShippingLine).remove(shippingLinesToDelete); } // assign the ShippingLines to the OrderLines await this.connection .getRepository(ctx, order_line_entity_1.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, order_line_entity_1.OrderLine) .createQueryBuilder('line') .update({ shippingLineId: shippingLine.id }) .whereInIds(orderLinesForShippingLine.map(l => l.id)) .execute(); orderLinesForShippingLine.forEach(line => { line.shippingLine = shippingLine; }); } return order; } noChangesSpecified(input) { var _a, _b, _c; const noChanges = !((_a = input.adjustOrderLines) === null || _a === void 0 ? void 0 : _a.length) && !((_b = input.addItems) === null || _b === void 0 ? void 0 : _b.length) && !((_c = input.surcharges) === null || _c === void 0 ? void 0 : _c.length) && !input.updateShippingAddress && !input.updateBillingAddress && !input.couponCodes && !input.customFields && (!input.shippingMethodIds || input.shippingMethodIds.length === 0); return noChanges; } /** * @description * Because a Refund's amount is calculated based on the orderItems changed, plus shipping change, * we need to make sure the amount gets adjusted to match any changes caused by other factors, * i.e. promotions that were previously active but are no longer. * * TODO: Deprecated - can be removed once we remove support for the "shipping" & "adjustment" input * fields for refunds. */ async calculateRefundAdjustment(ctx, delta, refundInput) { var _a, _b, _c; const existingAdjustment = (_a = refundInput.adjustment) !== null && _a !== void 0 ? _a : 0; let itemAmount = 0; // TODO: figure out what this should be for (const lineInput of (_b = refundInput.lines) !== null && _b !== void 0 ? _b : []) { const orderLine = await this.connection.getEntityOrThrow(ctx, order_line_entity_1.OrderLine, lineInput.orderLineId); itemAmount += orderLine.proratedUnitPriceWithTax * lineInput.quantity; } const calculatedDelta = itemAmount + ((_c = refundInput.shipping) !== null && _c !== void 0 ? _c : 0) + existingAdjustment; const absDelta = Math.abs(delta); return absDelta !== calculatedDelta ? absDelta - calculatedDelta : 0; } getOrderPayments(ctx, orderId) { return this.connection.getRepository(ctx, payment_entity_1.Payment).find({ relations: ['refunds'], where: { order: { id: orderId }, }, }); } async customFieldsAreEqual(ctx, orderLine, inputCustomFields, existingCustomFields) { const customFieldDefs = this.configService.customFields.OrderLine; if (inputCustomFields == null && typeof existingCustomFields === 'object') { // A null value for an OrderLine customFields input is the equivalent // of every property of an existing customFields object being null // or equal to the defaultValue for (const def of customFieldDefs) { const key = def.name; const existingValue = this.coerceValue(def, existingCustomFields); if (existingValue != null && (!def.list || (existingValue === null || existingValue === void 0 ? void 0 : existingValue.length) !== 0)) { if (def.defaultValue != null) { if (existingValue !== def.defaultValue) { return false; } } else { return false; } } } return true; } const customFieldRelations = customFieldDefs.filter(d => d.type === 'relation'); let lineWithCustomFieldRelations; if (customFieldRelations.length) { // for relation types, we need to actually query the DB and check if there is an // existing entity assigned. lineWithCustomFieldRelations = await this.connection .getRepository(ctx, order_line_entity_1.OrderLine) .findOne({ where: { id: orderLine.id }, relations: customFieldRelations.map(r => `customFields.${r.name}`), }) .then(result => result !== null && result !== void 0 ? result : undefined); } for (const def of customFieldDefs) { const key = def.name; const existingValue = this.coerceValue(def, existingCustomFields); if (def.type !== 'relation' && existingValue !== undefined) { const valuesMatch = JSON.stringify(inputCustomFields === null || inputCustomFields === void 0 ? void 0 : inputCustomFields[key]) === JSON.stringify(existingValue); const undefinedMatchesNull = existingValue === null && (inputCustomFields === null || inputCustomFields === void 0 ? void 0 : inputCustomFields[key]) === undefined; const defaultValueMatch = (inputCustomFields === null || inputCustomFields === void 0 ? void 0 : inputCustomFields[key]) === undefined && def.defaultValue === existingValue; if (!valuesMatch && !undefinedMatchesNull && !defaultValueMatch) { return false; } } else if (def.type === 'relation') { const inputId = (0, shared_utils_1.getGraphQlInputName)(def); const inputValue = inputCustomFields === null || inputCustomFields === void 0 ? void 0 : inputCustomFields[inputId]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const existingRelation = lineWithCustomFieldRelations.customFields[key]; if (inputValue) { const customFieldNotEqual = def.list ? JSON.stringify(inputValue.sort()) !== JSON.stringify(existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.map((relation) => relation.id).sort()) : inputValue !== (existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.id); if (customFieldNotEqual) { return false; } } } } return true; } /** * This function is required because with the MySQL driver, boolean customFields with a default * of `false` were being represented as `0`, thus causing the equality check to fail. * So if it's a boolean, we'll explicitly coerce the value to a boolean. */ coerceValue(def, existingCustomFields) { const key = def.name; return def.type === 'boolean' && typeof (existingCustomFields === null || existingCustomFields === void 0 ? void 0 : existingCustomFields[key]) === 'number' ? !!(existingCustomFields === null || existingCustomFields === void 0 ? void 0 : existingCustomFields[key]) : existingCustomFields === null || existingCustomFields === void 0 ? void 0 : existingCustomFields[key]; } async getProductVariantOrThrow(ctx, productVariantId, order) { const variant = await this.connection.findOneInChannel(ctx, product_variant_entity_1.ProductVariant, productVariantId, ctx.channelId, { relations: ['product', 'productVariantPrices', 'taxCategory'], loadEagerRelations: false, where: { deletedAt: (0, typeorm_1.IsNull)() }, }); if (variant) { return await this.productVariantService.applyChannelPriceAndTax(variant, ctx, order); } else { throw new errors_1.EntityNotFoundError('ProductVariant', productVariantId); } } }; exports.OrderModifier = OrderModifier; exports.OrderModifier = OrderModifier = __decorate([ (0, common_1.Injectable)(), (0, instrument_decorator_1.Instrument)(), __metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection, config_service_1.ConfigService, order_calculator_1.OrderCalculator, payment_service_1.PaymentService, country_service_1.CountryService, stock_movement_service_1.StockMovementService, product_variant_service_1.ProductVariantService, custom_field_relation_service_1.CustomFieldRelationService, promotion_service_1.PromotionService, event_bus_2.EventBus, shipping_calculator_1.ShippingCalculator, history_service_1.HistoryService, translator_service_1.TranslatorService]) ], OrderModifier); //# sourceMappingURL=order-modifier.js.map