@unchainedshop/core 4.8.28 → 4.8.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/lib/bulk-importer/handlers/product/upsertVariations.js +5 -3
  2. package/lib/directors/FileDirector.d.ts +4 -4
  3. package/lib/directors/FileDirector.js +2 -13
  4. package/lib/directors/FilterDirector.js +2 -2
  5. package/lib/directors/registryDirector.d.ts +2 -0
  6. package/lib/directors/registryDirector.js +6 -0
  7. package/lib/factory/registerEnrollment.d.ts +8 -1
  8. package/lib/factory/registerEnrollment.js +12 -14
  9. package/lib/factory/registerQuotation.d.ts +2 -1
  10. package/lib/factory/registerQuotation.js +3 -2
  11. package/lib/plugins/PluginRegistry.d.ts +2 -1
  12. package/lib/plugins/PluginRegistry.js +16 -3
  13. package/lib/services/addCartQuotation.d.ts +9 -13
  14. package/lib/services/addCartQuotation.js +43 -38
  15. package/lib/services/reserveOrderDiscountsForCheckout.d.ts +5 -0
  16. package/lib/services/reserveOrderDiscountsForCheckout.js +34 -0
  17. package/lib/services/reserveOrderQuotationsForCheckout.d.ts +8 -0
  18. package/lib/services/reserveOrderQuotationsForCheckout.js +49 -0
  19. package/lib/services/simulateProductDispatching.d.ts +3 -1
  20. package/lib/services/simulateProductDispatching.js +3 -3
  21. package/lib/services/simulateProductInventory.d.ts +3 -1
  22. package/lib/services/simulateProductInventory.js +3 -3
  23. package/lib/services/supportedDeliveryProviders.js +9 -0
  24. package/lib/services/supportedPaymentProviders.js +9 -0
  25. package/lib/services/supportedWarehousingProviders.js +9 -0
  26. package/lib/services/suspendEnrollment.d.ts +5 -0
  27. package/lib/services/suspendEnrollment.js +21 -0
  28. package/lib/services/updateCartItem.d.ts +7 -7
  29. package/lib/services/updateCartItem.js +28 -25
  30. package/lib/services/updateEnrollmentPlan.d.ts +5 -0
  31. package/lib/services/updateEnrollmentPlan.js +26 -0
  32. package/package.json +1 -1
  33. package/lib/errors/common.d.ts +0 -568
  34. package/lib/errors/common.js +0 -789
  35. package/lib/errors/index.d.ts +0 -1
  36. package/lib/errors/index.js +0 -1
  37. package/lib/services/addCartDiscount.d.ts +0 -10
  38. package/lib/services/addCartDiscount.js +0 -27
  39. package/lib/services/addCartItem.d.ts +0 -15
  40. package/lib/services/addCartItem.js +0 -41
  41. package/lib/services/addEmail.d.ts +0 -7
  42. package/lib/services/addEmail.js +0 -17
  43. package/lib/services/addProductAssignment.d.ts +0 -3
  44. package/lib/services/addProductAssignment.js +0 -63
  45. package/lib/services/authenticateWithPassword.d.ts +0 -15
  46. package/lib/services/authenticateWithPassword.js +0 -34
  47. package/lib/services/changePassword.d.ts +0 -10
  48. package/lib/services/changePassword.js +0 -19
  49. package/lib/services/countOrdersWithFilters.d.ts +0 -3
  50. package/lib/services/countOrdersWithFilters.js +0 -25
  51. package/lib/services/createAssortment.d.ts +0 -7
  52. package/lib/services/createAssortment.js +0 -10
  53. package/lib/services/createBookmark.d.ts +0 -8
  54. package/lib/services/createBookmark.js +0 -21
  55. package/lib/services/createDeliveryProvider.d.ts +0 -6
  56. package/lib/services/createDeliveryProvider.js +0 -19
  57. package/lib/services/createEnrollment.d.ts +0 -17
  58. package/lib/services/createEnrollment.js +0 -33
  59. package/lib/services/createProduct.d.ts +0 -7
  60. package/lib/services/createProduct.js +0 -8
  61. package/lib/services/createProductBundleItem.d.ts +0 -5
  62. package/lib/services/createProductBundleItem.js +0 -25
  63. package/lib/services/createProductReview.d.ts +0 -8
  64. package/lib/services/createProductReview.js +0 -12
  65. package/lib/services/deleteDeadCarts.d.ts +0 -2
  66. package/lib/services/deleteDeadCarts.js +0 -17
  67. package/lib/services/deliverOrder.d.ts +0 -5
  68. package/lib/services/deliverOrder.js +0 -26
  69. package/lib/services/findActiveWorkTypes.d.ts +0 -2
  70. package/lib/services/findActiveWorkTypes.js +0 -6
  71. package/lib/services/findDeliveryInterfaces.d.ts +0 -7
  72. package/lib/services/findDeliveryInterfaces.js +0 -13
  73. package/lib/services/findOrdersWithFilters.d.ts +0 -9
  74. package/lib/services/findOrdersWithFilters.js +0 -25
  75. package/lib/services/findPaymentInterfaces.d.ts +0 -7
  76. package/lib/services/findPaymentInterfaces.js +0 -13
  77. package/lib/services/findWarehousingInterfaces.d.ts +0 -7
  78. package/lib/services/findWarehousingInterfaces.js +0 -13
  79. package/lib/services/getDeliveryDiscounts.d.ts +0 -8
  80. package/lib/services/getDeliveryDiscounts.js +0 -14
  81. package/lib/services/getPaymentDiscounts.d.ts +0 -8
  82. package/lib/services/getPaymentDiscounts.js +0 -14
  83. package/lib/services/getShopInfo.d.ts +0 -25
  84. package/lib/services/getShopInfo.js +0 -47
  85. package/lib/services/impersonateUser.d.ts +0 -8
  86. package/lib/services/impersonateUser.js +0 -20
  87. package/lib/services/loginAsGuest.d.ts +0 -10
  88. package/lib/services/loginAsGuest.js +0 -19
  89. package/lib/services/obfuscateWorkerData.d.ts +0 -4
  90. package/lib/services/obfuscateWorkerData.js +0 -4
  91. package/lib/services/orderPricingAccessors.d.ts +0 -18
  92. package/lib/services/orderPricingAccessors.js +0 -29
  93. package/lib/services/payOrder.d.ts +0 -5
  94. package/lib/services/payOrder.js +0 -25
  95. package/lib/services/registerUser.d.ts +0 -14
  96. package/lib/services/registerUser.js +0 -44
  97. package/lib/services/removeBookmark.d.ts +0 -6
  98. package/lib/services/removeBookmark.js +0 -10
  99. package/lib/services/removeCartItem.d.ts +0 -6
  100. package/lib/services/removeCartItem.js +0 -19
  101. package/lib/services/resetPasswordWithMigration.d.ts +0 -14
  102. package/lib/services/resetPasswordWithMigration.js +0 -31
  103. package/lib/services/resolveWorkType.d.ts +0 -1
  104. package/lib/services/resolveWorkType.js +0 -7
  105. package/lib/services/searchProductsWithAssortment.d.ts +0 -19
  106. package/lib/services/searchProductsWithAssortment.js +0 -17
  107. package/lib/services/toggleBookmark.d.ts +0 -8
  108. package/lib/services/toggleBookmark.js +0 -33
  109. package/lib/services/updateCart.d.ts +0 -15
  110. package/lib/services/updateCart.js +0 -27
  111. package/lib/services/updateEnrollment.d.ts +0 -13
  112. package/lib/services/updateEnrollment.js +0 -36
  113. package/lib/services/verifyEmailWithMigration.d.ts +0 -15
  114. package/lib/services/verifyEmailWithMigration.js +0 -23
