@unchainedshop/plugins 2.6.1 → 2.7.1

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 (50) hide show
  1. package/lib/payment/payrexx/api/index.d.ts +35 -0
  2. package/lib/payment/payrexx/api/index.js +100 -0
  3. package/lib/payment/payrexx/api/index.js.map +1 -0
  4. package/lib/payment/payrexx/index.d.ts +1 -0
  5. package/lib/payment/payrexx/index.js +139 -0
  6. package/lib/payment/payrexx/index.js.map +1 -0
  7. package/lib/payment/payrexx/middleware.d.ts +1 -0
  8. package/lib/payment/payrexx/middleware.js +55 -0
  9. package/lib/payment/payrexx/middleware.js.map +1 -0
  10. package/lib/payment/payrexx/payrexx.d.ts +16 -0
  11. package/lib/payment/payrexx/payrexx.js +19 -0
  12. package/lib/payment/payrexx/payrexx.js.map +1 -0
  13. package/lib/payment/stripe/middleware.d.ts +4 -0
  14. package/lib/payment/stripe/middleware.js +54 -47
  15. package/lib/payment/stripe/middleware.js.map +1 -1
  16. package/lib/plugins-index.js +3 -1
  17. package/lib/plugins-index.js.map +1 -1
  18. package/lib/pricing/delivery-swiss-tax.js +3 -0
  19. package/lib/pricing/delivery-swiss-tax.js.map +1 -1
  20. package/lib/pricing/discount-100-off.d.ts +2 -1
  21. package/lib/pricing/discount-100-off.js.map +1 -1
  22. package/lib/pricing/discount-half-price-manual.d.ts +2 -1
  23. package/lib/pricing/discount-half-price-manual.js.map +1 -1
  24. package/lib/pricing/discount-half-price.d.ts +2 -1
  25. package/lib/pricing/discount-half-price.js.map +1 -1
  26. package/lib/pricing/order-discount.d.ts +2 -1
  27. package/lib/pricing/order-discount.js +2 -8
  28. package/lib/pricing/order-discount.js.map +1 -1
  29. package/lib/pricing/order-items-discount.js +4 -3
  30. package/lib/pricing/order-items-discount.js.map +1 -1
  31. package/lib/pricing/product-discount.js +35 -18
  32. package/lib/pricing/product-discount.js.map +1 -1
  33. package/lib/pricing/product-swiss-tax.js +3 -0
  34. package/lib/pricing/product-swiss-tax.js.map +1 -1
  35. package/package.json +22 -22
  36. package/src/payment/payrexx/api/index.ts +132 -0
  37. package/src/payment/payrexx/index.ts +184 -0
  38. package/src/payment/payrexx/middleware.ts +64 -0
  39. package/src/payment/payrexx/payrexx.ts +25 -0
  40. package/src/payment/payrexx/readme.md +4 -0
  41. package/src/payment/stripe/middleware.ts +58 -51
  42. package/src/plugins-index.ts +10 -0
  43. package/src/pricing/delivery-swiss-tax.ts +6 -1
  44. package/src/pricing/discount-100-off.ts +2 -1
  45. package/src/pricing/discount-half-price-manual.ts +2 -1
  46. package/src/pricing/discount-half-price.ts +2 -1
  47. package/src/pricing/order-discount.ts +4 -10
  48. package/src/pricing/order-items-discount.ts +9 -4
  49. package/src/pricing/product-discount.ts +48 -20
  50. package/src/pricing/product-swiss-tax.ts +6 -1
