@akinon/pz-tamara-extension 2.0.0-beta.8 → 2.0.0

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.
package/CHANGELOG.md CHANGED
@@ -1,14 +1,135 @@
1
1
  # @akinon/pz-tamara-extension
2
2
 
3
- ## 2.0.0-beta.8
3
+ ## 2.0.0
4
4
 
5
- ## 2.0.0-beta.7
5
+ ## 2.0.0-beta.27
6
6
 
7
- ## 2.0.0-beta.6
7
+ ## 2.0.0-beta.26
8
+
9
+ ## 2.0.0-beta.25
10
+
11
+ ## 2.0.0-beta.24
12
+
13
+ ## 2.0.0-beta.23
14
+
15
+ ## 2.0.0-beta.22
16
+
17
+ ## 2.0.0-beta.21
18
+
19
+ ## 2.0.0-beta.20
20
+
21
+ ## 1.126.0
22
+
23
+ ## 1.125.2
24
+
25
+ ## 1.125.1
26
+
27
+ ## 1.125.0
28
+
29
+ ## 1.124.0
30
+
31
+ ## 1.123.0
32
+
33
+ ## 1.122.0
34
+
35
+ ## 1.121.0
36
+
37
+ ## 1.120.0
38
+
39
+ ## 1.119.0
40
+
41
+ ## 1.118.0
42
+
43
+ ## 1.117.0
44
+
45
+ ## 1.116.0
46
+
47
+ ## 1.115.0
48
+
49
+ ## 1.114.0
50
+
51
+ ## 1.113.0
52
+
53
+ ## 1.112.0
54
+
55
+ ## 1.111.0
56
+
57
+ ## 1.110.0
58
+
59
+ ## 1.109.0
60
+
61
+ ## 1.108.0
62
+
63
+ ## 1.107.0
64
+
65
+ ## 1.106.0
66
+
67
+ ### Minor Changes
68
+
69
+ - 155cd23: ZERO-3377: Add customization options for Tamara payment gateway component
70
+
71
+ ## 1.105.0
72
+
73
+ ## 1.104.0
74
+
75
+ ## 1.103.0
76
+
77
+ ## 1.102.0
78
+
79
+ ## 1.101.0
80
+
81
+ ## 1.100.0
82
+
83
+ ## 1.99.0
84
+
85
+ ### Minor Changes
86
+
87
+ - d58538b: ZERO-3638: Enhance RC pipeline: add fetch, merge, and pre-release setup with conditional commit
88
+
89
+ ## 1.98.0
90
+
91
+ ## 1.97.0
92
+
93
+ ## 1.96.0
94
+
95
+ ## 1.95.0
96
+
97
+ ## 1.94.0
98
+
99
+ ## 1.93.0
100
+
101
+ ## 1.92.0
102
+
103
+ ## 1.91.0
104
+
105
+ ### Minor Changes
106
+
107
+ - 1e53e17: ZERO-3302: decimal precision in formatDecimal function
108
+ - 07248e0: ZERO-3302: fix decimal formatting for Tamara payment gateway
109
+
110
+ ## 1.90.0
111
+
112
+ ## 1.89.0
113
+
114
+ ### Minor Changes
115
+
116
+ - 1ba5af2: ZERO-3354: Add data-testids for tamara package
117
+
118
+ ## 1.88.0
119
+
120
+ ## 1.87.0
121
+
122
+ ## 1.86.0
123
+
124
+ ## 1.85.0
125
+
126
+ ## 1.84.0
8
127
 
9
128
  ### Minor Changes
10
129
 
11
- - 8f05f9b: ZERO-3250: Beta branch synchronized with Main branch
130
+ - 624a4eb: ZERO-3276: Update installation instructions across multiple README files to standardize format and improve clarity
131
+
132
+ ## 1.83.0
12
133
 
13
134
  ## 1.82.0
14
135
 
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} **/
2
+ module.exports = {
3
+ testEnvironment: 'node',
4
+ transform: {
5
+ '^.+.tsx?$': ['ts-jest', {}]
6
+ }
7
+ };
package/package.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
2
  "name": "@akinon/pz-tamara-extension",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.tsx",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:watch": "jest --watch"
9
+ },
6
10
  "peerDependencies": {
7
- "react": "^18.0.0",
8
- "react-dom": "^18.0.0"
11
+ "react": "^18.0.0 || ^19.0.0",
12
+ "react-dom": "^18.0.0 || ^19.0.0"
9
13
  },