@@ -28,11 +28,13 @@ const upsert = async (productVariation, unchainedAPI) => {
28
28
  };
29
29
  export default async function upsertVariations({ variations, productId }, unchainedAPI) {
30
30
  const { modules } = unchainedAPI;
31
- const upsertedProductVariationIds = await Promise.all(variations.map(async ({ content, options, ...variationsRest }) => {
31
+ const upsertedProductVariationIds = [];
32
+ for (const [index, { content, options, ...variationsRest }] of variations.entries()) {
32
33
  const variation = await upsert({
33
34
  ...variationsRest,
34
35
  options: options.map((option) => option.value),
35
36
  productId,
37
+ sortKey: index + 1,
36
38
  }, unchainedAPI);
37
39
  await Promise.all(options.map(async ({ content: optionContent, value: optionValue }) => {
38
40
  if (optionContent) {
@@ -52,8 +54,8 @@ export default async function upsertVariations({ variations, productId }, unchai
52
54
  };
53
55
  }));
54
56
  }
55
- return variation._id;
56
- }));
57
+ upsertedProductVariationIds.push(variation._id);
58
+ }
57
59
  const allVariations = await modules.products.variations.findProductVariations({
58
60
  productId,
59
61
  });
@@ -1,10 +1,10 @@
1
1
  import type { IFileAdapter, UploadedFile } from '@unchainedshop/core-files';
2
2
  export type UploadFileCallback<UnchainedAPI = unknown> = (file: UploadedFile, unchainedAPI: UnchainedAPI) => Promise<void>;
3
3
  export declare const FileDirector: {
4
- getAdapter: (key: string) => IFileAdapter<unknown> | null;
5
- getAdapters: (options?: {
6
- adapterFilter?: (adapter: IFileAdapter) => boolean;
7
- }) => IFileAdapter<unknown>[];
8
4
  registerFileUploadCallback<UnchainedAPI>(directoryName: string, fn: UploadFileCallback<UnchainedAPI>): void;
9
5
  getFileUploadCallback(directoryName: string): UploadFileCallback<unknown> | null;
6
+ getAdapters: (options?: {
7
+ adapterFilter?: ((adapter: IFileAdapter<unknown>) => boolean) | undefined;
8
+ } | undefined) => IFileAdapter<unknown>[];
9
+ getAdapter: (key: string) => IFileAdapter<unknown> | null;
10
10
  };
@@ -1,19 +1,8 @@
1
- import { BaseDirector } from '@unchainedshop/utils';
2
1
  import { FileAdapter } from '@unchainedshop/core-files';
3
- import { pluginRegistry } from "../plugins/PluginRegistry.js";
2
+ import { registryDirector } from "./registryDirector.js";
4
3
  const FileUploadRegistry = new Map();
5
- const baseDirector = BaseDirector('FileDirector');
6
4
  export const FileDirector = {
7
- ...baseDirector,
8
- getAdapter: (key) => {
9
- const adapters = pluginRegistry.getAdapters(FileAdapter.adapterType);
10
- return adapters.find((adapter) => adapter.key === key) || null;
11
- },
12
- getAdapters: (options) => {
13
- const { adapterFilter } = options || {};
14
- const adapters = pluginRegistry.getAdapters(FileAdapter.adapterType);
15
- return adapters.filter(adapterFilter || (() => true));
16
- },
5
+ ...registryDirector(FileAdapter.adapterType),
17
6
  registerFileUploadCallback(directoryName, fn) {
18
7
  if (!FileUploadRegistry.has(directoryName)) {
19
8
  FileUploadRegistry.set(directoryName, fn);
@@ -116,7 +116,7 @@ export const FilterDirector = {
116
116
  includeInactive: true,
117
117
  })
118
118
  : [];
119
- const intersectedProductIds = await filters.reduce(async (productIdSetPromise, filter) => {
119
+ const intersected = await filters.reduce(async (productIdSetPromise, filter) => {
120
120
  const productIdSet = await productIdSetPromise;
121
121
  if (!parsedFilterQuery[filter.key])
122
122
  return productIdSet;
@@ -127,7 +127,7 @@ export const FilterDirector = {
127
127
  }, unchainedAPI);
128
128
  return productIdSet.intersection(filterOptionProductIds);
129
129
  }, Promise.resolve(new Set(productIds)));
130
- return [...intersectedProductIds];
130
+ return productIds.filter((id) => intersected.has(id));
131
131
  },
132
132
  async filterFacets(filter, params, unchainedAPI) {
133
133
  const { allProductIdSet, searchQuery, forceLiveCollection, otherFilters } = params;
@@ -0,0 +1,2 @@
1
+ import type { IBaseAdapter, IBaseDirector } from '@unchainedshop/utils';
2
+ export declare const registryDirector: <Adapter extends IBaseAdapter>(adapterType: symbol) => IBaseDirector<Adapter>;
@@ -0,0 +1,6 @@
1
+ import { pluginRegistry } from "../plugins/PluginRegistry.js";
2
+ export const registryDirector = (adapterType) => ({
3
+ getAdapter: (key) => pluginRegistry.getAdapters(adapterType).find((adapter) => adapter.key === key) ||
4
+ null,
5
+ getAdapters: ({ adapterFilter } = {}) => pluginRegistry.getAdapters(adapterType).filter(adapterFilter || (() => true)),
6
+ });
@@ -2,8 +2,9 @@ import type { EnrollmentOrderPositionTemplate, EnrollmentPeriod, EnrollmentPlan
2
2
  import type { ProductPlan } from '@unchainedshop/core-products';
3
3
  import type { OrderPosition } from '@unchainedshop/core-orders';
4
4
  import { type EnrollmentContext, type IPlugin } from '../core-index.ts';
5
- export default function registerEnrollment({ adapterId, isActivatedFor, transformOrderItem, configurationForOrder, isOverdue, isValidForActivation, nextPeriod, }: {
5
+ export default function registerEnrollment({ adapterId, orderIndex, isActivatedFor, transformOrderItem, configurationForOrder, isOverdue, isValidForActivation, nextPeriod, terminationDate, transformPlanToNewPlan, }: {
6
6
  adapterId: string;
7
+ orderIndex?: number;
7
8
  isActivatedFor?: (productPlan?: ProductPlan) => boolean;
8
9
  transformOrderItem?: (orderPosition: OrderPosition, unchainedAPI: any) => Promise<EnrollmentPlan>;
9
10
  configurationForOrder: (params: {
@@ -15,4 +16,10 @@ export default function registerEnrollment({ adapterId, isActivatedFor, transfor
15
16
  isOverdue?: (context: EnrollmentContext) => Promise<boolean>;
16
17
  isValidForActivation?: (context: EnrollmentContext) => Promise<boolean>;
17
18
  nextPeriod?: (context: EnrollmentContext) => Promise<EnrollmentPeriod | null>;
19
+ terminationDate?: (context: EnrollmentContext, params: {
20
+ referenceDate: Date;
21
+ }) => Promise<Date | null>;
22
+ transformPlanToNewPlan?: (context: EnrollmentContext, params: {
23
+ plan: EnrollmentPlan;
24
+ }) => Promise<EnrollmentPlan | null>;
18
25
  }): IPlugin;
@@ -1,11 +1,12 @@
1
1
  import { EnrollmentAdapter, } from "../core-index.js";
2
2
  import { pluginRegistry } from "../plugins/PluginRegistry.js";
3
- export default function registerEnrollment({ adapterId, isActivatedFor, transformOrderItem, configurationForOrder, isOverdue, isValidForActivation, nextPeriod, }) {
3
+ export default function registerEnrollment({ adapterId, orderIndex, isActivatedFor, transformOrderItem, configurationForOrder, isOverdue, isValidForActivation, nextPeriod, terminationDate, transformPlanToNewPlan, }) {
4
4
  const adapter = {
5
5
  ...EnrollmentAdapter,
6
6
  key: `shop.unchained.enrollment.${adapterId}`,
7
7
  label: 'Enrollment: ' + adapterId,
8
8
  version: '1.0.0',
9
+ orderIndex: orderIndex ?? 0,
9
10
  isActivatedFor: (productPlan) => {
10
11
  return isActivatedFor ? isActivatedFor(productPlan) : true;
11
12
  },
@@ -15,20 +16,17 @@ export default function registerEnrollment({ adapterId, isActivatedFor, transfor
15
16
  : EnrollmentAdapter.transformOrderItemToEnrollmentPlan(orderPosition, unchainedAPI);
16
17
  },
17
18
  actions: (context) => {
19
+ const base = EnrollmentAdapter.actions(context);
18
20
  return {
19
- ...EnrollmentAdapter.actions(context),
20
- configurationForOrder: async (params) => {
21
- return configurationForOrder(params, context);
22
- },
23
- isOverdue: async () => {
24
- return isOverdue ? isOverdue(context) : false;
25
- },
26
- isValidForActivation: async () => {
27
- return isValidForActivation ? isValidForActivation(context) : false;
28
- },
29
- nextPeriod: async () => {
30
- return nextPeriod ? nextPeriod(context) : EnrollmentAdapter.actions(context).nextPeriod();
31
- },
21
+ ...base,
22
+ configurationForOrder: (params) => configurationForOrder(params, context),
23
+ isOverdue: () => (isOverdue ? isOverdue(context) : base.isOverdue()),
24
+ isValidForActivation: () => isValidForActivation ? isValidForActivation(context) : base.isValidForActivation(),
25
+ nextPeriod: () => (nextPeriod ? nextPeriod(context) : base.nextPeriod()),
26
+ terminationDate: (params) => terminationDate ? terminationDate(context, params) : base.terminationDate(params),
27
+ transformPlanToNewPlan: (params) => transformPlanToNewPlan
28
+ ? transformPlanToNewPlan(context, params)
29
+ : base.transformPlanToNewPlan(params),
32
30
  };
33
31
  },
34
32
  };
@@ -1,7 +1,8 @@
1
1
  import type { QuotationItemConfiguration, QuotationProposal } from '@unchainedshop/core-quotations';
2
2
  import { type QuotationContext, type IPlugin } from '../core-index.ts';
3
- export default function registerQuotation({ adapterId, isManualRequestVerificationRequired, isManualProposalRequired, quote, submitRequest, verifyRequest, rejectRequest, transformItemConfiguration, }: {
3
+ export default function registerQuotation({ adapterId, orderIndex, isManualRequestVerificationRequired, isManualProposalRequired, quote, submitRequest, verifyRequest, rejectRequest, transformItemConfiguration, }: {
4
4
  adapterId: string;
5
+ orderIndex?: number;
5
6
  isManualRequestVerificationRequired?: boolean;
6
7
  isManualProposalRequired?: boolean;
7
8
  quote?: (context: QuotationContext) => Promise<QuotationProposal>;
@@ -1,11 +1,12 @@
1
1
  import { QuotationAdapter, } from "../core-index.js";
2
2
  import { pluginRegistry } from "../plugins/PluginRegistry.js";
3
- export default function registerQuotation({ adapterId, isManualRequestVerificationRequired, isManualProposalRequired, quote, submitRequest, verifyRequest, rejectRequest, transformItemConfiguration, }) {
3
+ export default function registerQuotation({ adapterId, orderIndex, isManualRequestVerificationRequired, isManualProposalRequired, quote, submitRequest, verifyRequest, rejectRequest, transformItemConfiguration, }) {
4
4
  const adapter = {
5
5
  ...QuotationAdapter,
6
6
  key: `shop.unchained.quotation.${adapterId}`,
7
7
  label: 'Quotation: ' + adapterId,
8
8
  version: '1.0.0',
9
+ orderIndex: orderIndex ?? 0,
9
10
  isActivatedFor: () => {
10
11
  return true;
11
12
  },
@@ -36,7 +37,7 @@ export default function registerQuotation({ adapterId, isManualRequestVerificati
36
37
  transformItemConfiguration: async (params) => {
37
38
  return transformItemConfiguration
38
39
  ? transformItemConfiguration(params, context)
39
- : { quantity: params.quantity, configuration: params.configuration };
40
+ : QuotationAdapter.actions(context).transformItemConfiguration(params);
40
41
  },
41
42
  };
42
43
  },
@@ -35,8 +35,9 @@ declare class PluginRegistry {
35
35
  getPlugin(key: string): IPlugin | undefined;
36
36
  getAllPlugins(): IPlugin[];
37
37
  getAdapters(adapterType: symbol): IBaseAdapter[];
38
+ getAllAdapters(): IBaseAdapter[];
38
39
  hasPlugin(key: string): boolean;
39
- clear(): void;
40
+ clear(): () => void;
40
41
  }
41
42
  export declare const pluginRegistry: PluginRegistry;
42
43
  export {};
@@ -5,9 +5,11 @@ class PluginRegistry {
5
5
  moduleFactories = [];
6
6
  skippedPlugins = new Set();
7
7
  register(plugin) {
8
- if (this.plugins.has(plugin.key)) {
8
+ const registered = this.plugins.get(plugin.key);
9
+ if (registered) {
9
10
  logger.warn(`Plugin ${plugin.key} already registered, skipping`, {
10
11
  label: plugin.label,
12
+ registeredLabel: registered.label,
11
13
  });
12
14
  return;
13
15
  }
@@ -103,13 +105,24 @@ class PluginRegistry {
103
105
  }
104
106
  return result.toSorted((left, right) => (left.orderIndex ?? 0) - (right.orderIndex ?? 0));
105
107
  }
108
+ getAllAdapters() {
109
+ return this.getAllPlugins()
110
+ .filter((plugin) => !this.skippedPlugins.has(plugin.key))
111
+ .flatMap((plugin) => plugin.adapters ?? []);
112
+ }
106
113
  hasPlugin(key) {
107
114
  return this.plugins.has(key);
108
115
  }
109
116
  clear() {
110
- this.plugins.clear();
117
+ const { plugins, moduleFactories, skippedPlugins } = this;
118
+ this.plugins = new Map();
111
119
  this.moduleFactories = [];
112
- this.skippedPlugins.clear();
120
+ this.skippedPlugins = new Set();
121
+ return () => {
122
+ this.plugins = plugins;
123
+ this.moduleFactories = moduleFactories;
124
+ this.skippedPlugins = skippedPlugins;
125
+ };
113
126
  }
114
127
  }
115
128
  export const pluginRegistry = new PluginRegistry();
@@ -1,16 +1,12 @@
1
- import type { OrderPosition } from '@unchainedshop/core-orders';
2
- import type { User } from '@unchainedshop/core-users';
1
+ import { type Order, type OrderPosition } from '@unchainedshop/core-orders';
2
+ import { type Quotation } from '@unchainedshop/core-quotations';
3
3
  import type { Modules } from '../modules.ts';
4
- export interface AddCartQuotationParams {
5
- orderId?: string;
6
- quotationId: string;
7
- quantity: number;
8
- configuration: {
4
+ export declare function addCartQuotationService(this: Modules, { order, quotation, quantity, configuration, }: {
5
+ order: Order;
6
+ quotation: Quotation;
7
+ quantity?: number | null;
8
+ configuration?: {
9
9
  key: string;
10
10
  value: string;
11
- }[];
12
- user: User;
13
- countryCode: string;
14
- context: unknown;
15
- }
16
- export declare function addCartQuotationService(this: Modules, params: AddCartQuotationParams): Promise<OrderPosition>;
11
+ }[] | null;
12
+ }, unchainedAPI?: Record<string, any>): Promise<OrderPosition>;
@@ -1,52 +1,57 @@
1
+ import { ordersSettings } from '@unchainedshop/core-orders';
1
2
  import { QuotationStatus } from '@unchainedshop/core-quotations';
2
3
  import { QuotationDirector } from "../directors/QuotationDirector.js";
3
- import { updateCalculationService } from "./updateCalculation.js";
4
- import { findOrInitCartService } from "./findOrInitCart.js";
5
- import { OrderNotFoundError, OrderWrongStatusError, OrderQuantityTooLowError, ProductNotFoundError, QuotationNotFoundError, QuotationWrongStatusError, QuotationItemConfigurationError, } from "../errors/index.js";
6
- export async function addCartQuotationService(params) {
7
- const { orderId, quotationId, quantity, configuration, user, countryCode, context } = params;
8
- if (quantity < 1) {
9
- throw new OrderQuantityTooLowError({ quantity });
10
- }
11
- const quotation = await this.quotations.findQuotation({ quotationId });
12
- if (!quotation) {
13
- throw new QuotationNotFoundError({ quotationId });
14
- }
4
+ import { createServiceError } from "../errors.js";
5
+ import { isQuotationReserved } from "./reserveOrderQuotationsForCheckout.js";
6
+ export async function addCartQuotationService({ order, quotation, quantity, configuration, }, unchainedAPI = {}) {
7
+ const context = { ...unchainedAPI, modules: this };
15
8
  if (quotation.status !== QuotationStatus.PROPOSED) {
16
- throw new QuotationWrongStatusError({ status: quotation.status });
9
+ throw createServiceError('QuotationWrongStatusError', `Quotation ${quotation._id} is ${quotation.status}, not PROPOSED`);
17
10
  }
18
- const order = await findOrInitCartService.bind(this)({
19
- orderId,
20
- user,
21
- countryCode,
22
- });
23
- if (!order) {
24
- throw new OrderNotFoundError({ orderId: orderId || null });
11
+ if (quotation.currencyCode !== order.currencyCode) {
12
+ throw createServiceError('QuotationInvalidError', `Quotation ${quotation._id} is not in the currency of order ${order._id}`);
25
13
  }
26
- if (!this.orders.isCart(order)) {
27
- throw new OrderWrongStatusError({ status: order.status });
14
+ if (await isQuotationReserved.call(this, quotation._id, { exceptOrderId: order._id })) {
15
+ throw createServiceError('QuotationInvalidError', `Quotation ${quotation._id} is reserved by another order`);
28
16
  }
29
- if (!(await this.products.productExists({ productId: quotation.productId }))) {
30
- throw new ProductNotFoundError({ productId: quotation.productId });
17
+ const product = await this.products.findProduct({ productId: quotation.productId });
18
+ if (!product) {
19
+ throw createServiceError('ProductNotFoundError', `Product not found: ${quotation.productId}`);
31
20
  }
21
+ const existingPosition = (await this.orders.positions.findOrderPositions({ orderId: order._id })).find((position) => position.quotationId === quotation._id);
22
+ const existingQuantity = existingPosition?.quantity || 0;
23
+ const requestedQuantity = existingQuantity + (quantity ?? quotation.quantity ?? 1);
32
24
  const director = await QuotationDirector.actions({ quotation }, context);
33
25
  const quotationConfiguration = await director.transformItemConfiguration({
34
- quantity,
35
- configuration,
26
+ quantity: requestedQuantity,
27
+ configuration: configuration ?? existingPosition?.configuration ?? [],
36
28
  });
37
29
  if (!quotationConfiguration) {
38
- throw new QuotationItemConfigurationError({ configuration });
30
+ throw createServiceError('QuotationItemConfigurationError', `Quotation ${quotation._id} does not take quantity ${requestedQuantity}`);
39
31
  }
40
- const updatedOrderPosition = await this.orders.positions.addProductItem({
41
- quantity: quotationConfiguration.quantity || 1,
32
+ const lineQuantity = quotationConfiguration.quantity || requestedQuantity;
33
+ await ordersSettings.validateOrderPosition({
34
+ order,
35
+ product,
42
36
  configuration: quotationConfiguration.configuration,
43
- quotationId,
44
- productId: quotation.productId,
45
- originalProductId: quotation.productId,
46
- orderId: order._id,
47
- });
48
- await updateCalculationService.bind(this)(order._id);
49
- return (await this.orders.positions.findOrderPosition({
50
- itemId: updatedOrderPosition._id,
51
- }));
37
+ quantityDiff: lineQuantity - existingQuantity,
38
+ }, context);
39
+ const position = existingPosition
40
+ ? await this.orders.positions.updateProductItem({
41
+ orderPositionId: existingPosition._id,
42
+ quantity: lineQuantity,
43
+ configuration: quotationConfiguration.configuration ?? null,
44
+ })
45
+ : await this.orders.positions.addProductItem({
46
+ quantity: lineQuantity,
47
+ configuration: quotationConfiguration.configuration,
48
+ quotationId: quotation._id,
49
+ productId: quotation.productId,
50
+ originalProductId: quotation.productId,
51
+ orderId: order._id,
52
+ });
53
+ if (!position) {
54
+ throw createServiceError('OrderItemNotFoundError', `Order item not found: ${existingPosition?._id}`);
55
+ }
56
+ return position;
52
57
  }
@@ -0,0 +1,5 @@
1
+ import type { Order } from '@unchainedshop/core-orders';
2
+ import type { Modules } from '../modules.ts';
3
+ export declare function reserveOrderDiscountsForCheckout(this: Modules, order: Order): Promise<{
4
+ release: () => Promise<void>;
5
+ }>;
@@ -0,0 +1,34 @@
1
+ import { createLogger } from '@unchainedshop/logger';
2
+ import { OrderDiscountDirector } from "../directors/OrderDiscountDirector.js";
3
+ const logger = createLogger('unchained:core:checkoutOrder');
4
+ export async function reserveOrderDiscountsForCheckout(order) {
5
+ const reservations = [];
6
+ const release = async () => {
7
+ const results = await Promise.allSettled(reservations.map((reservation) => reservation.release()));
8
+ for (const result of results) {
9
+ if (result.status === 'rejected') {
10
+ logger.error(`Could not release a discount reservation of order ${order._id}`, {
11
+ error: result.reason,
12
+ });
13
+ }
14
+ }
15
+ };
16
+ try {
17
+ const discounts = await this.orders.discounts.findOrderDiscounts({ orderId: order._id });
18
+ for (const orderDiscount of discounts) {
19
+ const Adapter = OrderDiscountDirector.getAdapter(orderDiscount.discountKey);
20
+ if (!Adapter)
21
+ continue;
22
+ const adapter = await Adapter.actions({
23
+ context: { order, orderDiscount, code: orderDiscount.code, modules: this },
24
+ });
25
+ if (adapter.reserveForCheckout)
26
+ reservations.push(await adapter.reserveForCheckout());
27
+ }
28
+ return { release };
29
+ }
30
+ catch (error) {
31
+ await release();
32
+ throw error;
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ import { type Order } from '@unchainedshop/core-orders';
2
+ import type { Modules } from '../modules.ts';
3
+ export declare function isQuotationReserved(this: Modules, quotationId: string, { exceptOrderId }?: {
4
+ exceptOrderId?: string;
5
+ }): Promise<boolean>;
6
+ export declare function reserveOrderQuotationsForCheckout(this: Modules, order: Order): Promise<{
7
+ release: () => Promise<void>;
8
+ }>;
@@ -0,0 +1,49 @@
1
+ import { OrderStatus } from '@unchainedshop/core-orders';
2
+ import { createServiceError } from "../errors.js";
3
+ const RESERVATION_LOCK_TTL = 60000;
4
+ export async function isQuotationReserved(quotationId, { exceptOrderId } = {}) {
5
+ const orderIds = (await this.orders.positions.findOrderPositions({ quotationId }))
6
+ .map(({ orderId }) => orderId)
7
+ .filter((orderId) => orderId !== exceptOrderId);
8
+ if (!orderIds.length)
9
+ return false;
10
+ const count = await this.orders.count({
11
+ orderIds,
12
+ status: [OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.FULFILLED],
13
+ });
14
+ return count > 0;
15
+ }
16
+ export async function reserveOrderQuotationsForCheckout(order) {
17
+ const locks = [];
18
+ const release = async () => {
19
+ await Promise.all(locks.map((lock) => lock.release()));
20
+ };
21
+ try {
22
+ const positions = await this.orders.positions.findOrderPositions({ orderId: order._id });
23
+ const quotationIds = [
24
+ ...new Set(positions.map(({ quotationId }) => quotationId).filter(Boolean)),
25
+ ].sort();
26
+ for (const quotationId of quotationIds) {
27
+ try {
28
+ locks.push(await this.orders.acquireLock(quotationId, 'quotation', RESERVATION_LOCK_TTL));
29
+ }
30
+ catch (error) {
31
+ if (!error?.message?.startsWith('Could not acquire lock'))
32
+ throw error;
33
+ throw createServiceError('QuotationInvalidError', 'Quotation is being ordered by another order');
34
+ }
35
+ const quotation = await this.quotations.findQuotation({ quotationId });
36
+ if (!quotation || !this.quotations.isProposalValid(quotation)) {
37
+ throw createServiceError('QuotationInvalidError', 'Quotation expired, fulfilled or not valid for this order, please request a new offer');
38
+ }
39
+ if (await isQuotationReserved.call(this, quotationId, { exceptOrderId: order._id })) {
40
+ throw createServiceError('QuotationInvalidError', 'Quotation is reserved by another order');
41
+ }
42
+ }
43
+ return { release };
44
+ }
45
+ catch (error) {
46
+ await release();
47
+ throw error;
48
+ }
49
+ }
@@ -3,10 +3,12 @@ import type { Product } from '@unchainedshop/core-products';
3
3
  import { type EstimatedDispatch } from '../directors/WarehousingDirector.ts';
4
4
  import type { WarehousingContext } from '../directors/WarehousingAdapter.ts';
5
5
  import type { WarehousingProvider } from '@unchainedshop/core-warehousing';
6
- export declare function simulateProductDispatchingService(this: Modules, { product, quantity, referenceDate, }: {
6
+ import type { DeliveryProviderType } from '@unchainedshop/core-delivery';
7
+ export declare function simulateProductDispatchingService(this: Modules, { product, quantity, referenceDate, deliveryProviderType, }: {
7
8
  product: Product;
8
9
  quantity: number;
9
10
  referenceDate?: Date;
11
+ deliveryProviderType?: DeliveryProviderType | null;
10
12
  }): Promise<(WarehousingContext & EstimatedDispatch & {
11
13
  warehousingProvider: WarehousingProvider;
12
14
  })[]>;
@@ -1,7 +1,7 @@
1
1
  import { supportedWarehousingProvidersService } from "./supportedWarehousingProviders.js";
2
2
  import { WarehousingDirector } from "../directors/WarehousingDirector.js";
3
- export async function simulateProductDispatchingService({ product, quantity, referenceDate, }) {
4
- const deliveryProviders = await this.delivery.allProviders();
3
+ export async function simulateProductDispatchingService({ product, quantity, referenceDate, deliveryProviderType, }) {
4
+ const deliveryProviders = (await this.delivery.allProviders()).filter(({ type }) => !deliveryProviderType || type === deliveryProviderType);
5
5
  return deliveryProviders.reduce(async (oldResult, deliveryProvider) => {
6
6
  const result = await oldResult;
7
7
  const warehousingProviders = await supportedWarehousingProvidersService.bind(this)({
@@ -25,6 +25,6 @@ export async function simulateProductDispatchingService({ product, quantity, ref
25
25
  ...dispatch,
26
26
  };
27
27
  }));
28
- return result.concat(result, mappedWarehousingProviders);
28
+ return result.concat(mappedWarehousingProviders);
29
29
  }, Promise.resolve([]));
30
30
  }
@@ -2,9 +2,11 @@ import type { Modules } from '../modules.ts';
2
2
  import type { Product } from '@unchainedshop/core-products';
3
3
  import type { WarehousingContext } from '../directors/WarehousingAdapter.ts';
4
4
  import type { WarehousingProvider } from '@unchainedshop/core-warehousing';
5
- export declare function simulateProductInventoryService(this: Modules, { product, referenceDate, }: {
5
+ import type { DeliveryProviderType } from '@unchainedshop/core-delivery';
6
+ export declare function simulateProductInventoryService(this: Modules, { product, referenceDate, deliveryProviderType, }: {
6
7
  product: Product;
7
8
  referenceDate?: Date;
9
+ deliveryProviderType?: DeliveryProviderType | null;
8
10
  }): Promise<(WarehousingContext & {
9
11
  quantity?: number;
10
12
  } & {
@@ -1,7 +1,7 @@
1
1
  import { supportedWarehousingProvidersService } from "./supportedWarehousingProviders.js";
2
2
  import { WarehousingDirector } from "../directors/WarehousingDirector.js";
3
- export async function simulateProductInventoryService({ product, referenceDate, }) {
4
- const deliveryProviders = await this.delivery.allProviders();
3
+ export async function simulateProductInventoryService({ product, referenceDate, deliveryProviderType, }) {
4
+ const deliveryProviders = (await this.delivery.allProviders()).filter(({ type }) => !deliveryProviderType || type === deliveryProviderType);
5
5
  return deliveryProviders.reduce(async (oldResult, deliveryProvider) => {
6
6
  const result = await oldResult;
7
7
  const warehousingProviders = await supportedWarehousingProvidersService.bind(this)({
@@ -24,6 +24,6 @@ export async function simulateProductInventoryService({ product, referenceDate,
24
24
  ...stock,
25
25
  };
26
26
  }));
27
- return result.concat(result, mappedWarehousingProviders);
27
+ return result.concat(mappedWarehousingProviders);
28
28
  }, Promise.resolve([]));
29
29
  }
@@ -1,8 +1,17 @@
1
1
  import { deliverySettings } from '@unchainedshop/core-delivery';
2
+ import { createLogger } from '@unchainedshop/logger';
2
3
  import { DeliveryDirector } from "../directors/index.js";
4
+ const logger = createLogger('unchained:core');
3
5
  export async function supportedDeliveryProvidersService(params) {
4
6
  const allProviders = await this.delivery.allProviders();
5
7
  const providers = (await Promise.all(allProviders.map(async (provider) => {
8
+ if (!DeliveryDirector.getAdapter(provider.adapterKey)) {
9
+ logger.warn('Delivery provider skipped, its adapter is not registered', {
10
+ deliveryProviderId: provider._id,
11
+ adapterKey: provider.adapterKey,
12
+ });
13
+ return [];
14
+ }
6
15
  const adapter = await DeliveryDirector.actions(provider, params, { modules: this });
7
16
  return adapter.isActive() ? [provider] : [];
8
17
  }))).flat();
@@ -1,8 +1,17 @@
1
1
  import { paymentSettings } from '@unchainedshop/core-payment';
2
+ import { createLogger } from '@unchainedshop/logger';
2
3
  import { PaymentDirector } from "../directors/PaymentDirector.js";
4
+ const logger = createLogger('unchained:core');
3
5
  export async function supportedPaymentProvidersService(params) {
4
6
  const allProviders = await this.payment.paymentProviders.allProviders();
5
7
  const providers = (await Promise.all(allProviders.map(async (provider) => {
8
+ if (!PaymentDirector.getAdapter(provider.adapterKey)) {
9
+ logger.warn('Payment provider skipped, its adapter is not registered', {
10
+ paymentProviderId: provider._id,
11
+ adapterKey: provider.adapterKey,
12
+ });
13
+ return [];
14
+ }
6
15
  const adapter = await PaymentDirector.actions(provider, params, { modules: this });
7
16
  return adapter.isActive() ? [provider] : [];
8
17
  }))).flat();
@@ -1,7 +1,16 @@
1
+ import { createLogger } from '@unchainedshop/logger';
1
2
  import { WarehousingDirector } from "../directors/index.js";
3
+ const logger = createLogger('unchained:core');
2
4
  export async function supportedWarehousingProvidersService(params) {
3
5
  const allProviders = await this.warehousing.allProviders();
4
6
  const providers = (await Promise.all(allProviders.map(async (provider) => {
7
+ if (!WarehousingDirector.getAdapter(provider.adapterKey)) {
8
+ logger.warn('Warehousing provider skipped, its adapter is not registered', {
9
+ warehousingProviderId: provider._id,
10
+ adapterKey: provider.adapterKey,
11
+ });
12
+ return [];
13
+ }
5
14
  const adapter = await WarehousingDirector.actions(provider, params, { modules: this });
6
15
  return adapter.isActive() ? [provider] : [];
7
16
  }))).flat();
@@ -0,0 +1,5 @@
1
+ import { type Enrollment } from '@unchainedshop/core-enrollments';
2
+ import type { Modules } from '../modules.ts';
3
+ export declare function suspendEnrollmentService(this: Modules, enrollment: Enrollment, { resumeAt }?: {
4
+ resumeAt?: Date;
5
+ }): Promise<Enrollment>;
@@ -0,0 +1,21 @@
1
+ import { EnrollmentStatus } from '@unchainedshop/core-enrollments';
2
+ import { addMessageService } from "./addMessage.js";
3
+ export async function suspendEnrollmentService(enrollment, { resumeAt } = {}) {
4
+ if (enrollment.status === EnrollmentStatus.TERMINATED)
5
+ return enrollment;
6
+ let updatedEnrollment = (await this.enrollments.updateStatus(enrollment._id, {
7
+ status: EnrollmentStatus.SUSPENDED,
8
+ info: 'suspended manually',
9
+ }));
10
+ if (resumeAt) {
11
+ updatedEnrollment = (await this.enrollments.updateResumeAt(enrollment._id, resumeAt));
12
+ }
13
+ const user = await this.users.findUserById(enrollment.userId);
14
+ const locale = this.users.userLocale(user);
15
+ await addMessageService.bind(this)('ENROLLMENT_STATUS', {
16
+ reason: 'status_change',
17
+ locale: locale.baseName,
18
+ enrollmentId: updatedEnrollment._id,
19
+ });
20
+ return updatedEnrollment;
21
+ }