@vendure/core
Version:
A modern, headless ecommerce framework
791 lines • 44.5 kB
JavaScript
"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