10
14
  "devDependencies": {
15
+ "@types/jest": "^29.5.14",
11
16
  "@types/node": "^18.7.8",
12
17
  "@types/react": "^18.0.17",
13
18
  "@types/react-dom": "^18.0.6",
19
+ "jest": "^29.7.0",
14
20
  "prettier": "^3.0.3",
15
- "react": "^18.2.0",
16
- "react-dom": "^18.2.0",
21
+ "react": "19.2.5",
22
+ "react-dom": "19.2.5",
23
+ "ts-jest": "^29.3.1",
17
24
  "typescript": "^5.2.2"
18
25
  }
19
26
  }
package/readme.md CHANGED
@@ -40,6 +40,94 @@ const TamaraGateway = async ({
40
40
  export default TamaraGateway;
41
41
  ```
42
42
 
43
+ ## Customizing the Tamara Component
44
+
45
+ You can customize the appearance of the Tamara payment gateway using the `renderer` prop. This allows you to provide custom rendering functions for different parts of the component.
46
+
47
+ ### Custom Form Component
48
+
49
+ ```jsx
50
+ import { TamaraPaymentGateway } from '@akinon/pz-tamara-extension';
51
+
52
+ const TamaraGateway = async ({
53
+ searchParams: { sessionId },
54
+ params: { currency, locale }
55
+ }: {
56
+ searchParams: Record<string, string>;
57
+ params: { currency: string; locale: string };
58
+ }) => {
59
+ return (
60
+ <TamaraPaymentGateway
61
+ sessionId={sessionId}
62
+ currency={currency}
63
+ locale={locale}
64
+ extensionUrl={process.env.TAMARA_EXTENSION_URL}
65
+ hashKey={process.env.TAMARA_HASH_KEY}
66
+ renderer={{
67
+ formComponent: {
68
+ renderForm: ({ extensionUrl, sessionId, context, csrfToken, autoSubmit }) => (
69
+ <div className="custom-tamara-form-wrapper">
70
+ <h3 className="text-lg font-semibold mb-4">Tamara Payment</h3>
71
+ <p className="mb-4">You are beign redirected to Tamara payment page...</p>
72
+ <form
73
+ action={`${extensionUrl}/form-page/?sessionId=${sessionId}`}
74
+ method="post"
75
+ encType="multipart/form-data"
76
+ id="tamara-custom-form"
77
+ className="hidden"
78
+ >
79
+ <input type="hidden" name="csrf_token" value={csrfToken} />
80
+ <input type="hidden" name="data" value={JSON.stringify(context)} />
81
+ {autoSubmit && (
82
+ <script
83
+ dangerouslySetInnerHTML={{
84
+ __html: "document.getElementById('tamara-custom-form').submit()"
85
+ }}
86
+ />
87
+ )}
88
+ </form>
89
+ <div className="loader w-12 h-12 border-4 border-t-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
90
+ </div>
91
+ )
92
+ },
93
+ paymentGateway: {
94
+ renderContainer: ({ children }) => (
95
+ <div className="p-8 max-w-lg mx-auto bg-white rounded-lg shadow-md">
96
+ {children}
97
+ </div>
98
+ )
99
+ }
100
+ }}
101
+ />
102
+ );
103
+ };
104
+
105
+ export default TamaraGateway;
106
+ ```
107
+
108
+ ## Custom Renderer API
109
+
110
+ The renderer prop accepts an object with the following structure:
111
+
112
+ ```typescript
113
+ interface TamaraRendererProps {
114
+ formComponent?: {
115
+ renderForm?: (props: {
116
+ extensionUrl: string;
117
+ sessionId: string;
118
+ context: any;
119
+ csrfToken: string;
120
+ autoSubmit: boolean;
121
+ }) => React.ReactNode;
122
+ renderLoading?: () => React.ReactNode;
123
+ renderError?: (error: string) => React.ReactNode;
124
+ };
125
+ paymentGateway?: {
126
+ renderContainer?: (props: { children: React.ReactNode }) => React.ReactNode;
127
+ };
128
+ }
129
+ ```
130
+
43
131
  ## API Routes
44
132
 
45
133
  ### Check Availability API
@@ -0,0 +1,72 @@
1
+ import { formatDecimal, getQuantizeFormat } from '../utils';
2
+
3
+ describe('Utility Functions', () => {
4
+ describe('formatDecimal', () => {
5
+ it('should format decimal numbers correctly', () => {
6
+ expect(formatDecimal(10)).toBe('10');
7
+ expect(formatDecimal(10.5)).toBe('10.5');
8
+ expect(formatDecimal(10.55)).toBe('10.55');
9
+ expect(formatDecimal(10.555)).toBe('10.555');
10
+
11
+ expect(formatDecimal(0)).toBe('0');
12
+ expect(formatDecimal(0.0)).toBe('0');
13
+
14
+ expect(formatDecimal(-10.5)).toBe('-10.5');
15
+
16
+ expect(formatDecimal(0.001)).toBe('0.001');
17
+ });
18
+
19
+ it('should use provided quantizeFormat when specified', () => {
20
+ expect(formatDecimal(10.557, 0.01)).toBe('10.55');
21
+ expect(formatDecimal(10.557, 0.1)).toBe('10.5');
22
+ expect(formatDecimal(10.557, 1)).toBe('10');
23
+
24
+ expect(formatDecimal(10.557, 0)).toBe('10.557');
25
+ });
26
+ });
27
+
28
+ describe('getQuantizeFormat', () => {
29
+ it('should return correct quantize format for different price strings', () => {
30
+ expect(getQuantizeFormat('10')).toBe(0);
31
+ expect(getQuantizeFormat('10.5')).toBe(0.1);
32
+ expect(getQuantizeFormat('10.55')).toBe(0.01);
33
+ expect(getQuantizeFormat('10.555')).toBe(0.001);
34
+
35
+ expect(getQuantizeFormat('0')).toBe(0);
36
+ expect(getQuantizeFormat('0.0')).toBe(0.1);
37
+
38
+ expect(getQuantizeFormat('-10.5')).toBe(0.1);
39
+
40
+ expect(getQuantizeFormat('0.001')).toBe(0.001);
41
+ });
42
+ });
43
+
44
+ describe('Decimal Formatting Integration', () => {
45
+ it('should correctly round basket item amounts', () => {
46
+ const priceAsString = '100.55';
47
+ const quantizeFormat = getQuantizeFormat(priceAsString);
48
+
49
+ const amount = 100.557;
50
+ const roundedAmount =
51
+ Math.floor(amount / quantizeFormat) * quantizeFormat;
52
+
53
+ expect(roundedAmount).toBe(100.55);
54
+ expect(formatDecimal(amount, quantizeFormat)).toBe('100.55');
55
+ });
56
+
57
+ it('should correctly calculate and round tax amounts', () => {
58
+ const priceAsString = '100.55';
59
+ const quantizeFormat = getQuantizeFormat(priceAsString);
60
+
61
+ const amount = 100.55;
62
+ const taxRate = 18.0 / 100;
63
+ const taxAmount = amount * taxRate;
64
+
65
+ const roundedTaxAmount =
66
+ Math.floor(taxAmount / quantizeFormat) * quantizeFormat;
67
+
68
+ expect(roundedTaxAmount).toBe(18.09);
69
+ expect(formatDecimal(taxAmount, quantizeFormat)).toBe('18.09');
70
+ });
71
+ });
72
+ });
@@ -1,16 +1,13 @@
1
1
  import React from 'react';
2
2
  import { cookies } from 'next/headers';
3
-
4
- type FormComponentProps = {
5
- extensionUrl: string;
6
- sessionId: string;
7
- context: any;
8
- };
3
+ import { FormComponentProps } from '../types';
9
4
 
10
5
  const FormComponent = async ({
11
6
  extensionUrl,
12
7
  sessionId,
13
- context
8
+ context,
9
+ renderer,
10
+ autoSubmit = true
14
11
  }: FormComponentProps) => {
15
12
  const nextCookies = await cookies();
16
13
 
@@ -20,9 +17,15 @@ const FormComponent = async ({
20
17
 
21
18
  const csrfToken = nextCookies.get('csrftoken')?.value ?? '';
22
19
 
23
- return (
20
+ const DefaultForm = ({
21
+ extensionUrl,
22
+ sessionId,
23
+ context,
24
+ csrfToken,
25
+ autoSubmit
26
+ }) => (
24
27
  <form
25
- action={`${extensionUrlWithoutSlash}/form-page/?sessionId=${sessionId}`}
28
+ action={`${extensionUrl}/form-page/?sessionId=${sessionId}`}
26
29
  method="post"
27
30
  encType="multipart/form-data"
28
31
  id="tamara-extension-form"
@@ -30,13 +33,27 @@ const FormComponent = async ({
30
33
  <input type="hidden" name="csrf_token" value={csrfToken} />
31
34
  <input type="hidden" name="data" value={JSON.stringify(context)} />
32
35
 
33
- <script
34
- dangerouslySetInnerHTML={{
35
- __html: "document.getElementById('tamara-extension-form').submit()"
36
- }}
37
- />
36
+ {autoSubmit && (
37
+ <script
38
+ dangerouslySetInnerHTML={{
39
+ __html: "document.getElementById('tamara-extension-form').submit()"
40
+ }}
41
+ />
42
+ )}
38
43
  </form>
39
44
  );
45
+
46
+ const RenderForm = renderer?.renderForm || DefaultForm;
47
+
48
+ return (
49
+ <RenderForm
50
+ extensionUrl={extensionUrlWithoutSlash}
51
+ sessionId={sessionId}
52
+ context={context}
53
+ csrfToken={csrfToken}
54
+ autoSubmit={autoSubmit}
55
+ />
56
+ );
40
57
  };
41
58
 
42
59
  export default FormComponent;
package/src/index.tsx CHANGED
@@ -1 +1,6 @@
1
1
  export * from './pages/TamaraPaymentGateway';
2
+ export type {
3
+ TamaraPaymentGatewayProps,
4
+ TamaraRendererProps,
5
+ FormComponentProps
6
+ } from './types';
@@ -6,23 +6,18 @@ import {
6
6
  fetchData,
7
7
  generateHash,
8
8
  getRandomString,
9
- formatDecimal
9
+ formatDecimal,
10
+ getQuantizeFormat
10
11
  } from '../utils';
11
-
12
- type TamaraPaymentGatewayProps = {
13
- sessionId: string;
14
- currency: string;
15
- locale: string;
16
- extensionUrl: string;
17
- hashKey: string;
18
- };
12
+ import { TamaraPaymentGatewayProps } from '../types';
19
13
 
20
14
  export const TamaraPaymentGateway = async ({
21
15
  sessionId,
22
16
  currency,
23
17
  locale,
24
18
  extensionUrl,
25
- hashKey
19
+ hashKey,
20
+ renderer
26
21
  }: TamaraPaymentGatewayProps) => {
27
22
  if (!sessionId || !currency || !locale) {
28
23
  return <></>;
@@ -67,6 +62,10 @@ export const TamaraPaymentGateway = async ({
67
62
  0
68
63
  );
69
64
 
65
+ const quantizeFormat = getQuantizeFormat(
66
+ preOrder.pre_order.unpaid_amount.toString()
67
+ );
68
+
70
69
  let cumulativeAmount = 0;
71
70
  let totalTaxAmount = 0;
72
71
  const basketItems = basketItemSet.map((item: any, index: number) => {
@@ -74,6 +73,10 @@ export const TamaraPaymentGateway = async ({
74
73
  const weight = basketItemAmount / totalProductAmount;
75
74
  let amount = remainingAmount * weight + basketItemAmount;
76
75
 
76
+ if (quantizeFormat !== 0) {
77
+ amount = Math.floor(amount / quantizeFormat) * quantizeFormat;
78
+ }
79
+
77
80
  cumulativeAmount += amount;
78
81
 
79
82
  if (index === basketItemSet.length - 1) {
@@ -82,7 +85,14 @@ export const TamaraPaymentGateway = async ({
82
85
  }
83
86
 
84
87
  const taxRate = Number(item.tax_rate || 0) / 100;
85
- totalTaxAmount += amount * taxRate;
88
+ const itemTaxAmount = amount * taxRate;
89
+
90
+ if (quantizeFormat !== 0) {
91
+ totalTaxAmount +=
92
+ Math.floor(itemTaxAmount / quantizeFormat) * quantizeFormat;
93
+ } else {
94
+ totalTaxAmount += itemTaxAmount;
95
+ }
86
96
 
87
97
  return {
88
98
  name: item.product?.name?.substring(0, 255) || 'none',
@@ -91,7 +101,7 @@ export const TamaraPaymentGateway = async ({
91
101
  sku: item.product?.sku,
92
102
  quantity: item.quantity,
93
103
  total_amount: {
94
- amount: formatDecimal(amount)
104
+ amount: formatDecimal(amount, quantizeFormat)
95
105
  }
96
106
  };
97
107
  });
@@ -100,10 +110,10 @@ export const TamaraPaymentGateway = async ({
100
110
  hash,
101
111
  salt,
102
112
  tax_amount: {
103
- amount: formatDecimal(totalTaxAmount)
113
+ amount: formatDecimal(totalTaxAmount, quantizeFormat)
104
114
  },
105
115
  shipping_amount: {
106
- amount: formatDecimal(shippingAmount)
116
+ amount: formatDecimal(shippingAmount, quantizeFormat)
107
117
  },
108
118
  order_items: basketItems,
109
119
  billing_address: {
@@ -126,11 +136,18 @@ export const TamaraPaymentGateway = async ({
126
136
  }
127
137
  };
128
138
 
139
+ const DefaultContainer = ({ children }) => <>{children}</>;
140
+ const RenderContainer =
141
+ renderer?.paymentGateway?.renderContainer || DefaultContainer;
142
+
129
143
  return (
130
- <FormComponent
131
- extensionUrl={extensionUrl}
132
- sessionId={sessionId}
133
- context={context}
134
- />
144
+ <RenderContainer>
145
+ <FormComponent
146
+ extensionUrl={extensionUrl}
147
+ sessionId={sessionId}
148
+ context={context}
149
+ renderer={renderer?.formComponent}
150
+ />
151
+ </RenderContainer>
135
152
  );
136
153
  };
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+
3
+ export interface TamaraRendererProps {
4
+ formComponent?: {
5
+ renderForm?: (props: {
6
+ extensionUrl: string;
7
+ sessionId: string;
8
+ context: any;
9
+ csrfToken: string;
10
+ autoSubmit: boolean;
11
+ }) => React.ReactNode;
12
+ renderLoading?: () => React.ReactNode;
13
+ renderError?: (error: string) => React.ReactNode;
14
+ };
15
+ paymentGateway?: {
16
+ renderContainer?: (props: { children: React.ReactNode }) => React.ReactNode;
17
+ };
18
+ }
19
+
20
+ export interface FormComponentProps {
21
+ extensionUrl: string;
22
+ sessionId: string;
23
+ context: any;
24
+ renderer?: TamaraRendererProps['formComponent'];
25
+ autoSubmit?: boolean;
26
+ }
27
+
28
+ export interface TamaraPaymentGatewayProps {
29
+ sessionId: string;
30
+ currency: string;
31
+ locale: string;
32
+ extensionUrl: string;
33
+ hashKey: string;
34
+ renderer?: TamaraRendererProps;
35
+ }
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto';
1
+ import * as crypto from 'crypto';
2
2
 
3
3
  export const fetchData = async (url: string, requestHeaders: HeadersInit) => {
4
4
  const response = await fetch(url, { headers: requestHeaders });
@@ -18,11 +18,45 @@ export const generateHash = (...values: string[]): string => {
18
18
  return crypto.createHash('sha512').update(hashStr, 'utf8').digest('hex');
19
19
  };
20
20
 
21
- export const formatDecimal = (value: number): string => {
22
- const decimalStr = value.toString();
23
- const decimalPlaces = decimalStr.includes('.')
24
- ? decimalStr.split('.')[1].length
21
+ export const formatDecimal = (
22
+ value: number,
23
+ quantizeFormat?: number
24
+ ): string => {
25
+ const valueStr = value.toString();
26
+
27
+ // If quantizeFormat is provided, use it to determine decimal places
28
+ if (quantizeFormat !== undefined && quantizeFormat !== 0) {
29
+ // Calculate decimal places from quantizeFormat
30
+ const decimalPlaces = Math.abs(Math.log10(quantizeFormat));
31
+ const roundedValue = Math.floor(value / quantizeFormat) * quantizeFormat;
32
+ return roundedValue.toFixed(decimalPlaces);
33
+ }
34
+
35
+ // Otherwise, determine decimal places from the value
36
+ let decimalPlaces = 0;
37
+ if (valueStr.includes('.')) {
38
+ decimalPlaces = valueStr.split('.')[1].length;
39
+ }
40
+
41
+ if (decimalPlaces === 0) {
42
+ return valueStr;
43
+ }
44
+
45
+ const defaultQuantizeFormat = Math.pow(10, -decimalPlaces);
46
+ const roundedValue =
47
+ Math.floor(value / defaultQuantizeFormat) * defaultQuantizeFormat;
48
+
49
+ return roundedValue.toFixed(decimalPlaces);
50
+ };
51
+
52
+ export const getQuantizeFormat = (priceAsString: string): number => {
53
+ const decimalPlaces = priceAsString.includes('.')
54
+ ? priceAsString.split('.')[1].length
25
55
  : 0;
26
56
 
27
- return value.toFixed(decimalPlaces);
57
+ if (decimalPlaces === 0) {
58
+ return 0;
59
+ }
60
+
61
+ return Math.pow(10, -decimalPlaces);
28
62
  };