@@ -0,0 +1,184 @@
1
+ import { IPaymentAdapter } from '@unchainedshop/types/payments.js';
2
+ import { PaymentAdapter, PaymentDirector, PaymentError } from '@unchainedshop/core-payment';
3
+ import { createLogger } from '@unchainedshop/logger';
4
+ import { mapOrderDataToGatewayObject } from './payrexx.js';
5
+ import createPayrexxAPI, { GatewayObjectStatus } from './api/index.js';
6
+
7
+ export * from './middleware.js';
8
+
9
+ const logger = createLogger('unchained:core-payment:payrexx');
10
+
11
+ const Payrexx: IPaymentAdapter = {
12
+ ...PaymentAdapter,
13
+
14
+ key: 'shop.unchained.payment.payrexx',
15
+ label: 'Payrexx',
16
+ version: '1.0.0',
17
+
18
+ typeSupported(type) {
19
+ return type === 'GENERIC';
20
+ },
21
+
22
+ actions: (params) => {
23
+ const { modules } = params.context;
24
+
25
+ const getInstance = () => {
26
+ return params.config.find((c) => c.key === 'instance')?.value;
27
+ };
28
+
29
+ const api = createPayrexxAPI(getInstance(), process.env.PAYREXX_SECRET);
30
+
31
+ const adapterActions = {
32
+ ...PaymentAdapter.actions(params),
33
+
34
+ configurationError() {
35
+ if (!process.env.PAYREXX_SECRET) {
36
+ return PaymentError.INCOMPLETE_CONFIGURATION;
37
+ }
38
+ if (!getInstance()) {
39
+ return PaymentError.INCOMPLETE_CONFIGURATION;
40
+ }
41
+ return null;
42
+ },
43
+
44
+ isActive: () => {
45
+ if (adapterActions.configurationError() === null) return true;
46
+ return false;
47
+ },
48
+
49
+ isPayLaterAllowed() {
50
+ return false;
51
+ },
52
+
53
+ validate: async ({ token }) => {
54
+ throw new Error('Token Registration Flow not implemented yet');
55
+ },
56
+
57
+ register: async ({ setupIntentId }) => {
58
+ throw new Error('Token Registration Flow not implemented yet');
59
+ },
60
+
61
+ sign: async (transactionContext = {}) => {
62
+ const { orderPayment, order } = params.paymentContext;
63
+ if (orderPayment) {
64
+ const pricing = await modules.orders.pricingSheet(order);
65
+ const gatewayObject = mapOrderDataToGatewayObject(
66
+ { order, orderPayment, pricing },
67
+ transactionContext,
68
+ );
69
+ const gateway = await api.createGateway(gatewayObject);
70
+ return JSON.stringify(gateway);
71
+ }
72
+
73
+ throw new Error('Token Registration Flow not implemented yet');
74
+ },
75
+
76
+ async confirm() {
77
+ const { orderPayment } = params.paymentContext;
78
+ const { transactionId } = orderPayment;
79
+
80
+ if (!transactionId) {
81
+ return false;
82
+ }
83
+
84
+ const gatewayObject = await api.getGateway(transactionId);
85
+
86
+ if (!gatewayObject) {
87
+ return false;
88
+ }
89
+
90
+ if (
91
+ gatewayObject.status === GatewayObjectStatus.authorized ||
92
+ gatewayObject.status === GatewayObjectStatus.reserved
93
+ ) {
94
+ const allTransactions = gatewayObject.invoices?.flatMap((invoice) => invoice.transactions);
95
+ await Promise.all(
96
+ allTransactions.map(async (transaction) =>
97
+ api.chargePreAuthorized(transaction.id, { referenceId: orderPayment._id }),
98
+ ),
99
+ );
100
+ return true;
101
+ }
102
+ return false;
103
+ },
104
+
105
+ async cancel() {
106
+ const { orderPayment } = params.paymentContext;
107
+ const { transactionId } = orderPayment;
108
+ if (!transactionId) {
109
+ return false;
110
+ }
111
+
112
+ const gatewayObject = await api.getGateway(transactionId);
113
+
114
+ if (!gatewayObject) {
115
+ return false;
116
+ }
117
+
118
+ if (gatewayObject.status === GatewayObjectStatus.reserved) {
119
+ const allTransactions = gatewayObject.invoices?.flatMap((invoice) => invoice.transactions);
120
+ await Promise.all(
121
+ allTransactions.map(async (transaction) => api.deleteReservation(transaction.id)),
122
+ );
123
+ return true;
124
+ }
125
+ return false;
126
+ },
127
+
128
+ charge: async ({ gatewayId, paymentCredentials }) => {
129
+ if (!gatewayId && !paymentCredentials) {
130
+ throw new Error('You have to provide gatewayId or paymentCredentials');
131
+ }
132
+
133
+ const { order } = params.paymentContext;
134
+ const orderPayment = await modules.orders.payments.findOrderPayment({
135
+ orderPaymentId: order.paymentId,
136
+ });
137
+
138
+ const gatewayObject = await api.getGateway(gatewayId);
139
+
140
+ const pricing = await modules.orders.pricingSheet(order);
141
+ const { currency, amount } = pricing.total({ useNetPrice: false });
142
+
143
+ if (
144
+ gatewayObject.currency !== currency.toUpperCase() ||
145
+ gatewayObject.amount !== Math.round(amount)
146
+ ) {
147
+ throw new Error('The price has changed since the intent has been created');
148
+ }
149
+ if (gatewayObject.referenceId !== orderPayment?._id) {
150
+ throw new Error('The order payment is different from the initiating intent');
151
+ }
152
+
153
+ switch (gatewayObject.status) {
154
+ case GatewayObjectStatus.waiting:
155
+ logger.verbose('Charge postponed because Gateway is pending', {
156
+ orderPaymentId: gatewayObject.referenceId,
157
+ });
158
+ return false;
159
+ case GatewayObjectStatus.authorized:
160
+ case GatewayObjectStatus.reserved:
161
+ case GatewayObjectStatus.confirmed:
162
+ // confirm will do the transition, to do a checkout those stati above are fine
163
+ logger.verbose(`Mark as charged, status is ${gatewayObject.status}`, {
164
+ orderPaymentId: gatewayObject.referenceId,
165
+ });
166
+ return {
167
+ transactionId: gatewayId,
168
+ gatewayObject,
169
+ };
170
+ default:
171
+ logger.verbose('Charge not possible', {
172
+ orderPaymentId: gatewayObject.referenceId,
173
+ status: gatewayObject.status,
174
+ });
175
+ throw new Error(`Gateway Status ${gatewayObject.status} does not allow checkout`);
176
+ }
177
+ },
178
+ };
179
+
180
+ return adapterActions;
181
+ },
182
+ };
183
+
184
+ PaymentDirector.registerAdapter(Payrexx);
@@ -0,0 +1,64 @@
1
+ import { Context } from '@unchainedshop/types/api.js';
2
+ import { createLogger } from '@unchainedshop/logger';
3
+
4
+ const logger = createLogger('unchained:core-payment:payrexx:webhook');
5
+
6
+ export const payrexxHandler = async (request, response) => {
7
+ const resolvedContext = request.unchainedContext as Context;
8
+ const { modules } = resolvedContext;
9
+
10
+ const { transaction } = request.body;
11
+
12
+ if (!transaction) {
13
+ logger.verbose(`unhandled event type`, {
14
+ type: Object.keys(request.body).join(','),
15
+ });
16
+ response.writeHead(200);
17
+ response.end({
18
+ ignored: true,
19
+ message: `Unhandled event type: ${Object.keys(request.body).join(',')}. Supported type: transaction`,
20
+ });
21
+ }
22
+
23
+ logger.verbose(`Processing event`, {
24
+ transactionId: transaction.id,
25
+ });
26
+ try {
27
+ const { referenceId: orderPaymentId } = transaction;
28
+
29
+ logger.verbose(`checkout with orderPaymentId: ${orderPaymentId}`);
30
+ await modules.orders.payments.logEvent(orderPaymentId, {
31
+ transactionId: transaction.id,
32
+ });
33
+ const orderPayment = await modules.orders.payments.findOrderPayment({
34
+ orderPaymentId,
35
+ });
36
+ if (!orderPayment) {
37
+ throw new Error(`order payment not found with orderPaymentId: ${orderPaymentId}`);
38
+ }
39
+ const order = await modules.orders.checkout(
40
+ orderPayment.orderId,
41
+ {
42
+ paymentContext: {
43
+ transactionId: transaction.id,
44
+ },
45
+ },
46
+ resolvedContext,
47
+ );
48
+ logger.info(`checkout successful`, {
49
+ orderPaymentId,
50
+ orderId: order._id,
51
+ });
52
+ response.writeHead(200);
53
+ response.end({
54
+ message: 'checkout successful',
55
+ orderId: order._id,
56
+ });
57
+ } catch (error) {
58
+ logger.error(error, {
59
+ transactionId: transaction.id,
60
+ });
61
+ response.writeHead(500);
62
+ response.end(error.message);
63
+ }
64
+ };
@@ -0,0 +1,25 @@
1
+ export const mapOrderDataToGatewayObject = ({ order, orderPayment, pricing }, options = {}) => {
2
+ const {
3
+ EMAIL_WEBSITE_NAME = 'Unchained',
4
+ EMAIL_WEBSITE_URL,
5
+ DATATRANS_SUCCESS_PATH = '/payrexx/success',
6
+ DATATRANS_ERROR_PATH = '/payrexx/error',
7
+ DATATRANS_CANCEL_PATH = '/payrexx/cancel',
8
+ } = process.env;
9
+
10
+ const { currency, amount } = pricing.total({ useNetPrice: false });
11
+ const gatewayObject = {
12
+ amount: Math.round(amount),
13
+ currency: currency.toUpperCase(),
14
+ purpose: encodeURIComponent(`${EMAIL_WEBSITE_NAME} #${order._id}`),
15
+ reservation: true,
16
+ skipResultPage: true,
17
+ successRedirectUrl: `${EMAIL_WEBSITE_URL}${DATATRANS_SUCCESS_PATH}`,
18
+ failedRedirectUrl: `${EMAIL_WEBSITE_URL}${DATATRANS_ERROR_PATH}`,
19
+ cancelRedirectUrl: `${EMAIL_WEBSITE_URL}${DATATRANS_CANCEL_PATH}`,
20
+ referenceId: orderPayment._id,
21
+ 'fields[email]': order.contact?.emailAddress,
22
+ ...options,
23
+ };
24
+ return gatewayObject;
25
+ };
@@ -0,0 +1,4 @@
1
+ ## Test Webhooks:
2
+
3
+ ```
4
+ ```
@@ -4,18 +4,10 @@ import stripe from './stripe.js';
4
4
 
