simplepay-js-sdk 0.6.1 → 0.8.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/README-ENG.md +203 -0
- package/README.md +138 -36
- package/dist/index.d.ts +57 -19
- package/dist/index.js +140 -68
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +8 -157
- package/src/oneTime.spec.ts +97 -0
- package/src/oneTime.ts +33 -0
- package/src/recurring.spec.ts +71 -0
- package/src/recurring.ts +94 -0
- package/src/types.ts +72 -12
- package/src/{index.spec.ts → utils.spec.ts} +6 -78
- package/src/utils.ts +149 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { startRecurringPayment } from './recurring'
|
|
3
|
+
import { toISO8601DateString } from './utils'
|
|
4
|
+
|
|
5
|
+
const setEnv = () => {
|
|
6
|
+
process.env.SIMPLEPAY_MERCHANT_ID_HUF = 'testId'
|
|
7
|
+
process.env.SIMPLEPAY_MERCHANT_KEY_HUF = 'testKey'
|
|
8
|
+
process.env.SIMPLEPAY_MERCHANT_ID_EUR = 'merchantEuroId'
|
|
9
|
+
process.env.SIMPLEPAY_MERCHANT_KEY_EUR = 'secretEuroKey'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const paymentData = {
|
|
13
|
+
orderRef: 'TEST123',
|
|
14
|
+
customer: 'Radharadhya Dasa',
|
|
15
|
+
customerEmail: 'test@example.com',
|
|
16
|
+
total: 1212,
|
|
17
|
+
recurring: {
|
|
18
|
+
times: 3,
|
|
19
|
+
until: toISO8601DateString(new Date(Date.now() + 6 * 30 * 24 * 60 * 60 * 1000)),
|
|
20
|
+
maxAmount: 12000
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
describe('SimplePay Recurring Tests', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Clear all environment variables before each test
|
|
26
|
+
delete process.env.SIMPLEPAY_MERCHANT_ID_HUF
|
|
27
|
+
delete process.env.SIMPLEPAY_MERCHANT_KEY_HUF
|
|
28
|
+
delete process.env.SIMPLEPAY_MERCHANT_ID_EUR
|
|
29
|
+
delete process.env.SIMPLEPAY_MERCHANT_KEY_EUR
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('startRecurringPayment', () => {
|
|
33
|
+
it('should throw error when merchant configuration is missing', async () => {
|
|
34
|
+
await expect(startRecurringPayment(paymentData)).rejects.toThrow('Missing SimplePay configuration')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle API errors correctly', async () => {
|
|
38
|
+
setEnv()
|
|
39
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
40
|
+
ok: true,
|
|
41
|
+
headers: {
|
|
42
|
+
get: vi.fn().mockReturnValue('mockSignature')
|
|
43
|
+
},
|
|
44
|
+
text: vi.fn().mockResolvedValue(JSON.stringify({
|
|
45
|
+
transactionId: '123456',
|
|
46
|
+
total: '1212',
|
|
47
|
+
merchant: 'testId'
|
|
48
|
+
}))
|
|
49
|
+
}) as unknown as typeof fetch
|
|
50
|
+
await expect(startRecurringPayment(paymentData)).rejects.toThrow('Invalid response signature')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should successfully start recurring payment and card registration when API returns valid response', async () => {
|
|
54
|
+
setEnv()
|
|
55
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
56
|
+
ok: true,
|
|
57
|
+
headers: {
|
|
58
|
+
get: vi.fn().mockReturnValue('bxSwUc0qn0oABSRcq9uawF6zncFBhRk/AbO4HznYR9Pt5SjocyxAD+9Q4bE44h0J')
|
|
59
|
+
},
|
|
60
|
+
text: vi.fn().mockResolvedValue(JSON.stringify({
|
|
61
|
+
transactionId: '123456',
|
|
62
|
+
total: '1212',
|
|
63
|
+
merchant: 'testId'
|
|
64
|
+
}))
|
|
65
|
+
}) as unknown as typeof fetch
|
|
66
|
+
|
|
67
|
+
await expect(startRecurringPayment(paymentData)).resolves.toBeDefined()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
})
|
package/src/recurring.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { SimplePayRecurringRequestBody, RecurringPaymentData, TokenPaymentData, SimplePayTokenRequestBody, SimplePayCardCancelRequestBody} from './types'
|
|
3
|
+
import { getSimplePayConfig, simplepayLogger, toISO8601DateString, makeSimplePayTokenRequest, makeSimplePayRecurringRequest, makeSimplePayRequest, makeSimplePayCardCancelRequest} from './utils'
|
|
4
|
+
|
|
5
|
+
const INTERVAL_IN_MONTHS = 6
|
|
6
|
+
const DEFAULT_UNTIL = new Date(Date.now() + INTERVAL_IN_MONTHS * 30 * 24 * 60 * 60 * 1000)
|
|
7
|
+
const DEFAULT_MAX_AMOUNT = 12000
|
|
8
|
+
const DEFAULT_TIMES = 3
|
|
9
|
+
|
|
10
|
+
const startRecurringPayment = async (paymentData: RecurringPaymentData) => {
|
|
11
|
+
simplepayLogger({ function: 'SimplePay/startRecurringPayment', paymentData })
|
|
12
|
+
const currency = paymentData.currency || 'HUF'
|
|
13
|
+
const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)
|
|
14
|
+
simplepayLogger({ function: 'SimplePay/startRecurringPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })
|
|
15
|
+
|
|
16
|
+
if (!MERCHANT_KEY || !MERCHANT_ID) {
|
|
17
|
+
throw new Error(`Missing SimplePay configuration for ${currency}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const requestBody: SimplePayRecurringRequestBody = {
|
|
21
|
+
salt: crypto.randomBytes(16).toString('hex'),
|
|
22
|
+
merchant: MERCHANT_ID,
|
|
23
|
+
orderRef: paymentData.orderRef,
|
|
24
|
+
currency,
|
|
25
|
+
customer: paymentData.customer,
|
|
26
|
+
customerEmail: paymentData.customerEmail,
|
|
27
|
+
language: paymentData.language || 'HU',
|
|
28
|
+
sdkVersion: SDK_VERSION,
|
|
29
|
+
methods: ['CARD'],
|
|
30
|
+
recurring: {
|
|
31
|
+
times: paymentData.recurring.times || DEFAULT_TIMES,
|
|
32
|
+
until: paymentData.recurring.until || toISO8601DateString(DEFAULT_UNTIL),
|
|
33
|
+
maxAmount: paymentData.recurring.maxAmount || DEFAULT_MAX_AMOUNT
|
|
34
|
+
},
|
|
35
|
+
threeDSReqAuthMethod: '02',
|
|
36
|
+
total: String(paymentData.total),
|
|
37
|
+
timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),
|
|
38
|
+
url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',
|
|
39
|
+
invoice: paymentData.invoice,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return makeSimplePayRecurringRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const startTokenPayment = async (paymentData: TokenPaymentData) => {
|
|
46
|
+
simplepayLogger({ function: 'SimplePay/startTokenPayment', paymentData })
|
|
47
|
+
const currency = paymentData.currency || 'HUF'
|
|
48
|
+
const { MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING, SDK_VERSION } = getSimplePayConfig(currency)
|
|
49
|
+
simplepayLogger({ function: 'SimplePay/startTokenPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING })
|
|
50
|
+
|
|
51
|
+
if (!MERCHANT_KEY || !MERCHANT_ID) {
|
|
52
|
+
throw new Error(`Missing SimplePay configuration for ${currency}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const requestBody: SimplePayTokenRequestBody = {
|
|
56
|
+
salt: crypto.randomBytes(16).toString('hex'),
|
|
57
|
+
merchant: MERCHANT_ID,
|
|
58
|
+
orderRef: paymentData.orderRef,
|
|
59
|
+
currency,
|
|
60
|
+
customer: paymentData.customer,
|
|
61
|
+
customerEmail: paymentData.customerEmail,
|
|
62
|
+
language: paymentData.language || 'HU',
|
|
63
|
+
sdkVersion: SDK_VERSION,
|
|
64
|
+
methods: ['CARD'],
|
|
65
|
+
token: paymentData.token,
|
|
66
|
+
type: 'MIT',
|
|
67
|
+
threeDSReqAuthMethod: '02',
|
|
68
|
+
total: String(paymentData.total),
|
|
69
|
+
timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),
|
|
70
|
+
url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://recurring.url.to.redirect',
|
|
71
|
+
invoice: paymentData.invoice,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return makeSimplePayTokenRequest(API_URL_RECURRING, requestBody, MERCHANT_KEY)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cardCancel = async (cardId: string) => {
|
|
78
|
+
simplepayLogger({ function: 'SimplePay/cardCancel', cardId })
|
|
79
|
+
const {API_URL_CARD_CANCEL, MERCHANT_KEY, MERCHANT_ID, SDK_VERSION} = getSimplePayConfig('HUF')
|
|
80
|
+
|
|
81
|
+
if (!MERCHANT_KEY || !MERCHANT_ID) {
|
|
82
|
+
throw new Error(`Missing SimplePay configuration for HUF`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const requestBody: SimplePayCardCancelRequestBody = {
|
|
86
|
+
salt: crypto.randomBytes(16).toString('hex'),
|
|
87
|
+
cardId,
|
|
88
|
+
merchant: MERCHANT_ID,
|
|
89
|
+
sdkVersion: SDK_VERSION,
|
|
90
|
+
}
|
|
91
|
+
return makeSimplePayCardCancelRequest(API_URL_CARD_CANCEL, requestBody, MERCHANT_KEY)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { startRecurringPayment, startTokenPayment , cardCancel}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
type PaymentMethod = 'CARD' | 'WIRE'
|
|
1
|
+
export type PaymentMethod = 'CARD' | 'WIRE'
|
|
2
2
|
|
|
3
|
-
const CURRENCIES = ['HUF', 'EUR', 'USD'] as const
|
|
4
|
-
type Currency = typeof CURRENCIES[number]
|
|
3
|
+
export const CURRENCIES = ['HUF', 'EUR', 'USD'] as const
|
|
4
|
+
export type Currency = typeof CURRENCIES[number]
|
|
5
5
|
|
|
6
|
-
const LANGUAGES = [
|
|
6
|
+
export const LANGUAGES = [
|
|
7
7
|
'AR', // Arabic
|
|
8
8
|
'BG', // Bulgarian
|
|
9
9
|
'CS', // Czech
|
|
@@ -21,9 +21,9 @@ const LANGUAGES = [
|
|
|
21
21
|
'TR', // Turkish
|
|
22
22
|
'ZH', // Chinese
|
|
23
23
|
] as const
|
|
24
|
-
type Language = typeof LANGUAGES[number]
|
|
24
|
+
export type Language = typeof LANGUAGES[number]
|
|
25
25
|
|
|
26
|
-
interface PaymentData {
|
|
26
|
+
export interface PaymentData {
|
|
27
27
|
orderRef: string
|
|
28
28
|
total: number | string
|
|
29
29
|
customerEmail: string
|
|
@@ -42,7 +42,24 @@ interface PaymentData {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
export type ISO8601DateString = string
|
|
46
|
+
export interface Recurring {
|
|
47
|
+
times: number,
|
|
48
|
+
until: ISO8601DateString,
|
|
49
|
+
maxAmount: number
|
|
50
|
+
}
|
|
51
|
+
export interface RecurringPaymentData extends PaymentData {
|
|
52
|
+
customer: string,
|
|
53
|
+
recurring: Recurring
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TokenPaymentData extends Omit<PaymentData, 'method'> {
|
|
57
|
+
method: 'CARD',
|
|
58
|
+
customer: string,
|
|
59
|
+
token: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {
|
|
46
63
|
total: string
|
|
47
64
|
salt: string
|
|
48
65
|
merchant: string
|
|
@@ -52,24 +69,67 @@ interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {
|
|
|
52
69
|
url: string
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
interface
|
|
72
|
+
export interface SimplePayRecurringRequestBody extends SimplePayRequestBody {
|
|
73
|
+
customer: string
|
|
74
|
+
recurring: Recurring
|
|
75
|
+
threeDSReqAuthMethod: '02' // only registered users can use this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SimplePayTokenRequestBody extends SimplePayRequestBody {
|
|
79
|
+
customer: string
|
|
80
|
+
token: string
|
|
81
|
+
threeDSReqAuthMethod: '02' // only registered users can use this
|
|
82
|
+
type: 'MIT' // Merchant Initiated Transaction
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SimplePayCardCancelRequestBody {
|
|
86
|
+
salt: string
|
|
87
|
+
cardId: string
|
|
88
|
+
merchant: string
|
|
89
|
+
sdkVersion: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SimplePayResponse {
|
|
56
93
|
salt: string
|
|
57
94
|
merchant: string
|
|
58
95
|
orderRef: string
|
|
59
96
|
currency: Currency
|
|
60
97
|
transactionId: string
|
|
61
|
-
timeout:
|
|
98
|
+
timeout: ISO8601DateString
|
|
62
99
|
total: string
|
|
63
100
|
paymentUrl: string
|
|
64
101
|
errorCodes?: string[]
|
|
65
102
|
}
|
|
66
103
|
|
|
67
|
-
interface
|
|
104
|
+
export interface SimplePayRecurringResponse extends SimplePayResponse {
|
|
105
|
+
tokens: string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> { }
|
|
109
|
+
|
|
110
|
+
export interface SimplePayCardCancelResponse {
|
|
111
|
+
salt: string
|
|
112
|
+
merchant: string
|
|
113
|
+
cardId: string
|
|
114
|
+
status: 'DISABLED'
|
|
115
|
+
expiry: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL'
|
|
119
|
+
|
|
120
|
+
export interface SimplePayAPIResult {
|
|
68
121
|
r: number // response code
|
|
69
122
|
t: string // transaction id
|
|
70
|
-
e:
|
|
123
|
+
e: SimplePayEvents // event
|
|
71
124
|
m: string // merchant id
|
|
72
125
|
o: string // order id
|
|
73
126
|
}
|
|
74
127
|
|
|
75
|
-
export
|
|
128
|
+
export interface SimplePayResult {
|
|
129
|
+
responseCode: number,
|
|
130
|
+
transactionId: string,
|
|
131
|
+
event: SimplePayEvents,
|
|
132
|
+
merchantId: string,
|
|
133
|
+
orderRef: string,
|
|
134
|
+
tokens?: string[],
|
|
135
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import { checkSignature, generateSignature, getPaymentResponse, getSimplePayConfig, startPayment, getCurrencyFromMerchantId } from './index'
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
3
2
|
import { Currency } from './types'
|
|
3
|
+
import { checkSignature, generateSignature, getCurrencyFromMerchantId, getSimplePayConfig } from './utils'
|
|
4
4
|
|
|
5
5
|
const setEnv = () => {
|
|
6
6
|
process.env.SIMPLEPAY_MERCHANT_ID_HUF = 'testId'
|
|
@@ -14,11 +14,13 @@ const paymentData = {
|
|
|
14
14
|
customerEmail: 'test@example.com',
|
|
15
15
|
total: 1212
|
|
16
16
|
}
|
|
17
|
-
describe('SimplePay
|
|
17
|
+
describe('SimplePay Utils Tests', () => {
|
|
18
18
|
beforeEach(() => {
|
|
19
19
|
// Clear all environment variables before each test
|
|
20
|
-
delete process.env.SIMPLEPAY_MERCHANT_KEY_HUF
|
|
21
20
|
delete process.env.SIMPLEPAY_MERCHANT_ID_HUF
|
|
21
|
+
delete process.env.SIMPLEPAY_MERCHANT_KEY_HUF
|
|
22
|
+
delete process.env.SIMPLEPAY_MERCHANT_ID_EUR
|
|
23
|
+
delete process.env.SIMPLEPAY_MERCHANT_KEY_EUR
|
|
22
24
|
})
|
|
23
25
|
describe('generateSignature', () => {
|
|
24
26
|
it('should generate correct signature for sample payload from documentation', () => {
|
|
@@ -101,7 +103,6 @@ describe('SimplePay SDK Tests', () => {
|
|
|
101
103
|
m: 'testId',
|
|
102
104
|
o: 'ORDER123'
|
|
103
105
|
}
|
|
104
|
-
const encodedResponse = Buffer.from(JSON.stringify(mockResponse)).toString('base64')
|
|
105
106
|
|
|
106
107
|
describe('getCurrencyFromMerchantId', () => {
|
|
107
108
|
it('should return correct currency for merchantId', () => {
|
|
@@ -110,77 +111,4 @@ describe('SimplePay SDK Tests', () => {
|
|
|
110
111
|
expect(currency).toBe('HUF')
|
|
111
112
|
})
|
|
112
113
|
})
|
|
113
|
-
|
|
114
|
-
describe('getPaymentResponse', () => {
|
|
115
|
-
it.only('TODO: test for invalid response', () => {
|
|
116
|
-
process.env.SIMPLEPAY_MERCHANT_ID_HUF = 'P085602'
|
|
117
|
-
process.env.SIMPLEPAY_MERCHANT_KEY_HUF = 'QnJvDEEj51jdDEa1P125258p8g5gU383'
|
|
118
|
-
const r = 'eyJyIjowLCJ0Ijo1MDQyNDU5MDIsImUiOiJTVUNDRVNTIiwibSI6IlAwODU2MDIiLCJvIjoiMTczMzQ5MzQxMjA1NyJ9'
|
|
119
|
-
const s = 'apIUA%2B8cislIlgYUB7lSpoasi%2BCIRg1SMS5zVbEytnEnxcvdx%2BWDXkAznnseADiI'
|
|
120
|
-
const result = getPaymentResponse(r, s)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('should correctly decode and parse valid response', () => {
|
|
124
|
-
setEnv()
|
|
125
|
-
const r = 'eyJyIjowLCJ0Ijo1MDQyMzM4ODEsImUiOiJTVUNDRVNTIiwibSI6Im1lcmNoYW50RXVyb0lkIiwibyI6ImMtMS1ldXIifQ=='
|
|
126
|
-
// { r: 0, t: 504233881, e: 'SUCCESS', m: 'merchantEuroId', o: 'c-1-eur' }
|
|
127
|
-
const s = 'WWx4cnBEYThqRi94VkIvck5zRUpvRnhPb0hRK0NpemlCbVdTTUloWVdIU0NKbXZMb2M2a3pBaVpQbVlEVTh6Ng=='
|
|
128
|
-
const result = getPaymentResponse(r, s)
|
|
129
|
-
expect(result).toEqual({
|
|
130
|
-
responseCode: 0,
|
|
131
|
-
transactionId: 504233881,
|
|
132
|
-
event: 'SUCCESS',
|
|
133
|
-
merchantId: 'merchantEuroId',
|
|
134
|
-
orderRef: 'c-1-eur'
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('should throw error for invalid signature', () => {
|
|
139
|
-
setEnv()
|
|
140
|
-
expect(() =>
|
|
141
|
-
getPaymentResponse(encodedResponse, 'invalid-signature')
|
|
142
|
-
).toThrow('Invalid response signature')
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('startPayment', () => {
|
|
147
|
-
it('should throw error when merchant configuration is missing', async () => {
|
|
148
|
-
await expect(startPayment(paymentData)).rejects.toThrow('Missing SimplePay configuration')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('should handle API errors correctly', async () => {
|
|
152
|
-
setEnv()
|
|
153
|
-
|
|
154
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
155
|
-
ok: true,
|
|
156
|
-
headers: {
|
|
157
|
-
get: vi.fn().mockReturnValue('mockSignature')
|
|
158
|
-
},
|
|
159
|
-
text: vi.fn().mockResolvedValue(JSON.stringify({
|
|
160
|
-
transactionId: '123456',
|
|
161
|
-
total: '1212',
|
|
162
|
-
merchant: 'testId'
|
|
163
|
-
}))
|
|
164
|
-
}) as unknown as typeof fetch
|
|
165
|
-
await expect(startPayment(paymentData)).rejects.toThrow('Invalid response signature')
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should successfully start CARD, HUF, HU payment when API returns valid response', async () => {
|
|
169
|
-
setEnv()
|
|
170
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
171
|
-
ok: true,
|
|
172
|
-
headers: {
|
|
173
|
-
get: vi.fn().mockReturnValue('bxSwUc0qn0oABSRcq9uawF6zncFBhRk/AbO4HznYR9Pt5SjocyxAD+9Q4bE44h0J')
|
|
174
|
-
},
|
|
175
|
-
text: vi.fn().mockResolvedValue(JSON.stringify({
|
|
176
|
-
transactionId: '123456',
|
|
177
|
-
total: '1212',
|
|
178
|
-
merchant: 'testId'
|
|
179
|
-
}))
|
|
180
|
-
}) as unknown as typeof fetch
|
|
181
|
-
|
|
182
|
-
await expect(startPayment(paymentData)).resolves.toBeDefined()
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
114
|
})
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { CURRENCIES, Currency, ISO8601DateString, SimplePayAPIResult, SimplePayCardCancelRequestBody, SimplePayCardCancelResponse, SimplePayRecurringRequestBody, SimplePayRecurringResponse, SimplePayRequestBody, SimplePayResponse, SimplePayResult, SimplePayTokenRequestBody, SimplePayTokenResponse } from "./types"
|
|
3
|
+
|
|
4
|
+
export const simplepayLogger = (...args: any[]) => {
|
|
5
|
+
if (process.env.SIMPLEPAY_LOGGER !== 'true') {
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
console.log('👉 ', ...args)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getSimplePayConfig = (currency: Currency) => {
|
|
13
|
+
if (!CURRENCIES.includes(currency)) {
|
|
14
|
+
throw new Error(`Unsupported currency: ${currency}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'
|
|
18
|
+
const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'
|
|
19
|
+
const SDK_VERSION = 'SimplePayV2.1_Rrd_0.6.1'
|
|
20
|
+
const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]
|
|
21
|
+
const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]
|
|
22
|
+
|
|
23
|
+
const API_URL = process.env.SIMPLEPAY_PRODUCTION === 'true' ? SIMPLEPAY_API_URL : SIMPLEPAY_SANDBOX_URL
|
|
24
|
+
const API_URL_PAYMENT = API_URL + '/start'
|
|
25
|
+
const API_URL_RECURRING = API_URL + '/dorecurring'
|
|
26
|
+
const API_URL_CARD_CANCEL = API_URL + '/cancelcard'
|
|
27
|
+
return {
|
|
28
|
+
MERCHANT_KEY,
|
|
29
|
+
MERCHANT_ID,
|
|
30
|
+
API_URL_PAYMENT,
|
|
31
|
+
API_URL_RECURRING,
|
|
32
|
+
API_URL_CARD_CANCEL,
|
|
33
|
+
SDK_VERSION
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// escaping slashes for the request body to prevent strange SimplePay API errors (eg Missing Signature)
|
|
38
|
+
export const prepareRequestBody = (body: any) =>
|
|
39
|
+
JSON.stringify(body).replace(/\//g, '\\/')
|
|
40
|
+
|
|
41
|
+
export const generateSignature = (body: string, merchantKey: string) => {
|
|
42
|
+
const hmac = crypto.createHmac('sha384', merchantKey.trim())
|
|
43
|
+
hmac.update(body, 'utf8')
|
|
44
|
+
return hmac.digest('base64')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const checkSignature = (responseText: string, signature: string, merchantKey: string) =>
|
|
48
|
+
signature === generateSignature(responseText, merchantKey)
|
|
49
|
+
|
|
50
|
+
export const toISO8601DateString = (date: Date): ISO8601DateString => date.toISOString().replace(/\.\d{3}Z$/, '+00:00')
|
|
51
|
+
|
|
52
|
+
export const getCurrencyFromMerchantId = (merchantId: string) => {
|
|
53
|
+
const currency = Object.entries(process.env)
|
|
54
|
+
.find(([key, value]) =>
|
|
55
|
+
key.startsWith('SIMPLEPAY_MERCHANT_ID_') && value === merchantId
|
|
56
|
+
)?.[0]?.replace('SIMPLEPAY_MERCHANT_ID_', '') as Currency
|
|
57
|
+
if (!currency) {
|
|
58
|
+
throw new Error(`Merchant id not found in the environment: ${merchantId}`)
|
|
59
|
+
}
|
|
60
|
+
return currency
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const makeSimplePayRequest = async (apiUrl: string, requestBody: SimplePayRequestBody, merchantKey: string) => {
|
|
64
|
+
return makeRequest(apiUrl, requestBody, merchantKey, 'oneTime') as Promise<SimplePayResponse>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const makeSimplePayRecurringRequest = async (apiUrl: string, requestBody: SimplePayRecurringRequestBody, merchantKey: string) => {
|
|
68
|
+
return makeRequest(apiUrl, requestBody, merchantKey, 'recurring') as Promise<SimplePayRecurringResponse>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const makeSimplePayTokenRequest = async (apiUrl: string, requestBody: SimplePayTokenRequestBody, merchantKey: string) => {
|
|
72
|
+
return makeRequest(apiUrl, requestBody, merchantKey, 'token') as Promise<SimplePayTokenResponse>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const makeSimplePayCardCancelRequest = async (apiUrl: string, requestBody: SimplePayCardCancelRequestBody, merchantKey: string) => {
|
|
76
|
+
return makeRequest(apiUrl, requestBody, merchantKey, 'cardCancel') as Promise<SimplePayCardCancelResponse>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const makeRequest = async (apiUrl: string, requestBody: SimplePayRequestBody | SimplePayRecurringRequestBody | SimplePayTokenRequestBody | SimplePayCardCancelRequestBody, merchantKey: string, type: 'oneTime' | 'recurring' | 'token' | 'cardCancel') => {
|
|
80
|
+
const bodyString = prepareRequestBody(requestBody)
|
|
81
|
+
const signature = generateSignature(bodyString, merchantKey)
|
|
82
|
+
simplepayLogger({ function: `SimplePay/makeRequest/${type}`, bodyString, signature })
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(apiUrl, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Signature': signature,
|
|
90
|
+
},
|
|
91
|
+
body: bodyString,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
simplepayLogger({ function: `SimplePay/makeRequest/${type}`, response })
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`SimplePay API error: ${response.status}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const responseSignature = response.headers.get('Signature')
|
|
101
|
+
simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseSignature })
|
|
102
|
+
if (!responseSignature) {
|
|
103
|
+
throw new Error('Missing response signature')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const responseText = await response.text()
|
|
107
|
+
const responseJSON = JSON.parse(responseText) as { errorCodes?: string[] }
|
|
108
|
+
simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseText, responseJSON })
|
|
109
|
+
|
|
110
|
+
if (responseJSON.errorCodes) {
|
|
111
|
+
throw new Error(`SimplePay API error: ${responseJSON.errorCodes}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!checkSignature(responseText, responseSignature, merchantKey)) {
|
|
115
|
+
throw new Error('Invalid response signature')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return responseJSON
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const getPaymentResponse = (r: string, signature: string) => {
|
|
126
|
+
simplepayLogger({ function: 'SimplePay/getPaymentResponse', r, signature })
|
|
127
|
+
signature = decodeURIComponent(signature)
|
|
128
|
+
const rDecoded = Buffer.from(r, 'base64').toString('utf-8')
|
|
129
|
+
const rDecodedJSON = JSON.parse(rDecoded) as SimplePayAPIResult
|
|
130
|
+
const currency = getCurrencyFromMerchantId(rDecodedJSON.m)
|
|
131
|
+
const { MERCHANT_KEY } = getSimplePayConfig(currency as Currency)
|
|
132
|
+
|
|
133
|
+
if (!checkSignature(rDecoded, signature, MERCHANT_KEY || '')) {
|
|
134
|
+
simplepayLogger({ function: 'SimplePay/getPaymentResponse', rDecoded, signature })
|
|
135
|
+
throw new Error('Invalid response signature')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const responseJson = JSON.parse(rDecoded)
|
|
139
|
+
const response: SimplePayResult = {
|
|
140
|
+
responseCode: responseJson.r,
|
|
141
|
+
transactionId: responseJson.t,
|
|
142
|
+
event: responseJson.e,
|
|
143
|
+
merchantId: responseJson.m,
|
|
144
|
+
orderRef: responseJson.o,
|
|
145
|
+
tokens: responseJson.tokens,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return response
|
|
149
|
+
}
|