@unchainedshop/plugins 4.8.18 → 4.8.20

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.
@@ -87,7 +87,7 @@ export const gridfsRouteHandler = async (request, context) => {
87
87
  const signature = url.searchParams.get('s');
88
88
  const expiryTimestamp = url.searchParams.get('e');
89
89
  const fileDocument = await modules.files.findFile({
90
- url: `${GRIDFS_PUT_SERVER_PATH}/${directoryName}/${fileName}`,
90
+ url: `${GRIDFS_PUT_SERVER_PATH}/${directoryName}/${encodeURIComponent(fileName)}`,
91
91
  });
92
92
  if (!fileDocument) {
93
93
  logger.error('File not found', { fileName });
@@ -2,6 +2,7 @@ import { createLogger } from '@unchainedshop/logger';
2
2
  import createDatatransAPI from "./api/index.js";
3
3
  import parseRegistrationData from "./parseRegistrationData.js";
4
4
  import roundedAmountFromOrder from "./roundedAmountFromOrder.js";
5
+ import build3DSCardholder, { withCardCardholder, withSecureFieldsCardholder, } from "./build3DSCardholder.js";
5
6
  import { PaymentAdapter, PaymentError, PaymentPricingRowCategory, PaymentPricingSheet, OrderPricingSheet, } from '@unchainedshop/core';
6
7
  const logger = createLogger('unchained:datatrans');
7
8
  const { DATATRANS_SECRET, DATATRANS_SIGN_KEY, DATATRANS_API_ENDPOINT = 'https://api.sandbox.datatrans.com', DATATRANS_MERCHANT_ID, } = process.env;
@@ -221,22 +222,18 @@ export const Datatrans = {
221
222
  const price = order
222
223
  ? roundedAmountFromOrder(order)
223
224
  : {};
225
+ const cardholder = build3DSCardholder(order);
224
226
  if (useSecureFields) {
225
227
  const result = await api().secureFields({
226
- ...arbitraryFields,
228
+ ...withSecureFieldsCardholder(arbitraryFields, cardholder),
227
229
  currency: price.currencyCode || 'CHF',
228
- refno,
229
- refno2,
230
- customer: {
231
- id: userId,
232
- },
233
230
  amount: price.amount,
234
231
  });
235
232
  throwIfResponseError(result);
236
233
  return JSON.stringify(result);
237
234
  }
238
235
  const result = await api().init({
239
- ...arbitraryFields,
236
+ ...withCardCardholder(arbitraryFields, cardholder),
240
237
  currency: price.currencyCode || 'CHF',
241
238
  refno,
242
239
  refno2,
@@ -349,11 +346,17 @@ export const Datatrans = {
349
346
  if (status === 'authenticated') {
350
347
  if (authorizeAuthenticated) {
351
348
  checkIfTransactionAmountValid(transactionId, transaction);
349
+ const { orderPayment, order } = context;
350
+ const userId = order?.userId || context?.userId;
351
+ const refno = orderPayment
352
+ ? Buffer.from(orderPayment._id, 'hex').toString('base64')
353
+ : transaction.refno;
354
+ const refno2 = userId || transaction.refno2;
352
355
  await authorizeAuth({
353
356
  ...(authorizeAuthenticated || {}),
354
357
  transactionId,
355
- refno: transaction.refno,
356
- refno2: transaction.refno2,
358
+ refno,
359
+ refno2,
357
360
  });
358
361
  status = 'authorized';
359
362
  }
@@ -10,7 +10,7 @@ else {
10
10
  try {
11
11
  const { default: Stripe } = await import('stripe');
12
12
  stripe = new Stripe(STRIPE_SECRET, {
13
- apiVersion: '2026-06-24.dahlia',
13
+ apiVersion: '2026-07-29.dahlia',
14
14
  });
15
15
  }
16
16
  catch {
@@ -0,0 +1,2 @@
1
+ import { type IProductPricingAdapter } from '@unchainedshop/core';
2
+ export declare const ProductQuotationPrice: IProductPricingAdapter;
@@ -0,0 +1,40 @@
1
+ import { ProductPricingDirector, ProductPricingAdapter, } from '@unchainedshop/core';
2
+ export const ProductQuotationPrice = {
3
+ ...ProductPricingAdapter,
4
+ key: 'shop.unchained.pricing.quotation-price',
5
+ version: '1.0.0',
6
+ label: 'Apply negotiated quotation price',
7
+ orderIndex: 20,
8
+ isActivatedFor: (context) => {
9
+ return Boolean(context.quotationId);
10
+ },
11
+ actions: (params) => {
12
+ const pricingAdapter = ProductPricingAdapter.actions(params);
13
+ return {
14
+ ...pricingAdapter,
15
+ calculate: async () => {
16
+ const { quotationId, currencyCode, quantity, modules } = params.context;
17
+ const quotation = await modules.quotations.findQuotation({ quotationId: quotationId });
18
+ const isApplicable = quotation &&
19
+ quotation.price != null &&
20
+ quotation.currencyCode === currencyCode &&
21
+ (modules.quotations.isProposalValid(quotation) ||
22
+ modules.quotations.normalizedStatus(quotation) === 'FULFILLED');
23
+ if (isApplicable) {
24
+ const negotiatedTotal = Math.round(quotation.price) * quantity;
25
+ const correction = negotiatedTotal - params.calculationSheet.sum();
26
+ if (correction !== 0) {
27
+ pricingAdapter.resultSheet().addItem({
28
+ amount: correction,
29
+ isTaxable: false,
30
+ isNetPrice: false,
31
+ meta: { adapter: ProductQuotationPrice.key, quotationId: quotation._id },
32
+ });
33
+ }
34
+ }
35
+ return pricingAdapter.calculate();
36
+ },
37
+ };
38
+ },
39
+ };
40
+ ProductPricingDirector.registerAdapter(ProductQuotationPrice);
@@ -10,8 +10,13 @@ export const ManualOffering = {
10
10
  return {
11
11
  ...QuotationAdapter.actions(params),
12
12
  quote: async () => {
13
+ const { quotation } = params;
14
+ const price = Number(quotation?.context?.price);
13
15
  return {
14
- expires: new Date(new Date().getTime() + 3600 * 1000),
16
+ expires: quotation?.context?.expires
17
+ ? new Date(quotation.context.expires)
18
+ : new Date(new Date().getTime() + 3600 * 1000),
19
+ price: Number.isFinite(price) && price > 0 ? Math.round(price) : undefined,
15
20
  };
16
21
  },
17
22
  };
@@ -10,8 +10,13 @@ const ManualOffering = {
10
10
  return {
11
11
  ...QuotationAdapter.actions(params),
12
12
  quote: async () => {
13
+ const { quotation } = params;
14
+ const price = Number(quotation?.context?.price);
13
15
  return {
14
- expires: new Date(new Date().getTime() + 3600 * 1000),
16
+ expires: quotation?.context?.expires
17
+ ? new Date(quotation.context.expires)
18
+ : new Date(new Date().getTime() + 3600 * 1000),
19
+ price: Number.isFinite(price) && price > 0 ? Math.round(price) : undefined,
15
20
  };
16
21
  },
17
22
  };
@@ -0,0 +1,9 @@
1
+ import { type IWorkerAdapter } from '@unchainedshop/core';
2
+ export declare const GCGuestsWorker: IWorkerAdapter<{
3
+ guestUserMaxAgeInDays?: number;
4
+ }, {
5
+ scannedGuestCount: number;
6
+ deletedGuestCount: number;
7
+ }>;
8
+ export default GCGuestsWorker;
9
+ export declare const configureGCGuestsAutoscheduling: () => void;
@@ -0,0 +1,56 @@
1
+ import { WorkerAdapter, WorkerDirector, schedule } from '@unchainedshop/core';
2
+ import { userSettings } from '@unchainedshop/core-users';
3
+ import { createLogger } from '@unchainedshop/logger';
4
+ const logger = createLogger('unchained:worker:gc-guests');
5
+ const ONE_DAY_IN_MILLISECONDS = 86400000;
6
+ const everyDayAtHalfPastTwo = schedule.parse.cron('30 2 * * *');
7
+ export const GCGuestsWorker = {
8
+ ...WorkerAdapter,
9
+ key: 'shop.unchained.worker-plugin.gc-guests',
10
+ label: 'Garbage Collect Guests',
11
+ version: '1.0.0',
12
+ type: 'GC_GUESTS',
13
+ doWork: async ({ guestUserMaxAgeInDays } = {}, unchainedAPI) => {
14
+ const { modules, services } = unchainedAPI;
15
+ try {
16
+ const maxAgeInDays = guestUserMaxAgeInDays ?? userSettings.guestUserMaxAgeInDays;
17
+ const before = new Date(Date.now() - maxAgeInDays * ONE_DAY_IN_MILLISECONDS);
18
+ const userIdsToDelete = await modules.users.findGuestUserIds({ before });
19
+ let deletedGuestCount = 0;
20
+ await Array.fromAsync(userIdsToDelete, async (userId) => {
21
+ try {
22
+ await services.users.deleteUser({ userId });
23
+ deletedGuestCount += 1;
24
+ }
25
+ catch (err) {
26
+ logger.warn(`Failed to garbage-collect guest ${userId}: ${err.message}`);
27
+ }
28
+ });
29
+ return {
30
+ success: true,
31
+ result: {
32
+ scannedGuestCount: userIdsToDelete.length,
33
+ deletedGuestCount,
34
+ },
35
+ };
36
+ }
37
+ catch (err) {
38
+ return {
39
+ success: false,
40
+ error: {
41
+ name: err.name,
42
+ message: err.message,
43
+ stack: err.stack,
44
+ },
45
+ };
46
+ }
47
+ },
48
+ };
49
+ export default GCGuestsWorker;
50
+ export const configureGCGuestsAutoscheduling = () => {
51
+ WorkerDirector.configureAutoscheduling({
52
+ type: GCGuestsWorker.type,
53
+ schedule: everyDayAtHalfPastTwo,
54
+ retries: 0,
55
+ });
56
+ };
@@ -0,0 +1,4 @@
1
+ import { type IPlugin } from '@unchainedshop/core';
2
+ export declare const GCGuestsPlugin: IPlugin;
3
+ export default GCGuestsPlugin;
4
+ export * from './adapter.ts';
@@ -0,0 +1,13 @@
1
+ import {} from '@unchainedshop/core';
2
+ import { GCGuestsWorker, configureGCGuestsAutoscheduling } from "./adapter.js";
3
+ export const GCGuestsPlugin = {
4
+ key: 'shop.unchained.worker-plugin.gc-guests',
5
+ label: 'Garbage Collect Guests Worker Plugin',
6
+ version: '1.0.0',
7
+ adapters: [GCGuestsWorker],
8
+ onRegister: () => {
9
+ configureGCGuestsAutoscheduling();
10
+ },
11
+ };
12
+ export default GCGuestsPlugin;
13
+ export * from "./adapter.js";
@@ -0,0 +1,9 @@
1
+ import { type IWorkerAdapter } from '@unchainedshop/core';
2
+ export declare const InvalidateCartsWorker: IWorkerAdapter<{
3
+ maxAgeDays?: number;
4
+ }, {
5
+ scannedCartCount: number;
6
+ recalculatedCartCount: number;
7
+ }>;
8
+ export default InvalidateCartsWorker;
9
+ export declare const configureInvalidateCartsAutoscheduling: () => void;
@@ -0,0 +1,52 @@
1
+ import { WorkerAdapter, WorkerDirector, schedule } from '@unchainedshop/core';
2
+ import { createLogger } from '@unchainedshop/logger';
3
+ const logger = createLogger('unchained:worker:invalidate-carts');
4
+ const firstOfMonthAtLocalMidnight = schedule.parse.cron('0 0 1 * *');
5
+ export const InvalidateCartsWorker = {
6
+ ...WorkerAdapter,
7
+ key: 'shop.unchained.worker-plugin.invalidate-carts',
8
+ label: 'Invalidate Carts',
9
+ version: '1.0.0',
10
+ type: 'INVALIDATE_CARTS',
11
+ doWork: async ({ maxAgeDays } = {}, unchainedAPI) => {
12
+ const { modules, services } = unchainedAPI;
13
+ try {
14
+ const orderIds = await modules.orders.findCartIdsToInvalidate(maxAgeDays);
15
+ let recalculatedCartCount = 0;
16
+ for (const orderId of orderIds) {
17
+ try {
18
+ await services.orders.updateCalculation(orderId);
19
+ recalculatedCartCount += 1;
20
+ }
21
+ catch (err) {
22
+ logger.warn(`Failed to recalculate cart ${orderId}: ${err.message}`);
23
+ }
24
+ }
25
+ return {
26
+ success: true,
27
+ result: {
28
+ scannedCartCount: orderIds.length,
29
+ recalculatedCartCount,
30
+ },
31
+ };
32
+ }
33
+ catch (err) {
34
+ return {
35
+ success: false,
36
+ error: {
37
+ name: err.name,
38
+ message: err.message,
39
+ stack: err.stack,
40
+ },
41
+ };
42
+ }
43
+ },
44
+ };
45
+ export default InvalidateCartsWorker;
46
+ export const configureInvalidateCartsAutoscheduling = () => {
47
+ WorkerDirector.configureAutoscheduling({
48
+ type: InvalidateCartsWorker.type,
49
+ schedule: firstOfMonthAtLocalMidnight,
50
+ retries: 0,
51
+ });
52
+ };
@@ -0,0 +1,4 @@
1
+ import { type IPlugin } from '@unchainedshop/core';
2
+ export declare const InvalidateCartsPlugin: IPlugin;
3
+ export default InvalidateCartsPlugin;
4
+ export * from './adapter.ts';
@@ -0,0 +1,13 @@
1
+ import {} from '@unchainedshop/core';
2
+ import { InvalidateCartsWorker, configureInvalidateCartsAutoscheduling } from "./adapter.js";
3
+ export const InvalidateCartsPlugin = {
4
+ key: 'shop.unchained.worker-plugin.invalidate-carts',
5
+ label: 'Invalidate Carts Worker Plugin',
6
+ version: '1.0.0',
7
+ adapters: [InvalidateCartsWorker],
8
+ onRegister: () => {
9
+ configureInvalidateCartsAutoscheduling();
10
+ },
11
+ };
12
+ export default InvalidateCartsPlugin;
13
+ export * from "./adapter.js";
@@ -9,5 +9,7 @@ export declare const ZombieKillerWorker: IWorkerAdapter<{
9
9
  deletedProductTextsCount: number;
10
10
  deletedProductVariationsCount: number;
11
11
  deletedAssortmentTextsCount: number;
12
+ deletedCartsCount: number;
12
13
  }>;
13
14
  export default ZombieKillerWorker;
15
+ export declare const configureZombieKillerAutoscheduling: () => void;
@@ -1,12 +1,14 @@
1
- import { WorkerAdapter } from '@unchainedshop/core';
1
+ import { WorkerAdapter, WorkerDirector, schedule } from '@unchainedshop/core';
2
+ const everyDayAtTwo = schedule.parse.cron('0 2 * * *');
2
3
  export const ZombieKillerWorker = {
3
4
  ...WorkerAdapter,
4
5
  key: 'shop.unchained.worker-plugin.zombie-killer',
5
6
  label: 'Zombie Killer',
6
7
  version: '1.0.0',
7
8
  type: 'ZOMBIE_KILLER',
8
- doWork: async ({ bulkImportMaxAgeInDays } = { bulkImportMaxAgeInDays: 5 }, unchainedAPI) => {
9
+ doWork: async (input, unchainedAPI) => {
9
10
  const { modules, services } = unchainedAPI;
11
+ const { bulkImportMaxAgeInDays = 5 } = input || {};
10
12
  try {
11
13
  const error = false;
12
14
  const filters = await modules.filters.findFilters({ includeInactive: true }, { projection: { _id: 1 } });
@@ -38,7 +40,9 @@ export const ZombieKillerWorker = {
38
40
  const productMedia = await modules.products.media.findProductMedias({}, { projection: { mediaId: 1 } });
39
41
  const assortmentMedia = await modules.assortments.media.findAssortmentMedias({}, { projection: { mediaId: 1 } });
40
42
  const allFileIdsLinked = [...productMedia, ...assortmentMedia].map((l) => l?.mediaId);
41
- const allFileIdsRelevant = (await modules.files.findFiles({ paths: ['product-media', 'assortment-media'] }, { projection: { _id: 1 } })).map((a) => a._id);
43
+ const allFileIdsRelevant = (await modules.files.findFiles({ paths: ['product-media', 'assortment-media'] }, { projection: { _id: 1, expires: 1 } }))
44
+ .filter((file) => !file.expires)
45
+ .map((a) => a._id);
42
46
  const fileIdsToRemove = allFileIdsRelevant.filter((fileId) => {
43
47
  return !allFileIdsLinked.includes(fileId);
44
48
  }) || [];
@@ -54,6 +58,17 @@ export const ZombieKillerWorker = {
54
58
  fileIds: fileIdsToRemove,
55
59
  })
56
60
  : 0;
61
+ const cartUserIds = (await modules.orders.findCartUserIds()).filter(Boolean);
62
+ const existingUserIds = new Set(cartUserIds.length ? await modules.users.findExistingUserIds({ userIds: cartUserIds }) : []);
63
+ const deadUserIds = cartUserIds.filter((userId) => !existingUserIds.has(userId));
64
+ const deadCarts = deadUserIds.length
65
+ ? await modules.orders.findCarts({ userIds: deadUserIds }, { projection: { _id: 1 } })
66
+ : [];
67
+ let deletedCartsCount = 0;
68
+ await Array.fromAsync(deadCarts, async (cart) => {
69
+ await services.orders.deleteCart(cart._id);
70
+ deletedCartsCount += 1;
71
+ });
57
72
  const result = {
58
73
  deletedFilterTextsCount,
59
74
  deletedProductMediaCount,
@@ -62,6 +77,7 @@ export const ZombieKillerWorker = {
62
77
  deletedAssortmentMediaCount,
63
78
  deletedAssortmentTextsCount,
64
79
  deletedFilesCount,
80
+ deletedCartsCount,
65
81
  };
66
82
  if (error) {
67
83
  return {
@@ -88,3 +104,10 @@ export const ZombieKillerWorker = {
88
104
  },
89
105
  };
90
106
  export default ZombieKillerWorker;
107
+ export const configureZombieKillerAutoscheduling = () => {
108
+ WorkerDirector.configureAutoscheduling({
109
+ type: ZombieKillerWorker.type,
110
+ schedule: everyDayAtTwo,
111
+ retries: 0,
112
+ });
113
+ };
@@ -1,10 +1,13 @@
1
1
  import {} from '@unchainedshop/core';
2
- import { ZombieKillerWorker } from "./adapter.js";
2
+ import { ZombieKillerWorker, configureZombieKillerAutoscheduling } from "./adapter.js";
3
3
  export const ZombieKillerPlugin = {
4
4
  key: 'shop.unchained.worker-plugin.zombie-killer',
5
5
  label: 'Zombie Killer Worker Plugin',
6
6
  version: '1.0.0',
7
7
  adapters: [ZombieKillerWorker],
8
+ onRegister: () => {
9
+ configureZombieKillerAutoscheduling();
10
+ },
8
11
  };
9
12
  export default ZombieKillerPlugin;
10
13
  export { ZombieKillerWorker } from "./adapter.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/plugins",
3
3
  "description": "Official plugin collection for the Unchained Engine with payment, delivery, and pricing adapters",
4
- "version": "4.8.18",
4
+ "version": "4.8.20",
5
5
  "main": "lib/plugins-index.js",
6
6
  "types": "lib/plugins-index.d.ts",
7
7
  "exports": {
@@ -118,10 +118,10 @@
118
118
  "@scure/bip32": "^2.0.0",
119
119
  "@scure/btc-signer": "^2.0.0",
120
120
  "@types/express": "^5.0.3",
121
- "@types/node": "^25.0.0",
121
+ "@types/node": "^26.2.0",
122
122
  "expiry-map": "^2.0.0",
123
123
  "express": "^5.1.0",
124
- "fastify": "^5.4.0",
124
+ "fastify": "^5.12.1",
125
125
  "minio": "^8.0.5",
126
126
  "nodemailer": "^9.0.0",
127
127
  "p-memoize": "^8.0.0",