5
5
  const logger = createLogger('unchained:core-payment:stripe:webhook');
6
6
 
7
- const { STRIPE_ENDPOINT_SECRET, STRIPE_WEBHOOK_ENVIRONMENT } = process.env;
8
-
9
- function checkEnvironment(metadata: any) {
10
- const environmentInMetadata = metadata?.environment;
11
- const environmentInEnv = STRIPE_WEBHOOK_ENVIRONMENT;
12
-
13
- if (!environmentInMetadata && !environmentInEnv) {
14
- return true;
15
- }
16
-
17
- return environmentInEnv === environmentInMetadata;
18
- }
7
+ export const WebhookEventTypes = {
8
+ PAYMENT_INTENT_SUCCEEDED: 'payment_intent.succeeded',
9
+ SETUP_INTENT_SUCCEEDED: 'setup_intent.succeeded',
10
+ };
19
11
 
20
12
  export const stripeHandler = async (request, response) => {
21
13
  const resolvedContext = request.unchainedContext as Context;
@@ -25,30 +17,50 @@ export const stripeHandler = async (request, response) => {
25
17
 
26
18
  try {
27
19
  const sig = request.headers['stripe-signature'];
28
- event = stripe.webhooks.constructEvent(request.body, sig, STRIPE_ENDPOINT_SECRET);
29
- logger.verbose(`received event`, {
30
- type: event.type,
31
- });
20
+ event = stripe.webhooks.constructEvent(request.body, sig, process.env.STRIPE_ENDPOINT_SECRET);
32
21
  } catch (err) {
33
- logger.error(`failed event validation with: ${err.message}`);
22
+ logger.error(`Error constructing event: ${err.message}`);
34
23
  response.writeHead(400);
35
24
  response.end(err.message);
36
25
  return;
37
26
  }
38
27
 
28
+ if (!Object.values(WebhookEventTypes).find(event.type)) {
29
+ logger.verbose(`unhandled event type`, {
30
+ type: event.type,
31
+ });
32
+ response.writeHead(200);
33
+ response.end({
34
+ ignored: true,
35
+ message: `Unhandled event type: ${event.type}. Supported types: ${Object.values(WebhookEventTypes).join(', ')}`,
36
+ });
37
+ }
38
+
39
+ const environmentInMetadata = event.data?.object?.metadata?.environment || '';
40
+ const environmentInEnv = process.env.STRIPE_WEBHOOK_ENVIRONMENT || '';
41
+ if (environmentInMetadata !== environmentInEnv) {
42
+ logger.verbose(`unhandled event environment`, {
43
+ type: event.type,
44
+ environment: environmentInMetadata,
45
+ });
46
+ response.writeHead(200);
47
+ response.end(
48
+ JSON.stringify({
49
+ ignored: true,
50
+ message: `Unhandled event environment: ${environmentInMetadata}. Supported environment: ${environmentInEnv}`,
51
+ }),
52
+ );
53
+ return;
54
+ }
55
+
56
+ logger.verbose(`Processing event`, {
57
+ type: event.type,
58
+ });
39
59
  try {
40
- if (event.type === 'payment_intent.succeeded') {
60
+ if (event.type === WebhookEventTypes.PAYMENT_INTENT_SUCCEEDED) {
41
61
  const paymentIntent = event.data.object;
42
62
  const { orderPaymentId } = paymentIntent.metadata || {};
43
63
 
44
- if (!checkEnvironment(paymentIntent.metadata)) {
45
- logger.verbose(`event ignored because of environment difference`, {
46
- type: event.type,
47
- });
48
- response.end(JSON.stringify({ received: true, ignored: true }));
49
- return;
50
- }
51
-
52
64
  logger.verbose(`checkout with orderPaymentId: ${orderPaymentId}`, {
53
65
  type: event.type,
54
66
  });
@@ -75,25 +87,26 @@ export const stripeHandler = async (request, response) => {
75
87
  resolvedContext,
76
88
  );
77
89
 
78
- logger.info(`confirmed checkout for order: ${order._id}`, {
90
+ logger.info(`checkout successful`, {
91
+ orderPaymentId,
79
92
  orderId: order._id,
80
93
  type: event.type,
81
94
  });
82
- } else if (event.type === 'setup_intent.succeeded') {
95
+ response.writeHead(200);
96
+ response.end({
97
+ message: 'checkout successful',
98
+ orderId: order._id,
99
+ });
100
+ } else if (event.type === WebhookEventTypes.SETUP_INTENT_SUCCEEDED) {
83
101
  const setupIntent = event.data.object;
84
102
  const { paymentProviderId, userId } = setupIntent.metadata || {};
85
103
 
86
- if (!checkEnvironment(setupIntent.metadata)) {
87
- response.end(JSON.stringify({ received: true, ignored: true }));
88
- return;
89
- }
90
-
91
104
  logger.verbose(`registered payment credential with paymentProviderId: ${paymentProviderId}`, {
92
105
  type: event.type,
93
106
  userId,
94
107
  });
95
108
 
96
- await modules.payment.registerCredentials(
109
+ const paymentCredentials = await modules.payment.registerCredentials(
97
110
  paymentProviderId,
98
111
  {
99
112
  transactionContext: {
@@ -104,29 +117,23 @@ export const stripeHandler = async (request, response) => {
104
117
  resolvedContext,
105
118
  );
106
119
 
107
- logger.info(`registered payment credentials with paymentProviderId: ${paymentProviderId}`, {
120
+ logger.info(`payment credentials registration successful`, {
108
121
  userId,
122
+ paymentProviderId,
123
+ paymentCredentialsId: paymentCredentials?._id,
109
124
  type: event.type,
110
125
  });
111
- } else {
112
- logger.verbose(`unhandled type`, {
113
- type: event.type,
126
+ response.writeHead(200);
127
+ response.end({
128
+ message: 'payment credentials registration successful',
129
+ paymentCredentialsId: paymentCredentials?._id,
114
130
  });
115
- response.writeHead(404);
116
- response.end();
117
- return;
118
131
  }
119
- } catch (err) {
120
- logger.error(`failed with: ${err.message}`, {
132
+ } catch (error) {
133
+ logger.error(error, {
121
134
  type: event.type,
122
135
  });
123
- response.writeHead(400);
124
- response.end(err.message || 'Error');
125
- return;
136
+ response.writeHead(500);
137
+ response.end(error.message);
126
138
  }
127
- // Return a 200 response to acknowledge receipt of the event
128
- logger.verbose(`event processed`, {
129
- type: event.type,
130
- });
131
- response.end(JSON.stringify({ received: true }));
132
139
  };
@@ -19,6 +19,8 @@ import { appleIAPHandler, configureAppleTransactionsModule } from './payment/app
19
19
  import { stripeHandler } from './payment/stripe/index.js';
20
20
  import { postfinanceCheckoutHandler } from './payment/postfinance-checkout/index.js';
21
21
 
22
+ import { payrexxHandler } from './payment/payrexx/index.js';
23
+
22
24
  // Warehousing
23
25
  import './warehousing/store.js';
24
26
  import './warehousing/eth-minter.js';
@@ -79,6 +81,7 @@ import './accounts/linkedin-oauth.js';
79
81
  const {
80
82
  CRYPTOPAY_WEBHOOK_PATH = '/payment/cryptopay',
81
83
  STRIPE_WEBHOOK_PATH = '/payment/stripe',
84
+ PAYREXX_WEBHOOK_PATH = '/payment/payrexx',
82
85
  PFCHECKOUT_WEBHOOK_PATH = '/payment/postfinance-checkout',
83
86
  DATATRANS_WEBHOOK_PATH = '/payment/datatrans/webhook',
84
87
  APPLE_IAP_WEBHOOK_PATH = '/payment/apple-iap',
@@ -146,6 +149,13 @@ export const connectDefaultPluginsToExpress4 = (
146
149
  appleIAPHandler,
147
150
  );
148
151
 
152
+ useMiddlewareWithCurrentContext(
153
+ app,
154
+ PAYREXX_WEBHOOK_PATH,
155
+ express.json({ type: 'application/json' }),
156
+ payrexxHandler,
157
+ );
158
+
149
159
  // useMiddlewareWithCurrentContext(
150
160
  // app,
151
161
  // MINIO_PUT_SERVER_PATH,
@@ -1,5 +1,8 @@
1
1
  import { DeliveryPricingAdapter, DeliveryPricingDirector } from '@unchainedshop/core-delivery';
2
- import { IDeliveryPricingAdapter } from '@unchainedshop/types/delivery.pricing.js';
2
+ import {
3
+ DeliveryPricingRowCategory,
4
+ IDeliveryPricingAdapter,
5
+ } from '@unchainedshop/types/delivery.pricing.js';
3
6
 
4
7
  import { Order } from '@unchainedshop/types/orders.js';
5
8
  import { DeliveryProvider } from '@unchainedshop/types/delivery.js';
@@ -62,6 +65,7 @@ export const DeliverySwissTax: IDeliveryPricingAdapter = {
62
65
  pricingAdapter.resultSheet().addTax({
63
66
  amount: taxAmount,
64
67
  rate: taxRate,
68
+ baseCategory: DeliveryPricingRowCategory.Delivery,
65
69
  meta: { adapter: DeliverySwissTax.key },
66
70
  });
67
71
  } else {
@@ -69,6 +73,7 @@ export const DeliverySwissTax: IDeliveryPricingAdapter = {
69
73
  pricingAdapter.resultSheet().addTax({
70
74
  amount: taxAmount,
71
75
  rate: taxRate,
76
+ baseCategory: DeliveryPricingRowCategory.Delivery,
72
77
  meta: { adapter: DeliverySwissTax.key },
73
78
  });
74
79
  }
@@ -1,7 +1,8 @@
1
1
  import { IDiscountAdapter } from '@unchainedshop/types/discount.js';
2
2
  import { OrderDiscountDirector, OrderDiscountAdapter } from '@unchainedshop/core-orders';
3
+ import { OrderDiscountConfiguration } from '@unchainedshop/core-orders/director/OrderDiscountConfiguration.js';
3
4
 
4
- export const HundredOff: IDiscountAdapter = {
5
+ export const HundredOff: IDiscountAdapter<OrderDiscountConfiguration> = {
5
6
  ...OrderDiscountAdapter,
6
7
 
7
8
  key: 'shop.unchained.discount.100-off',
@@ -1,7 +1,8 @@
1
1
  import { IDiscountAdapter } from '@unchainedshop/types/discount.js';
2
2
  import { OrderDiscountDirector, OrderDiscountAdapter } from '@unchainedshop/core-orders';
3
+ import { ProductDiscountConfiguration } from '@unchainedshop/core-products/director/ProductDiscountConfiguration.js';
3
4
 
4
- export const HalfPriceManual: IDiscountAdapter = {
5
+ export const HalfPriceManual: IDiscountAdapter<ProductDiscountConfiguration> = {
5
6
  ...OrderDiscountAdapter,
6
7
 
7
8
  key: 'shop.unchained.discount.half-price-manual',
@@ -1,7 +1,8 @@
1
1
  import { IDiscountAdapter } from '@unchainedshop/types/discount.js';
2
2
  import { OrderDiscountDirector, OrderDiscountAdapter } from '@unchainedshop/core-orders';
3
+ import { ProductDiscountConfiguration } from '@unchainedshop/core-products/director/ProductDiscountConfiguration.js';
3
4
 
4
- export const HalfPrice: IDiscountAdapter = {
5
+ export const HalfPrice: IDiscountAdapter<ProductDiscountConfiguration> = {
5
6
  ...OrderDiscountAdapter,
6
7
 
7
8
  key: 'shop.unchained.discount.half-price',
@@ -1,8 +1,9 @@
1
1
  import { IOrderPricingAdapter, OrderPricingRowCategory } from '@unchainedshop/types/orders.pricing.js';
2
2
  import { OrderPricingDirector, OrderPricingAdapter } from '@unchainedshop/core-orders';
3
3
  import { calculation as calcUtils } from '@unchainedshop/utils';
4
+ import { OrderDiscountConfiguration } from '@unchainedshop/core-orders/director/OrderDiscountConfiguration.js';
4
5
 
5
- export const OrderDiscount: IOrderPricingAdapter = {
6
+ export const OrderDiscount: IOrderPricingAdapter<OrderDiscountConfiguration> = {
6
7
  ...OrderPricingAdapter,
7
8
 
8
9
  key: 'shop.unchained.pricing.order-discount',
@@ -22,9 +23,6 @@ export const OrderDiscount: IOrderPricingAdapter = {
22
23
  ...pricingAdapter,
23
24
 
24
25
  calculate: async () => {
25
- // discounts need to provide a *fixedRate*
26
- // if you want to add percentual discounts,
27
- // add it to the order item calculation
28
26
  const totalAmountOfItems = params.calculationSheet.total({
29
27
  category: OrderPricingRowCategory.Items,
30
28
  useNetPrice: false,
@@ -61,10 +59,8 @@ export const OrderDiscount: IOrderPricingAdapter = {
61
59
 
62
60
  params.discounts.forEach(({ configuration, discountId }) => {
63
61
  // First, we deduce the discount from the items
64
- let alreadyDeducted = 0;
65
-
66
62
  const leftInDiscountToSplit = calcUtils.calculateAmountToSplit(
67
- { ...configuration, alreadyDeducted },
63
+ { ...configuration },
68
64
  totalAmountOfItems,
69
65
  );
70
66
  const [itemsDiscountAmount, itemsTaxAmount] = calcUtils.applyDiscountToMultipleShares(
@@ -72,11 +68,10 @@ export const OrderDiscount: IOrderPricingAdapter = {
72
68
  Math.max(0, Math.min(amountLeft, leftInDiscountToSplit)),
73
69
  );
74
70
  amountLeft -= itemsDiscountAmount;
75
- alreadyDeducted += itemsDiscountAmount;
76
71
 
77
72
  // After the items, we deduct the remaining discount from payment & delivery fees
78
73
  const leftInFeesToSplit = calcUtils.calculateAmountToSplit(
79
- { ...configuration, alreadyDeducted },
74
+ { ...configuration },
80
75
  totalAmountOfPaymentAndDelivery,
81
76
  );
82
77
  const [deliveryAndPaymentDiscountAmount, deliveryAndPaymentTaxAmount] =
@@ -85,7 +80,6 @@ export const OrderDiscount: IOrderPricingAdapter = {
85
80
  Math.max(0, Math.min(amountLeft, leftInFeesToSplit)),
86
81
  );
87
82
  amountLeft -= deliveryAndPaymentDiscountAmount;
88
- alreadyDeducted += itemsDiscountAmount;
89
83
 
90
84
  const discountAmount = (itemsDiscountAmount + deliveryAndPaymentDiscountAmount) * -1;
91
85
  const taxAmount = (itemsTaxAmount + deliveryAndPaymentTaxAmount) * -1;
@@ -1,8 +1,9 @@
1
1
  import { IOrderPricingAdapter, OrderPricingRowCategory } from '@unchainedshop/types/orders.pricing.js';
2
2
  import { OrderPricingDirector, OrderPricingAdapter } from '@unchainedshop/core-orders';
3
3
  import { calculation as calcUtils } from '@unchainedshop/utils';
4
+ import { OrderDiscountConfiguration } from '@unchainedshop/core-orders/director/OrderDiscountConfiguration.js';
4
5
 
5
- const OrderItemsDiscount: IOrderPricingAdapter = {
6
+ const OrderItemsDiscount: IOrderPricingAdapter<OrderDiscountConfiguration> = {
6
7
  ...OrderPricingAdapter,
7
8
 
8
9
  key: 'shop.unchained.pricing.order-items-discount',
@@ -38,15 +39,19 @@ const OrderItemsDiscount: IOrderPricingAdapter = {
38
39
  ),
39
40
  );
40
41
 
41
- let alreadyDeducted = 0;
42
+ let amountLeft = totalAmountOfItems;
42
43
 
43
44
  params.discounts.forEach(({ configuration, discountId }) => {
44
45
  // First, we deduce the discount from the items
46
+ const leftInItemsToSplit = calcUtils.calculateAmountToSplit(
47
+ { ...configuration },
48
+ totalAmountOfItems,
49
+ );
45
50
  const [itemsDiscountAmount, itemsTaxAmount] = calcUtils.applyDiscountToMultipleShares(
46
51
  itemShares,
47
- calcUtils.calculateAmountToSplit({ ...configuration, alreadyDeducted }, totalAmountOfItems),
52
+ Math.max(0, Math.min(amountLeft, leftInItemsToSplit)),
48
53
  );
49
- alreadyDeducted = +itemsDiscountAmount;
54
+ amountLeft -= itemsDiscountAmount;
50
55
 
51
56
  const discountAmount = itemsDiscountAmount * -1;
52
57
  const taxAmount = itemsTaxAmount * -1;