@vendure/core
Version:
A modern, headless ecommerce framework
1,011 lines • 82 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.OrderService = void 0;
const common_1 = require("@nestjs/common");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const omit_1 = require("@vendure/common/lib/omit");
const shared_utils_1 = require("@vendure/common/lib/shared-utils");
const typeorm_1 = require("typeorm");
const FindOptionsUtils_1 = require("typeorm/find-options/FindOptionsUtils");
const request_context_cache_service_1 = require("../../cache/request-context-cache.service");
const constants_1 = require("../../common/constants");
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 tax_utils_1 = require("../../common/tax-utils");
const utils_1 = require("../../common/utils");
const config_service_1 = require("../../config/config.service");
const vendure_logger_1 = require("../../config/logger/vendure-logger");
const transactional_connection_1 = require("../../connection/transactional-connection");
const fulfillment_line_entity_1 = require("../../entity/order-line-reference/fulfillment-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 refund_entity_1 = require("../../entity/refund/refund.entity");
const session_entity_1 = require("../../entity/session/session.entity");
const shipping_line_entity_1 = require("../../entity/shipping-line/shipping-line.entity");
const surcharge_entity_1 = require("../../entity/surcharge/surcharge.entity");
const event_bus_1 = require("../../event-bus/event-bus");
const coupon_code_event_1 = require("../../event-bus/events/coupon-code-event");
const order_event_1 = require("../../event-bus/events/order-event");
const order_line_event_1 = require("../../event-bus/events/order-line-event");
const order_state_transition_event_1 = require("../../event-bus/events/order-state-transition-event");
const refund_event_1 = require("../../event-bus/events/refund-event");
const refund_state_transition_event_1 = require("../../event-bus/events/refund-state-transition-event");
const custom_field_relation_service_1 = require("../helpers/custom-field-relation/custom-field-relation.service");
const list_query_builder_1 = require("../helpers/list-query-builder/list-query-builder");
const order_calculator_1 = require("../helpers/order-calculator/order-calculator");
const order_merger_1 = require("../helpers/order-merger/order-merger");
const order_modifier_1 = require("../helpers/order-modifier/order-modifier");
const order_state_machine_1 = require("../helpers/order-state-machine/order-state-machine");
const refund_state_machine_1 = require("../helpers/refund-state-machine/refund-state-machine");
const shipping_calculator_1 = require("../helpers/shipping-calculator/shipping-calculator");
const translator_service_1 = require("../helpers/translator/translator.service");
const order_utils_1 = require("../helpers/utils/order-utils");
const patch_entity_1 = require("../helpers/utils/patch-entity");
const channel_service_1 = require("./channel.service");
const country_service_1 = require("./country.service");
const customer_service_1 = require("./customer.service");
const fulfillment_service_1 = require("./fulfillment.service");
const history_service_1 = require("./history.service");
const payment_method_service_1 = require("./payment-method.service");
const payment_service_1 = require("./payment.service");
const product_variant_service_1 = require("./product-variant.service");
const promotion_service_1 = require("./promotion.service");
const stock_level_service_1 = require("./stock-level.service");
/**
* @description
* Contains methods relating to {@link Order} entities.
*
* @docsCategory services
*/
let OrderService = class OrderService {
constructor(connection, configService, productVariantService, customerService, countryService, orderCalculator, shippingCalculator, orderStateMachine, orderMerger, paymentService, paymentMethodService, fulfillmentService, listQueryBuilder, refundStateMachine, historyService, promotionService, eventBus, channelService, orderModifier, customFieldRelationService, requestCache, translator, stockLevelService) {
this.connection = connection;
this.configService = configService;
this.productVariantService = productVariantService;
this.customerService = customerService;
this.countryService = countryService;
this.orderCalculator = orderCalculator;
this.shippingCalculator = shippingCalculator;
this.orderStateMachine = orderStateMachine;
this.orderMerger = orderMerger;
this.paymentService = paymentService;
this.paymentMethodService = paymentMethodService;
this.fulfillmentService = fulfillmentService;
this.listQueryBuilder = listQueryBuilder;
this.refundStateMachine = refundStateMachine;
this.historyService = historyService;
this.promotionService = promotionService;
this.eventBus = eventBus;
this.channelService = channelService;
this.orderModifier = orderModifier;
this.customFieldRelationService = customFieldRelationService;
this.requestCache = requestCache;
this.translator = translator;
this.stockLevelService = stockLevelService;
}
/**
* @description
* Returns an array of all the configured states and transitions of the order process. This is
* based on the default order process plus all configured {@link OrderProcess} objects
* defined in the {@link OrderOptions} `process` array.
*/
getOrderProcessStates() {
return Object.entries(this.orderStateMachine.config.transitions).map(([name, { to }]) => ({
name,
to,
}));
}
findAll(ctx, options, relations) {
return this.listQueryBuilder
.build(order_entity_1.Order, options, {
ctx,
relations: relations !== null && relations !== void 0 ? relations : [
'lines',
'customer',
'lines.productVariant',
'channels',
'shippingLines',
'payments',
],
channelId: ctx.channelId,
customPropertyMap: {
customerLastName: 'customer.lastName',
transactionId: 'payments.transactionId',
},
})
.getManyAndCount()
.then(([items, totalItems]) => {
return {
items,
totalItems,
};
});
}
async findOne(ctx, orderId, relations) {
const qb = this.connection.getRepository(ctx, order_entity_1.Order).createQueryBuilder('order');
const effectiveRelations = relations !== null && relations !== void 0 ? relations : [
'channels',
'customer',
'customer.user',
'lines',
'lines.productVariant',
'lines.productVariant.taxCategory',
'lines.productVariant.productVariantPrices',
'lines.productVariant.translations',
'lines.featuredAsset',
'lines.taxCategory',
'shippingLines',
'surcharges',
];
if (relations &&
effectiveRelations.includes('lines.productVariant') &&
!effectiveRelations.includes('lines.productVariant.taxCategory')) {
effectiveRelations.push('lines.productVariant.taxCategory');
}
// Split relations into two groups for different loading strategies:
// Main order relations - loaded with 'query' strategy for performance
const orderRelations = effectiveRelations.filter(r => !r.startsWith('lines'));
// Lines relations - loaded with 'join' strategy to enable multi-column sorting
const lineRelations = effectiveRelations
.filter(r => r.startsWith('lines.'))
.map(r => r.replace('lines.', ''));
qb.setFindOptions({
relations: orderRelations,
relationLoadStrategy: 'query',
})
.leftJoin('order.channels', 'channel')
.where('order.id = :orderId', { orderId })
.andWhere('channel.id = :channelId', { channelId: ctx.channelId });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
const order = await qb.getOne();
if (order) {
const hasLinesRelations = effectiveRelations.some(r => r.startsWith('lines'));
if (hasLinesRelations) {
const linesQb = this.connection.getRepository(ctx, order_line_entity_1.OrderLine).createQueryBuilder('line');
linesQb
.setFindOptions({
relations: lineRelations,
})
.where('line.orderId = :orderId', { orderId })
.addOrderBy('line.createdAt', 'ASC')
.addOrderBy('line.productVariantId', 'ASC');
const lines = await linesQb.getMany();
order.lines = lines;
}
if (effectiveRelations.includes('lines.productVariant')) {
for (const line of order.lines) {
line.productVariant = this.translator.translate(await this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx, order), ctx);
}
}
return order;
}
}
async findOneByCode(ctx, orderCode, relations) {
const order = await this.connection.getRepository(ctx, order_entity_1.Order).findOne({
relations: ['customer'],
where: {
code: orderCode,
},
});
return order ? this.findOne(ctx, order.id, relations) : undefined;
}
async findOneByOrderLineId(ctx, orderLineId, relations) {
const order = await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.innerJoin('order.lines', 'line', 'line.id = :orderLineId', { orderLineId })
.getOne();
return order ? this.findOne(ctx, order.id, relations) : undefined;
}
async findByCustomerId(ctx, customerId, options, relations) {
const effectiveRelations = (relations !== null && relations !== void 0 ? relations : ['lines', 'customer', 'channels', 'shippingLines']).filter(r =>
// Don't join productVariant because it messes with the
// price calculation in certain edge-case field resolver scenarios
!r.includes('productVariant'));
return this.listQueryBuilder
.build(order_entity_1.Order, options, {
relations: relations !== null && relations !== void 0 ? relations : ['lines', 'customer', 'channels', 'shippingLines'],
channelId: ctx.channelId,
ctx,
})
.andWhere('order.state != :draftState', { draftState: 'Draft' })
.andWhere('order.customer.id = :customerId', { customerId })
.getManyAndCount()
.then(([items, totalItems]) => {
return {
items,
totalItems,
};
});
}
/**
* @description
* Returns all {@link Payment} entities associated with the Order.
*/
getOrderPayments(ctx, orderId) {
return this.connection.getRepository(ctx, payment_entity_1.Payment).find({
relations: ['refunds'],
where: {
order: { id: orderId },
},
});
}
/**
* @description
* Returns an array of any {@link OrderModification} entities associated with the Order.
*/
getOrderModifications(ctx, orderId) {
return this.connection.getRepository(ctx, order_modification_entity_1.OrderModification).find({
where: {
order: { id: orderId },
},
relations: ['lines', 'payment', 'refund', 'surcharges'],
});
}
/**
* @description
* Returns any {@link Refund}s associated with a {@link Payment}.
*/
getPaymentRefunds(ctx, paymentId) {
return this.connection.getRepository(ctx, refund_entity_1.Refund).find({
where: {
paymentId,
},
});
}
getSellerOrders(ctx, order) {
return this.connection.getRepository(ctx, order_entity_1.Order).find({
where: {
aggregateOrderId: order.id,
},
relations: ['channels'],
});
}
async getAggregateOrder(ctx, order) {
return order.aggregateOrderId == null
? undefined
: this.connection
.getRepository(ctx, order_entity_1.Order)
.findOne({ where: { id: order.aggregateOrderId }, relations: ['channels', 'lines'] })
.then(result => result !== null && result !== void 0 ? result : undefined);
}
getOrderChannels(ctx, order) {
return this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.relation('channels')
.of(order)
.loadMany();
}
/**
* @description
* Returns any Order associated with the specified User's Customer account
* that is still in the `active` state.
*/
async getActiveOrderForUser(ctx, userId) {
const customer = await this.customerService.findOneByUserId(ctx, userId);
if (customer) {
const activeOrder = await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.innerJoinAndSelect('order.channels', 'channel', 'channel.id = :channelId', {
channelId: ctx.channelId,
})
.leftJoinAndSelect('order.customer', 'customer')
.leftJoinAndSelect('order.shippingLines', 'shippingLines')
.where('order.active = :active', { active: true })
.andWhere('order.customer.id = :customerId', { customerId: customer.id })
.orderBy('order.createdAt', 'DESC')
.getOne();
if (activeOrder) {
return this.findOne(ctx, activeOrder.id);
}
}
}
/**
* @description
* Creates a new, empty Order. If a `userId` is passed, the Order will get associated with that
* User's Customer account.
*/
async create(ctx, userId) {
const newOrder = await this.createEmptyOrderEntity(ctx);
if (userId) {
const customer = await this.customerService.findOneByUserId(ctx, userId);
if (customer) {
newOrder.customer = customer;
}
}
await this.channelService.assignToCurrentChannel(newOrder, ctx);
const order = await this.connection.getRepository(ctx, order_entity_1.Order).save(newOrder);
await this.eventBus.publish(new order_event_1.OrderEvent(ctx, order, 'created'));
const transitionResult = await this.transitionToState(ctx, order.id, 'AddingItems');
if ((0, error_result_1.isGraphQlErrorResult)(transitionResult)) {
// this should never occur, so we will throw rather than return
throw transitionResult;
}
return transitionResult;
}
async createDraft(ctx) {
const newOrder = await this.createEmptyOrderEntity(ctx);
newOrder.active = false;
await this.channelService.assignToCurrentChannel(newOrder, ctx);
const order = await this.connection.getRepository(ctx, order_entity_1.Order).save(newOrder);
await this.eventBus.publish(new order_event_1.OrderEvent(ctx, order, 'created'));
const transitionResult = await this.transitionToState(ctx, order.id, 'Draft');
if ((0, error_result_1.isGraphQlErrorResult)(transitionResult)) {
// this should never occur, so we will throw rather than return
throw transitionResult;
}
return transitionResult;
}
async createEmptyOrderEntity(ctx) {
return new order_entity_1.Order({
type: generated_types_1.OrderType.Regular,
code: await this.configService.orderOptions.orderCodeStrategy.generate(ctx),
state: this.orderStateMachine.getInitialState(),
lines: [],
surcharges: [],
couponCodes: [],
modifications: [],
shippingAddress: {},
billingAddress: {},
subTotal: 0,
subTotalWithTax: 0,
currencyCode: ctx.currencyCode,
});
}
/**
* @description
* Updates the custom fields of an Order.
*/
async updateCustomFields(ctx, orderId, customFields) {
let order = await this.getOrderOrThrow(ctx, orderId);
order = (0, patch_entity_1.patchEntity)(order, { customFields });
const updatedOrder = await this.connection.getRepository(ctx, order_entity_1.Order).save(order);
await this.customFieldRelationService.updateRelations(ctx, order_entity_1.Order, { customFields }, updatedOrder);
await this.eventBus.publish(new order_event_1.OrderEvent(ctx, updatedOrder, 'updated', { customFields }));
return updatedOrder;
}
/**
* @description
* Updates the Customer which is assigned to a given Order. The target Customer must be assigned to the same
* Channels as the Order, otherwise an error will be thrown.
*
* @since 2.2.0
*/
async updateOrderCustomer(ctx, { customerId, orderId, note }) {
const order = await this.getOrderOrThrow(ctx, orderId, ['channels', 'customer']);
const currentCustomer = order.customer;
if ((currentCustomer === null || currentCustomer === void 0 ? void 0 : currentCustomer.id) === customerId) {
// No change in customer, so just return the order as-is
return order;
}
const targetCustomer = await this.customerService.findOne(ctx, customerId, ['channels']);
if (!targetCustomer) {
throw new errors_1.EntityNotFoundError('Customer', customerId);
}
// ensure the customer is assigned to the same channels as the order
const channelIds = order.channels.map(c => c.id);
const customerChannelIds = targetCustomer.channels.map(c => c.id);
const missingChannelIds = channelIds.filter(id => !customerChannelIds.includes(id));
if (missingChannelIds.length) {
throw new errors_1.UserInputError(`error.target-customer-not-assigned-to-order-channels`, {
channelIds: missingChannelIds.join(', '),
});
}
const updatedOrder = await this.addCustomerToOrder(ctx, order.id, targetCustomer);
await this.eventBus.publish(new order_event_1.OrderEvent(ctx, updatedOrder, 'updated', targetCustomer));
await this.historyService.createHistoryEntryForOrder({
ctx,
orderId,
type: generated_types_1.HistoryEntryType.ORDER_CUSTOMER_UPDATED,
data: {
previousCustomerId: currentCustomer === null || currentCustomer === void 0 ? void 0 : currentCustomer.id,
previousCustomerName: currentCustomer && `${currentCustomer.firstName} ${currentCustomer.lastName}`,
newCustomerId: targetCustomer.id,
newCustomerName: `${targetCustomer.firstName} ${targetCustomer.lastName}`,
note,
},
});
return updatedOrder;
}
/**
* @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, orderId, productVariantId, quantity, customFields, relations) {
const result = await this.addItemsToOrder(ctx, orderId, [{ productVariantId, quantity, customFields }], relations);
if (result.errorResults.length) {
return result.errorResults[0];
}
else {
return result.order;
}
}
/**
* @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, orderId, items, relations) {
const order = await this.getOrderOrThrow(ctx, orderId);
const errorResults = [];
const updatedOrderLines = [];
addItem: 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, product_variant_entity_1.ProductVariant, productVariantId, {
relations: ['product'],
where: {
enabled: true,
deletedAt: (0, typeorm_1.IsNull)(),
},
loadEagerRelations: false,
});
if (variant.product.enabled === false) {
throw new errors_1.EntityNotFoundError('ProductVariant', productVariantId);
}
const existingQuantityInOtherLines = (0, shared_utils_1.summate)(order.lines.filter(l => (0, utils_1.idsAreEqual)(l.productVariantId, productVariantId) &&
!(0, utils_1.idsAreEqual)(l.id, existingOrderLine === null || existingOrderLine === void 0 ? void 0 : existingOrderLine.id)), 'quantity');
const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(ctx, variant, quantity, existingOrderLine === null || existingOrderLine === void 0 ? void 0 : existingOrderLine.quantity, existingQuantityInOtherLines);
if (correctedQuantity === 0) {
errorResults.push(new generated_graphql_shop_errors_1.InsufficientStockError({ order, quantityAvailable: correctedQuantity }));
continue;
}
const { orderInterceptors } = this.configService.orderOptions;
for (const interceptor of orderInterceptors) {
if (interceptor.willAddItemToOrder) {
const error = await interceptor.willAddItemToOrder(ctx, order, {
productVariant: variant,
quantity: correctedQuantity,
customFields,
});
if (error) {
errorResults.push(new generated_graphql_shop_errors_1.OrderInterceptorError({ interceptorError: error }));
continue addItem;
}
}
}
const orderLine = await this.orderModifier.getOrCreateOrderLine(ctx, order, productVariantId, customFields);
if (correctedQuantity < quantity) {
const newQuantity = (existingOrderLine ? existingOrderLine === null || existingOrderLine === void 0 ? void 0 : 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 generated_graphql_shop_errors_1.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 generated_graphql_shop_errors_1.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, orderId, orderLineId, quantity, customFields, relations) {
const result = await this.adjustOrderLines(ctx, orderId, [{ orderLineId, quantity, customFields }], relations);
if (result.errorResults.length) {
return result.errorResults[0];
}
else {
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, orderId, lines, relations) {
const order = await this.getOrderOrThrow(ctx, orderId);
const errorResults = [];
const updatedOrderLines = [];
adjustLine: 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;
}
const { orderInterceptors } = this.configService.orderOptions;
for (const interceptor of orderInterceptors) {
if (interceptor.willAdjustOrderLine) {
const error = await interceptor.willAdjustOrderLine(ctx, order, {
orderLine,
quantity,
customFields,
});
if (error) {
errorResults.push(new generated_graphql_shop_errors_1.OrderInterceptorError({ interceptorError: error }));
continue adjustLine;
}
}
}
if (customFields != null) {
// Merge custom fields instead of replacing them entirely
// This preserves existing values while allowing updates and null-based unsetting
const existingCustomFields = orderLine.customFields || {};
const mergedCustomFields = Object.assign({}, existingCustomFields);
for (const [key, value] of Object.entries(customFields)) {
if (value !== undefined) {
// Update with the new value (including explicit null to unset)
mergedCustomFields[key] = value;
}
// If value is undefined, preserve the existing value (don't set it)
}
orderLine.customFields = mergedCustomFields;
await this.customFieldRelationService.updateRelations(ctx, order_line_entity_1.OrderLine, { customFields: mergedCustomFields }, orderLine);
}
const existingQuantityInOtherLines = (0, shared_utils_1.summate)(order.lines.filter(l => (0, utils_1.idsAreEqual)(l.productVariantId, orderLine.productVariantId) &&
!(0, utils_1.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 => !(0, utils_1.idsAreEqual)(l.id, orderLine.id));
const deletedOrderLine = new order_line_entity_1.OrderLine(orderLine);
await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).remove(orderLine);
await this.eventBus.publish(new order_line_event_1.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 generated_graphql_shop_errors_1.InsufficientStockError({
quantityAvailable: correctedQuantity,
order,
}));
}
}
const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines, relations);
for (const [i, errorResult] of Object.entries(errorResults)) {
if (errorResult.__typename === 'InsufficientStockError') {
errorResults[+i] = new generated_graphql_shop_errors_1.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, orderId, orderLineId) {
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, orderId, orderLineIds) {
const order = await this.getOrderOrThrow(ctx, orderId);
const validationError = this.assertAddingItemsState(order);
if (validationError) {
return validationError;
}
const orderLinesToDelete = [];
for (const orderLineId of orderLineIds) {
const orderLine = this.getOrderLineOrThrow(order, orderLineId);
const { orderInterceptors } = this.configService.orderOptions;
for (const interceptor of orderInterceptors) {
if (interceptor.willRemoveItemFromOrder) {
const error = await interceptor.willRemoveItemFromOrder(ctx, order, orderLine);
if (error) {
return new generated_graphql_shop_errors_1.OrderInterceptorError({ interceptorError: error });
}
}
}
orderLinesToDelete.push(orderLine);
}
order.lines = order.lines.filter(line => !orderLineIds.find(olId => (0, utils_1.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_entity_1.Order).save(order, { reload: false });
const updatedOrder = await this.applyPriceAdjustments(ctx, order);
for (const orderLine of orderLinesToDelete) {
const deletedOrderLine = new order_line_entity_1.OrderLine(orderLine);
await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).remove(orderLine);
await this.eventBus.publish(new order_line_event_1.OrderLineEvent(ctx, order, deletedOrderLine, 'deleted'));
}
return updatedOrder;
}
/**
* @description
* Removes all OrderLines from the Order.
*/
async removeAllItemsFromOrder(ctx, orderId) {
const order = await this.getOrderOrThrow(ctx, orderId);
const validationError = this.assertAddingItemsState(order);
if (validationError) {
return validationError;
}
const { orderInterceptors } = this.configService.orderOptions;
for (const orderLine of order.lines) {
for (const interceptor of orderInterceptors) {
if (interceptor.willRemoveItemFromOrder) {
const error = await interceptor.willRemoveItemFromOrder(ctx, order, orderLine);
if (error) {
return new generated_graphql_shop_errors_1.OrderInterceptorError({ interceptorError: error });
}
}
}
}
await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).remove(order.lines);
order.lines = [];
const updatedOrder = await this.applyPriceAdjustments(ctx, order);
return updatedOrder;
}
/**
* @description
* Adds a {@link Surcharge} to the Order.
*/
async addSurchargeToOrder(ctx, orderId, surchargeInput) {
const order = await this.getOrderOrThrow(ctx, orderId);
const surcharge = await this.connection.getRepository(ctx, surcharge_entity_1.Surcharge).save(new surcharge_entity_1.Surcharge(Object.assign({ taxLines: [], sku: '', listPriceIncludesTax: ctx.channel.pricesIncludeTax, order }, surchargeInput)));
order.surcharges.push(surcharge);
const updatedOrder = await this.applyPriceAdjustments(ctx, order);
return updatedOrder;
}
/**
* @description
* Removes a {@link Surcharge} from the Order.
*/
async removeSurchargeFromOrder(ctx, orderId, surchargeId) {
const order = await this.getOrderOrThrow(ctx, orderId);
const surcharge = await this.connection.getEntityOrThrow(ctx, surcharge_entity_1.Surcharge, surchargeId);
if (order.surcharges.find(s => (0, utils_1.idsAreEqual)(s.id, surcharge.id))) {
order.surcharges = order.surcharges.filter(s => !(0, utils_1.idsAreEqual)(s.id, surchargeId));
const updatedOrder = await this.applyPriceAdjustments(ctx, order);
await this.connection.getRepository(ctx, surcharge_entity_1.Surcharge).remove(surcharge);
return updatedOrder;
}
else {
return order;
}
}
/**
* @description
* Applies a coupon code to the Order, which should be a valid coupon code as specified in the configuration
* of an active {@link Promotion}.
*/
async applyCouponCode(ctx, orderId, couponCode) {
const order = await this.getOrderOrThrow(ctx, orderId);
if (order.couponCodes.includes(couponCode)) {
return order;
}
const validationResult = await this.promotionService.validateCouponCode(ctx, couponCode, order.customer && order.customer.id);
if ((0, error_result_1.isGraphQlErrorResult)(validationResult)) {
return validationResult;
}
order.couponCodes.push(couponCode);
await this.historyService.createHistoryEntryForOrder({
ctx,
orderId: order.id,
type: generated_types_1.HistoryEntryType.ORDER_COUPON_APPLIED,
data: { couponCode, promotionId: validationResult.id },
});
await this.eventBus.publish(new coupon_code_event_1.CouponCodeEvent(ctx, couponCode, orderId, 'assigned'));
return this.applyPriceAdjustments(ctx, order);
}
/**
* @description
* Removes a coupon code from the Order.
*/
async removeCouponCode(ctx, orderId, couponCode) {
const order = await this.getOrderOrThrow(ctx, orderId);
if (order.couponCodes.includes(couponCode)) {
// When removing a couponCode which has triggered an Order-level discount
// we need to make sure we persist the changes to the adjustments array of
// any affected OrderLines.
const affectedOrderLines = order.lines.filter(line => line.adjustments.filter(a => a.type === generated_types_1.AdjustmentType.DISTRIBUTED_ORDER_PROMOTION)
.length);
order.couponCodes = order.couponCodes.filter(cc => cc !== couponCode);
await this.historyService.createHistoryEntryForOrder({
ctx,
orderId: order.id,
type: generated_types_1.HistoryEntryType.ORDER_COUPON_REMOVED,
data: { couponCode },
});
await this.eventBus.publish(new coupon_code_event_1.CouponCodeEvent(ctx, couponCode, orderId, 'removed'));
const result = await this.applyPriceAdjustments(ctx, order);
await this.connection.getRepository(ctx, order_line_entity_1.OrderLine).save(affectedOrderLines);
return result;
}
else {
return order;
}
}
/**
* @description
* Returns all {@link Promotion}s associated with an Order.
*/
async getOrderPromotions(ctx, orderId) {
const order = await this.connection.getEntityOrThrow(ctx, order_entity_1.Order, orderId, {
channelId: ctx.channelId,
relations: ['promotions'],
});
return order.promotions.map(p => this.translator.translate(p, ctx)) || [];
}
/**
* @description
* Returns the next possible states that the Order may transition to.
*/
getNextOrderStates(order) {
return this.orderStateMachine.getNextStates(order);
}
/**
* @description
* Sets the shipping address for the Order.
*/
async setShippingAddress(ctx, orderId, input) {
const order = await this.getOrderOrThrow(ctx, orderId);
const country = await this.countryService.findOneByCode(ctx, input.countryCode);
const shippingAddress = Object.assign(Object.assign({}, input), { countryCode: input.countryCode, country: country.name });
await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.update(order_entity_1.Order)
.set({ shippingAddress })
.where('id = :id', { id: order.id })
.execute();
order.shippingAddress = shippingAddress;
// Since a changed ShippingAddress could alter the activeTaxZone,
// we will remove any cached activeTaxZone, so it can be re-calculated
// as needed.
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone, undefined);
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone_PPA, undefined);
return this.applyPriceAdjustments(ctx, order, order.lines);
}
/**
* @description
* Sets the billing address for the Order.
*/
async setBillingAddress(ctx, orderId, input) {
const order = await this.getOrderOrThrow(ctx, orderId);
const country = await this.countryService.findOneByCode(ctx, input.countryCode);
const billingAddress = Object.assign(Object.assign({}, input), { countryCode: input.countryCode, country: country.name });
await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.update(order_entity_1.Order)
.set({ billingAddress })
.where('id = :id', { id: order.id })
.execute();
order.billingAddress = billingAddress;
// Since a changed BillingAddress could alter the activeTaxZone,
// we will remove any cached activeTaxZone, so it can be re-calculated
// as needed.
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone, undefined);
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone_PPA, undefined);
return this.applyPriceAdjustments(ctx, order, order.lines);
}
/**
* @description
* Unsets the shipping address for the Order.
*
* @since 3.1.0
*/
async unsetShippingAddress(ctx, orderId) {
const order = await this.getOrderOrThrow(ctx, orderId);
await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.update(order_entity_1.Order)
.set({ shippingAddress: {} })
.where('id = :id', { id: order.id })
.execute();
order.shippingAddress = {};
// Since a changed ShippingAddress could alter the activeTaxZone,
// we will remove any cached activeTaxZone, so it can be re-calculated
// as needed.
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone, undefined);
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone_PPA, undefined);
return this.applyPriceAdjustments(ctx, order, order.lines);
}
/**
* @description
* Unsets the billing address for the Order.
*
* @since 3.1.0
*/
async unsetBillingAddress(ctx, orderId) {
const order = await this.getOrderOrThrow(ctx, orderId);
await this.connection
.getRepository(ctx, order_entity_1.Order)
.createQueryBuilder('order')
.update(order_entity_1.Order)
.set({ billingAddress: {} })
.where('id = :id', { id: order.id })
.execute();
order.billingAddress = {};
// Since a changed BillingAddress could alter the activeTaxZone,
// we will remove any cached activeTaxZone, so it can be re-calculated
// as needed.
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone, undefined);
this.requestCache.set(ctx, constants_1.CacheKey.ActiveTaxZone_PPA, undefined);
return this.applyPriceAdjustments(ctx, order, order.lines);
}
/**
* @description
* Returns an array of quotes stating which {@link ShippingMethod}s may be applied to this Order.
* This is determined by the configured {@link ShippingEligibilityChecker} of each ShippingMethod.
*
* The quote also includes a price for each method, as determined by the configured
* {@link ShippingCalculator} of each eligible ShippingMethod.
*/
async getEligibleShippingMethods(ctx, orderId) {
const order = await this.getOrderOrThrow(ctx, orderId);
const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
return eligibleMethods.map(eligible => {
const { price, taxRate, priceIncludesTax, metadata } = eligible.result;
return {
id: eligible.method.id,
price: priceIncludesTax ? (0, tax_utils_1.netPriceOf)(price, taxRate) : price,
priceWithTax: priceIncludesTax ? price : (0, tax_utils_1.grossPriceOf)(price, taxRate),
description: eligible.method.description,
name: eligible.method.name,
code: eligible.method.code,
metadata,
customFields: eligible.method.customFields,
};
});
}
/**
* @description
* Returns an array of quotes stating which {@link PaymentMethod}s may be used on this Order.
*/
async getEligiblePaymentMethods(ctx, orderId) {
const order = await this.getOrderOrThrow(ctx, orderId);
return this.paymentMethodService.getEligiblePaymentMethods(ctx, order);
}
/**
* @description
* Sets the ShippingMethod to be used on this Order.
*/
async setShippingMethod(ctx, orderId, shippingMethodIds) {
const order = await this.getOrderOrThrow(ctx, orderId);
const validationError = this.assertAddingItemsState(order);
if (validationError) {
return validationError;
}
const result = await this.orderModifier.setShippingMethods(ctx, order, shippingMethodIds);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
return result;
}
const updatedOrder = await this.getOrderOrThrow(ctx, orderId);
await this.applyPriceAdjustments(ctx, updatedOrder);
return this.connection.getRepository(ctx, order_entity_1.Order).save(updatedOrder);
}
/**
* @description
* Transitions the Order to the given state.
*/
async transitionToState(ctx, orderId, state) {
const order = await this.getOrderOrThrow(ctx, orderId);
order.payments = await this.getOrderPayments(ctx, orderId);
const fromState = order.state;
let finalize;
try {
const result = await this.orderStateMachine.transition(ctx, order, state);
finalize = result.finalize;
}
catch (e) {
const transitionError = ctx.translate(e.message, { fromState, toState: state });
return new generated_graphql_shop_errors_1.OrderStateTransitionError({ transitionError, fromState, toState: state });
}
await this.connection.getRepository(ctx, order_entity_1.Order).save(order, { reload: false });
await this.eventBus.publish(new order_state_transition_event_1.OrderStateTransitionEvent(fromState, state, ctx, order));
await finalize();
await this.connection.getRepository(ctx, order_entity_1.Order).save(order, { reload: false });
return order;
}
/**
* @description
* Transitions a Fulfillment to the given state and then transitions the Order state based on
* whether all Fulfillments of the Order are shipped or delivered.
*/
async transitionFulfillmentToState(ctx, fulfillmentId, state) {
const result = await this.fulfillmentService.transitionToState(ctx, fulfillmentId, state);
if ((0, error_result_1.isGraphQlErrorResult)(result)) {
return result;
}
return result.fulfillment;
}
/**
* @description
* Transitions a Refund to the given state
*/
async transitionRefundToState(ctx, refundId, state, transactionId) {
const refund = await this.connection.getEntityOrThrow(ctx, refund_entity_1.Refund, refundId, {
relations: ['payment', 'payment.order'],
});
if (transactionId && refund.transactionId !== transactionId) {
refund.transactionId = transactionId;
}
const fromState = refund.state;
const toState = state;
const { finalize } = await this.refundStateMachine.transition(ctx, refund.payment.order, refund, toState);
await this.connection.getRepository(ctx, refund_entity_1.Refund).save(refund);
await finalize();
await this.eventBus.publish(new refund_state_transition_event_1.RefundStateTransitionEvent(fromState, toState, ctx, refund, refund.payment.order));
return refund;
}
/**
* @description
*