@vendure/core
Version:
A modern, headless ecommerce framework
321 lines • 15.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultOrderProcess = void 0;
exports.configureDefaultOrderProcess = configureDefaultOrderProcess;
const generated_types_1 = require("@vendure/common/lib/generated-types");
const unique_1 = require("@vendure/common/lib/unique");
const transactional_connection_1 = require("../../connection/transactional-connection");
const order_entity_1 = require("../../entity/order/order.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 payment_entity_1 = require("../../entity/payment/payment.entity");
const product_variant_entity_1 = require("../../entity/product-variant/product-variant.entity");
const order_placed_event_1 = require("../../event-bus/events/order-placed-event");
const order_utils_1 = require("../../service/helpers/utils/order-utils");
/**
* @description
* Used to configure a customized instance of the default {@link OrderProcess} that ships with Vendure.
* Using this function allows you to turn off certain checks and constraints that are enabled by default.
*
* ```ts
* import { configureDefaultOrderProcess, VendureConfig } from '\@vendure/core';
*
* const myCustomOrderProcess = configureDefaultOrderProcess({
* // Disable the constraint that requires
* // Orders to have a shipping method assigned
* // before payment.
* arrangingPaymentRequiresShipping: false,
* });
*
* export const config: VendureConfig = {
* orderOptions: {
* process: [myCustomOrderProcess],
* },
* };
* ```
* The {@link DefaultOrderProcessOptions} type defines all available options. If you require even
* more customization, you can create your own implementation of the {@link OrderProcess} interface.
*
*
* @docsCategory Orders
* @docsPage OrderProcess
* @since 2.0.0
*/
function configureDefaultOrderProcess(options) {
let connection;
let productVariantService;
let configService;
let eventBus;
let stockMovementService;
let stockLevelService;
let historyService;
let orderSplitter;
const orderProcess = {
transitions: {
Created: {
to: ['AddingItems', 'Draft'],
},
Draft: {
to: ['Cancelled', 'ArrangingPayment'],
},
AddingItems: {
to: ['ArrangingPayment', 'Cancelled'],
},
ArrangingPayment: {
to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems', 'Cancelled'],
},
PaymentAuthorized: {
to: ['PaymentSettled', 'Cancelled', 'Modifying', 'ArrangingAdditionalPayment'],
},
PaymentSettled: {
to: [
'PartiallyDelivered',
'Delivered',
'PartiallyShipped',
'Shipped',
'Cancelled',
'Modifying',
'ArrangingAdditionalPayment',
],
},
PartiallyShipped: {
to: ['Shipped', 'PartiallyDelivered', 'Cancelled', 'Modifying'],
},
Shipped: {
to: ['PartiallyDelivered', 'Delivered', 'Cancelled', 'Modifying'],
},
PartiallyDelivered: {
to: ['Delivered', 'Cancelled', 'Modifying'],
},
Delivered: {
to: ['Cancelled'],
},
Modifying: {
to: [
'PaymentAuthorized',
'PaymentSettled',
'PartiallyShipped',
'Shipped',
'PartiallyDelivered',
'ArrangingAdditionalPayment',
],
},
ArrangingAdditionalPayment: {
to: [
'PaymentAuthorized',
'PaymentSettled',
'PartiallyShipped',
'Shipped',
'PartiallyDelivered',
'Cancelled',
],
},
Cancelled: {
to: [],
},
},
async init(injector) {
// Lazily import these services to avoid a circular dependency error
// due to this being used as part of the DefaultConfig
const ConfigService = await import('../config.service.js').then(m => m.ConfigService);
const EventBus = await import('../../event-bus/index.js').then(m => m.EventBus);
const StockMovementService = await import('../../service/index.js').then(m => m.StockMovementService);
const StockLevelService = await import('../../service/index.js').then(m => m.StockLevelService);
const HistoryService = await import('../../service/index.js').then(m => m.HistoryService);
const OrderSplitter = await import('../../service/index.js').then(m => m.OrderSplitter);
const ProductVariantService = await import('../../service/index.js').then(m => m.ProductVariantService);
connection = injector.get(transactional_connection_1.TransactionalConnection);
productVariantService = injector.get(ProductVariantService);
configService = injector.get(ConfigService);
eventBus = injector.get(EventBus);
stockMovementService = injector.get(StockMovementService);
stockLevelService = injector.get(StockLevelService);
historyService = injector.get(HistoryService);
orderSplitter = injector.get(OrderSplitter);
},
async onTransitionStart(fromState, toState, { ctx, order }) {
if (options.checkModificationPayments !== false && fromState === 'Modifying') {
const modifications = await connection
.getRepository(ctx, order_modification_entity_1.OrderModification)
.find({ where: { order: { id: order.id } }, relations: ['refund', 'payment'] });
if (toState === 'ArrangingAdditionalPayment') {
if (0 < modifications.length &&
modifications.every(modification => modification.isSettled)) {
return 'message.cannot-transition-no-additional-payments-needed';
}
}
else {
if (modifications.some(modification => !modification.isSettled)) {
return 'message.cannot-transition-without-modification-payment';
}
}
}
if (options.checkAdditionalPaymentsAmount !== false &&
fromState === 'ArrangingAdditionalPayment') {
if (toState === 'Cancelled') {
return;
}
const existingPayments = await connection.getRepository(ctx, payment_entity_1.Payment).find({
relations: ['refunds'],
where: {
order: { id: order.id },
},
});
order.payments = existingPayments;
const deficit = order.totalWithTax - (0, order_utils_1.totalCoveredByPayments)(order);
if (0 < deficit) {
return 'message.cannot-transition-from-arranging-additional-payment';
}
}
if (options.checkAllVariantsExist !== false &&
fromState === 'AddingItems' &&
toState !== 'Cancelled' &&
order.lines.length > 0) {
const variantIds = (0, unique_1.unique)(order.lines.map(l => l.productVariant.id));
const qb = connection
.getRepository(ctx, product_variant_entity_1.ProductVariant)
.createQueryBuilder('variant')
.leftJoin('variant.product', 'product')
.where('variant.deletedAt IS NULL')
.andWhere('product.deletedAt IS NULL')
.andWhere('variant.id IN (:...variantIds)', { variantIds });
const availableVariants = await qb.getMany();
if (availableVariants.length !== variantIds.length) {
return 'message.cannot-transition-order-contains-products-which-are-unavailable';
}
}
if (toState === 'ArrangingPayment') {
if (options.arrangingPaymentRequiresContents !== false && order.lines.length === 0) {
return 'message.cannot-transition-to-payment-when-order-is-empty';
}
if (options.arrangingPaymentRequiresCustomer !== false && !order.customer) {
return 'message.cannot-transition-to-payment-without-customer';
}
if (options.arrangingPaymentRequiresShipping !== false &&
(!order.shippingLines || order.shippingLines.length === 0)) {
return 'message.cannot-transition-to-payment-without-shipping-method';
}
if (options.arrangingPaymentRequiresStock !== false) {
const variantsWithInsufficientSaleableStock = [];
for (const line of order.lines) {
const availableStock = await productVariantService.getSaleableStockLevel(ctx, line.productVariant);
if (line.quantity > availableStock) {
variantsWithInsufficientSaleableStock.push(line.productVariant);
}
}
if (variantsWithInsufficientSaleableStock.length) {
return ctx.translate('message.cannot-transition-to-payment-due-to-insufficient-stock', {
productVariantNames: variantsWithInsufficientSaleableStock
.map(v => v.name)
.join(', '),
});
}
}
}
if (options.checkPaymentsCoverTotal !== false) {
if (toState === 'PaymentAuthorized') {
const hasAnAuthorizedPayment = !!order.payments.find(p => p.state === 'Authorized');
if (!(0, order_utils_1.orderTotalIsCovered)(order, ['Authorized', 'Settled']) || !hasAnAuthorizedPayment) {
return 'message.cannot-transition-without-authorized-payments';
}
}
if (toState === 'PaymentSettled' && !(0, order_utils_1.orderTotalIsCovered)(order, 'Settled')) {
return 'message.cannot-transition-without-settled-payments';
}
}
if (options.checkAllItemsBeforeCancel !== false) {
if (toState === 'Cancelled' &&
fromState !== 'AddingItems' &&
fromState !== 'ArrangingPayment') {
if (!(0, order_utils_1.orderLinesAreAllCancelled)(order)) {
return 'message.cannot-transition-unless-all-cancelled';
}
}
}
if (options.checkFulfillmentStates !== false) {
if (toState === 'PartiallyShipped') {
const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
if (!(0, order_utils_1.orderItemsArePartiallyShipped)(orderWithFulfillments)) {
return 'message.cannot-transition-unless-some-order-items-shipped';
}
}
if (toState === 'Shipped') {
const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
if (!(0, order_utils_1.orderItemsAreShipped)(orderWithFulfillments)) {
return 'message.cannot-transition-unless-all-order-items-shipped';
}
}
if (toState === 'PartiallyDelivered') {
const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
if (!(0, order_utils_1.orderItemsArePartiallyDelivered)(orderWithFulfillments)) {
return 'message.cannot-transition-unless-some-order-items-delivered';
}
}
if (toState === 'Delivered') {
const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
if (!(0, order_utils_1.orderItemsAreDelivered)(orderWithFulfillments)) {
return 'message.cannot-transition-unless-all-order-items-delivered';
}
}
}
},
async onTransitionEnd(fromState, toState, data) {
const { ctx, order } = data;
const { stockAllocationStrategy, orderPlacedStrategy } = configService.orderOptions;
if (order.active) {
const shouldSetAsPlaced = orderPlacedStrategy.shouldSetAsPlaced(ctx, fromState, toState, order);
if (shouldSetAsPlaced) {
order.active = false;
order.orderPlacedAt = new Date();
await Promise.all(order.lines.map(line => {
line.orderPlacedQuantity = line.quantity;
return connection
.getRepository(ctx, order_line_entity_1.OrderLine)
.update(line.id, { orderPlacedQuantity: line.quantity });
}));
await eventBus.publish(new order_placed_event_1.OrderPlacedEvent(fromState, toState, ctx, order));
await orderSplitter.createSellerOrders(ctx, order);
}
}
const shouldAllocateStock = await stockAllocationStrategy.shouldAllocateStock(ctx, fromState, toState, order);
if (shouldAllocateStock) {
await stockMovementService.createAllocationsForOrder(ctx, order);
}
if (toState === 'Cancelled') {
order.active = false;
}
if (fromState === 'Draft' && toState === 'ArrangingPayment') {
// Once we exit the Draft state, we can consider the order active,
// which will allow us to run the OrderPlacedStrategy at the correct point.
order.active = true;
}
await historyService.createHistoryEntryForOrder({
orderId: order.id,
type: generated_types_1.HistoryEntryType.ORDER_STATE_TRANSITION,
ctx,
data: {
from: fromState,
to: toState,
},
});
},
};
async function findOrderWithFulfillments(ctx, id) {
return await connection.getEntityOrThrow(ctx, order_entity_1.Order, id, {
relations: ['lines', 'fulfillments', 'fulfillments.lines', 'fulfillments.lines.fulfillment'],
});
}
return orderProcess;
}
/**
* @description
* This is the built-in {@link OrderProcess} that ships with Vendure. A customized version of this process
* can be created using the {@link configureDefaultOrderProcess} function, which allows you to pass in an object
* to enable/disable certain checks.
*
* @docsCategory Orders
* @docsPage OrderProcess
* @since 2.0.0
*/
exports.defaultOrderProcess = configureDefaultOrderProcess({});
//# sourceMappingURL=default-order-process.js.map