@akinon/pz-tamara-extension 1.80.0-rc.7
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/.prettierrc +13 -0
- package/CHANGELOG.md +7 -0
- package/package.json +19 -0
- package/readme.md +138 -0
- package/src/components/FormComponent.tsx +42 -0
- package/src/index.tsx +1 -0
- package/src/pages/TamaraPaymentGateway.tsx +136 -0
- package/src/pages/api/check-availability.ts +113 -0
- package/src/redux/api.ts +36 -0
- package/src/utils/index.ts +28 -0
package/.prettierrc
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bracketSameLine": false,
|
|
3
|
+
"tabWidth": 2,
|
|
4
|
+
"singleQuote": true,
|
|
5
|
+
"jsxSingleQuote": false,
|
|
6
|
+
"bracketSpacing": true,
|
|
7
|
+
"semi": true,
|
|
8
|
+
"useTabs": false,
|
|
9
|
+
"arrowParens": "always",
|
|
10
|
+
"endOfLine": "lf",
|
|
11
|
+
"proseWrap": "never",
|
|
12
|
+
"trailingComma": "none"
|
|
13
|
+
}
|
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akinon/pz-tamara-extension",
|
|
3
|
+
"version": "1.80.0-rc.7",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^18.0.0",
|
|
8
|
+
"react-dom": "^18.0.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^18.7.8",
|
|
12
|
+
"@types/react": "^18.0.17",
|
|
13
|
+
"@types/react-dom": "^18.0.6",
|
|
14
|
+
"prettier": "^3.0.3",
|
|
15
|
+
"react": "^18.2.0",
|
|
16
|
+
"react-dom": "^18.2.0",
|
|
17
|
+
"typescript": "^5.2.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Tamara Payment Gateway Extension
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
There are two ways to install the Tamara Payment Gateway extension:
|
|
6
|
+
|
|
7
|
+
### 1. Install the npm package using Yarn
|
|
8
|
+
|
|
9
|
+
Before installing with Yarn, you need to add 'pz-tamara-extension' to your `plugins.js` file:
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// plugins.js
|
|
13
|
+
module.exports = {
|
|
14
|
+
extensions: [
|
|
15
|
+
// ... other extensions
|
|
16
|
+
'pz-tamara-extension'
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then, you can install the package using Yarn:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
yarn add @akinon/pz-tamara-extension
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Preferred installation method
|
|
28
|
+
|
|
29
|
+
You can also use the following command to install the extension with the latest plugins:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @akinon/projectzero@latest --plugins
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Once the extension is installed, you can easily integrate the Tamara payment gateway into your application. Here's an example of how to use it.
|
|
38
|
+
|
|
39
|
+
1. Navigate to the `src/app/[commerce]/[locale]/[currency]/payment-gateway/tamara/` directory.
|
|
40
|
+
|
|
41
|
+
2. Create a file named `page.tsx` and include the following code:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { TamaraPaymentGateway } from '@akinon/pz-tamara-extension';
|
|
45
|
+
|
|
46
|
+
const TamaraGateway = async ({
|
|
47
|
+
searchParams: { sessionId },
|
|
48
|
+
params: { currency, locale }
|
|
49
|
+
}: {
|
|
50
|
+
searchParams: Record<string, string>;
|
|
51
|
+
params: { currency: string; locale: string };
|
|
52
|
+
}) => {
|
|
53
|
+
return (
|
|
54
|
+
<TamaraPaymentGateway
|
|
55
|
+
sessionId={sessionId}
|
|
56
|
+
currency={currency}
|
|
57
|
+
locale={locale}
|
|
58
|
+
extensionUrl={process.env.TAMARA_EXTENSION_URL}
|
|
59
|
+
hashKey={process.env.TAMARA_HASH_KEY}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default TamaraGateway;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API Routes
|
|
68
|
+
|
|
69
|
+
### Check Availability API
|
|
70
|
+
|
|
71
|
+
To enable Tamara payment availability checks, you need to create an API route. Create a file at `src/app/api/tamara-check-availability/route.ts` with the following content:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { POST } from '@akinon/pz-tamara-extension/src/pages/api/check-availability';
|
|
75
|
+
|
|
76
|
+
export { POST };
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This API endpoint handles checking the availability of Tamara payment for a given:
|
|
80
|
+
|
|
81
|
+
- Country
|
|
82
|
+
- Phone number
|
|
83
|
+
- Order amount
|
|
84
|
+
- Currency
|
|
85
|
+
|
|
86
|
+
The endpoint automatically validates the request and response using hash-based security measures.
|
|
87
|
+
|
|
88
|
+
### Using checkTamaraAvailability Mutation
|
|
89
|
+
|
|
90
|
+
The extension provides a Redux mutation hook that you can use to check Tamara payment availability. Here's an example of how to implement it:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { useCheckTamaraAvailabilityMutation } from '@akinon/pz-tamara-extension/src/redux/api';
|
|
94
|
+
|
|
95
|
+
const YourComponent = () => {
|
|
96
|
+
const [checkTamaraAvailability] = useCheckTamaraAvailabilityMutation();
|
|
97
|
+
const [isTamaraAvailable, setIsTamaraAvailable] = useState(false);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const checkAvailability = async () => {
|
|
101
|
+
try {
|
|
102
|
+
const response = await checkTamaraAvailability({
|
|
103
|
+
country: 'AE', // Country code
|
|
104
|
+
phone_number: '+971123456789', // Customer's phone number
|
|
105
|
+
order_amount: 1000 // Order total amount
|
|
106
|
+
}).unwrap();
|
|
107
|
+
|
|
108
|
+
setIsTamaraAvailable(response.has_availability);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error checking Tamara availability:', error);
|
|
111
|
+
setIsTamaraAvailable(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
checkAvailability();
|
|
116
|
+
}, [checkTamaraAvailability]);
|
|
117
|
+
|
|
118
|
+
// Use isTamaraAvailable to conditionally render Tamara payment option
|
|
119
|
+
return (
|
|
120
|
+
// Your component JSX
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The mutation returns an object with the following properties:
|
|
126
|
+
|
|
127
|
+
- `has_availability`: boolean indicating if Tamara payment is available
|
|
128
|
+
- `salt`: string used for hash verification
|
|
129
|
+
- `hash`: string for response validation
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Add these variables to your `.env` file
|
|
134
|
+
|
|
135
|
+
```env
|
|
136
|
+
TAMARA_EXTENSION_URL=<your_extension_url>
|
|
137
|
+
TAMARA_HASH_KEY=<your_hash_key>
|
|
138
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
type FormComponentProps = {
|
|
5
|
+
extensionUrl: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
context: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const FormComponent = ({
|
|
11
|
+
extensionUrl,
|
|
12
|
+
sessionId,
|
|
13
|
+
context
|
|
14
|
+
}: FormComponentProps) => {
|
|
15
|
+
const nextCookies = cookies();
|
|
16
|
+
|
|
17
|
+
const extensionUrlWithoutSlash = `${extensionUrl}`.endsWith('/')
|
|
18
|
+
? extensionUrl.slice(0, -1)
|
|
19
|
+
: extensionUrl;
|
|
20
|
+
|
|
21
|
+
const csrfToken = nextCookies.get('csrftoken')?.value ?? '';
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<form
|
|
25
|
+
action={`${extensionUrlWithoutSlash}/form-page/?sessionId=${sessionId}`}
|
|
26
|
+
method="post"
|
|
27
|
+
encType="multipart/form-data"
|
|
28
|
+
id="tamara-extension-form"
|
|
29
|
+
>
|
|
30
|
+
<input type="hidden" name="csrf_token" value={csrfToken} />
|
|
31
|
+
<input type="hidden" name="data" value={JSON.stringify(context)} />
|
|
32
|
+
|
|
33
|
+
<script
|
|
34
|
+
dangerouslySetInnerHTML={{
|
|
35
|
+
__html: "document.getElementById('tamara-extension-form').submit()"
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
</form>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default FormComponent;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pages/TamaraPaymentGateway';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import settings from 'settings';
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
import FormComponent from '../components/FormComponent';
|
|
5
|
+
import {
|
|
6
|
+
fetchData,
|
|
7
|
+
generateHash,
|
|
8
|
+
getRandomString,
|
|
9
|
+
formatDecimal
|
|
10
|
+
} from '../utils';
|
|
11
|
+
|
|
12
|
+
type TamaraPaymentGatewayProps = {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
locale: string;
|
|
16
|
+
extensionUrl: string;
|
|
17
|
+
hashKey: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const TamaraPaymentGateway = async ({
|
|
21
|
+
sessionId,
|
|
22
|
+
currency,
|
|
23
|
+
locale,
|
|
24
|
+
extensionUrl,
|
|
25
|
+
hashKey
|
|
26
|
+
}: TamaraPaymentGatewayProps) => {
|
|
27
|
+
if (!sessionId || !currency || !locale) {
|
|
28
|
+
return <></>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nextCookies = cookies();
|
|
32
|
+
|
|
33
|
+
const language = settings.localization.locales.find(
|
|
34
|
+
(item) => item.value === locale
|
|
35
|
+
).apiValue;
|
|
36
|
+
|
|
37
|
+
const requestHeaders = {
|
|
38
|
+
Cookie: `osessionid=${nextCookies.get('osessionid')?.value}`,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'X-Currency': currency,
|
|
41
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
42
|
+
'Accept-Language': language
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const preOrder = await fetchData(
|
|
46
|
+
`${settings.commerceUrl}/orders/checkout/?page=OrderNotePage`,
|
|
47
|
+
requestHeaders
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const salt = getRandomString(10);
|
|
51
|
+
const hash = generateHash(hashKey, sessionId, salt);
|
|
52
|
+
|
|
53
|
+
const unpaidAmount = Number(preOrder.pre_order.unpaid_amount || 0);
|
|
54
|
+
const shippingAmount = Math.min(
|
|
55
|
+
Number(preOrder.pre_order.shipping_amount || 0),
|
|
56
|
+
unpaidAmount
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const unpaidAmountWithoutShipping = unpaidAmount - shippingAmount;
|
|
60
|
+
const basketItemSet = preOrder.pre_order.basket.basketitem_set;
|
|
61
|
+
const totalProductAmount = basketItemSet.reduce(
|
|
62
|
+
(sum: number, item: any) => sum + Number(item.total_amount),
|
|
63
|
+
0
|
|
64
|
+
);
|
|
65
|
+
const remainingAmount = Math.max(
|
|
66
|
+
unpaidAmountWithoutShipping - totalProductAmount,
|
|
67
|
+
0
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let cumulativeAmount = 0;
|
|
71
|
+
let totalTaxAmount = 0;
|
|
72
|
+
const basketItems = basketItemSet.map((item: any, index: number) => {
|
|
73
|
+
const basketItemAmount = Number(item.total_amount);
|
|
74
|
+
const weight = basketItemAmount / totalProductAmount;
|
|
75
|
+
let amount = remainingAmount * weight + basketItemAmount;
|
|
76
|
+
|
|
77
|
+
cumulativeAmount += amount;
|
|
78
|
+
|
|
79
|
+
if (index === basketItemSet.length - 1) {
|
|
80
|
+
const delta = unpaidAmountWithoutShipping - cumulativeAmount;
|
|
81
|
+
amount += delta;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const taxRate = Number(item.tax_rate || 0) / 100;
|
|
85
|
+
totalTaxAmount += amount * taxRate;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
name: item.product?.name?.substring(0, 255) || 'none',
|
|
89
|
+
type: item.product?.category?.name,
|
|
90
|
+
reference_id: item.pk,
|
|
91
|
+
sku: item.product?.sku,
|
|
92
|
+
quantity: item.quantity,
|
|
93
|
+
total_amount: {
|
|
94
|
+
amount: formatDecimal(amount)
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const context = {
|
|
100
|
+
hash,
|
|
101
|
+
salt,
|
|
102
|
+
tax_amount: {
|
|
103
|
+
amount: formatDecimal(totalTaxAmount)
|
|
104
|
+
},
|
|
105
|
+
shipping_amount: {
|
|
106
|
+
amount: formatDecimal(shippingAmount)
|
|
107
|
+
},
|
|
108
|
+
order_items: basketItems,
|
|
109
|
+
billing_address: {
|
|
110
|
+
city: preOrder.pre_order.billing_address.city.name,
|
|
111
|
+
country_code: preOrder.pre_order.billing_address.country.code,
|
|
112
|
+
first_name: preOrder.pre_order.billing_address.first_name,
|
|
113
|
+
last_name: preOrder.pre_order.billing_address.last_name,
|
|
114
|
+
line1: preOrder.pre_order.billing_address.line,
|
|
115
|
+
phone_number: preOrder.pre_order.billing_address.phone_number,
|
|
116
|
+
region: preOrder.pre_order.billing_address.township.name
|
|
117
|
+
},
|
|
118
|
+
shipping_address: {
|
|
119
|
+
city: preOrder.pre_order.shipping_address.city.name,
|
|
120
|
+
country_code: preOrder.pre_order.shipping_address.country.code,
|
|
121
|
+
first_name: preOrder.pre_order.shipping_address.first_name,
|
|
122
|
+
last_name: preOrder.pre_order.shipping_address.last_name,
|
|
123
|
+
line1: preOrder.pre_order.shipping_address.line,
|
|
124
|
+
phone_number: preOrder.pre_order.shipping_address.phone_number,
|
|
125
|
+
region: preOrder.pre_order.shipping_address.township.name
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<FormComponent
|
|
131
|
+
extensionUrl={extensionUrl}
|
|
132
|
+
sessionId={sessionId}
|
|
133
|
+
context={context}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { getRandomString, generateHash } from '../../utils';
|
|
5
|
+
|
|
6
|
+
interface CheckAvailabilityResponse {
|
|
7
|
+
salt: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
has_availability: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verifyResponseHash = (
|
|
13
|
+
salt: string,
|
|
14
|
+
hasAvailability: boolean,
|
|
15
|
+
hashKey: string,
|
|
16
|
+
responseHash: string
|
|
17
|
+
): boolean => {
|
|
18
|
+
return (
|
|
19
|
+
generateHash(salt, hasAvailability ? 'True' : 'False', hashKey) ===
|
|
20
|
+
responseHash
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function POST(request: NextRequest) {
|
|
25
|
+
try {
|
|
26
|
+
const hashKey = process.env.TAMARA_HASH_KEY;
|
|
27
|
+
const extensionUrl = process.env.TAMARA_EXTENSION_URL;
|
|
28
|
+
|
|
29
|
+
if (!hashKey) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ message: 'TAMARA_HASH_KEY environment variable is not set' },
|
|
32
|
+
{ status: 500 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!extensionUrl) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ message: 'TAMARA_EXTENSION_URL environment variable is not set' },
|
|
39
|
+
{ status: 500 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = await request.json();
|
|
44
|
+
const { country, phone_number, order_amount } = body;
|
|
45
|
+
const currency = request.cookies.get('pz-currency')?.value;
|
|
46
|
+
|
|
47
|
+
if (!currency) {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ message: 'Currency not found in cookies' },
|
|
50
|
+
{ status: 400 }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!country || !phone_number || !order_amount) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ message: 'Missing required fields' },
|
|
57
|
+
{ status: 400 }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const upperCountry = country.toUpperCase();
|
|
62
|
+
const upperCurrency = currency.toUpperCase();
|
|
63
|
+
|
|
64
|
+
const salt = getRandomString(10);
|
|
65
|
+
const hash = generateHash(salt, upperCountry, phone_number, hashKey);
|
|
66
|
+
|
|
67
|
+
const response = await fetch(`${extensionUrl}/check-availability`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json'
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
salt,
|
|
74
|
+
hash,
|
|
75
|
+
country: upperCountry,
|
|
76
|
+
phone_number,
|
|
77
|
+
order_value: {
|
|
78
|
+
amount: order_amount,
|
|
79
|
+
currency: upperCurrency
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errorData = await response.json();
|
|
86
|
+
return NextResponse.json(errorData, { status: response.status });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data: CheckAvailabilityResponse = await response.json();
|
|
90
|
+
|
|
91
|
+
const isValidHash = verifyResponseHash(
|
|
92
|
+
data.salt,
|
|
93
|
+
data.has_availability,
|
|
94
|
+
hashKey,
|
|
95
|
+
data.hash
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!isValidHash) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ message: 'Invalid response hash' },
|
|
101
|
+
{ status: 400 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return NextResponse.json(data);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error checking Tamara availability:', error);
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
{ message: 'Internal server error' },
|
|
110
|
+
{ status: 500 }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/redux/api.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { api } from '@akinon/next/data/client/api';
|
|
2
|
+
import { setPaymentStepBusy } from '@akinon/next/redux/reducers/checkout';
|
|
3
|
+
|
|
4
|
+
interface CheckTamaraAvailabilityRequest {
|
|
5
|
+
country: string;
|
|
6
|
+
phone_number: string;
|
|
7
|
+
order_amount: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CheckTamaraAvailabilityResponse {
|
|
11
|
+
salt: string;
|
|
12
|
+
hash: string;
|
|
13
|
+
has_availability: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const tamaraApi = api.injectEndpoints({
|
|
17
|
+
endpoints: (build) => ({
|
|
18
|
+
checkTamaraAvailability: build.mutation<
|
|
19
|
+
CheckTamaraAvailabilityResponse,
|
|
20
|
+
CheckTamaraAvailabilityRequest
|
|
21
|
+
>({
|
|
22
|
+
query: (body) => ({
|
|
23
|
+
url: '/api/tamara-check-availability',
|
|
24
|
+
method: 'POST',
|
|
25
|
+
body
|
|
26
|
+
}),
|
|
27
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
28
|
+
dispatch(setPaymentStepBusy(true));
|
|
29
|
+
await queryFulfilled;
|
|
30
|
+
dispatch(setPaymentStepBusy(false));
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const { useCheckTamaraAvailabilityMutation } = tamaraApi;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export const fetchData = async (url: string, requestHeaders: HeadersInit) => {
|
|
4
|
+
const response = await fetch(url, { headers: requestHeaders });
|
|
5
|
+
return response.json();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getRandomString = (length: number): string => {
|
|
9
|
+
const characters =
|
|
10
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
11
|
+
return Array.from({ length }, () =>
|
|
12
|
+
characters.charAt(Math.floor(Math.random() * characters.length))
|
|
13
|
+
).join('');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const generateHash = (...values: string[]): string => {
|
|
17
|
+
const hashStr = values.join('|');
|
|
18
|
+
return crypto.createHash('sha512').update(hashStr, 'utf8').digest('hex');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const formatDecimal = (value: number): string => {
|
|
22
|
+
const decimalStr = value.toString();
|
|
23
|
+
const decimalPlaces = decimalStr.includes('.')
|
|
24
|
+
? decimalStr.split('.')[1].length
|
|
25
|
+
: 0;
|
|
26
|
+
|
|
27
|
+
return value.toFixed(decimalPlaces);
|
|
28
|
+
};
|