simplepay-js-sdk 0.11.2 → 0.12.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.
package/README.md CHANGED
@@ -95,12 +95,45 @@ A `response` a következő tulajdonságokkal rendelkezik:
95
95
  #### IPN végpont
96
96
 
97
97
  A SimplePay `POST` kérést küld az IPN URL-re, és válaszolnunk kell rá.
98
- Ennél a végpontnál a következőket kell tenned:
98
+
99
+ **Ajánlott megközelítés a `handleIpnRequest` függvénnyel:**
100
+
101
+ ```typescript
102
+ import { handleIpnRequest } from 'simplepay-js-sdk'
103
+
104
+ // Az IPN végpont kezelőjében (pl. Express, Next.js, stb.)
105
+ const ipnBody = await request.text() // A nyers body stringként (FONTOS: használd a .text()-et, ne a JSON.parse()-t)
106
+ const incomingSignature = request.headers.get('Signature')
107
+ const { MERCHANT_KEY } = getSimplePayConfig('HUF') // vagy a te valutád
108
+
109
+ const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
110
+
111
+ // KRITIKUS: A responseBody-t pontosan úgy küldd el, ahogy visszaadódott, NE módosítsd!
112
+ // NE:
113
+ // - Formázd újra vagy "szépítsd" a JSON-t
114
+ // - Parse-old és stringify-zd újra a JSON-t
115
+ // - Adj hozzá szóközöket vagy formázást
116
+ // Bármilyen módosítás érvényteleníti az aláírást!
117
+
118
+ // Válasz küldése HTTP 200 státusszal
119
+ return new Response(responseBody, {
120
+ status: 200,
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ 'Signature': signature
124
+ }
125
+ })
126
+ ```
127
+
128
+ **Manuális megközelítés:**
129
+
130
+ Ha manuálisan szeretnéd kezelni:
99
131
 
100
132
  - ellenőrizd az aláírás érvényességét - használd a `checkSignature(ipnBody, signatureHeader, SIMPLEPAY_MERCHANT_KEY_HUF)` függvényt
101
- - adj hozzá egy `receiveDate` tulajdonságot a kapott JSON-hoz
133
+ - adj hozzá egy `receiveDate` tulajdonságot a kapott JSON-hoz (ISO 8601 formátumban, pl. `2025-10-06T07:00:34+02:00`)
102
134
  - számítsd ki az új aláírást - használd a `generateSignature(responseText, SIMPLEPAY_MERCHANT_KEY_HUF)` függvényt
103
- - küldd el a `response`-t az új `signature`-rel
135
+ - küldd el a `response`-t az új `signature`-rel a HTTP fejlécben (ne a JSON body-ban)
136
+ - **Fontos**: A válasz JSON-nak tömörnek kell lennie (szóközmentes). A `JSON.stringify()` alapból tömör JSON-t ad vissza.
104
137
 
105
138
  ### Ismétlődő fizetés
106
139
 
package/dist/index.d.ts CHANGED
@@ -10,6 +10,50 @@ export declare const generateSignature: (body: string, merchantKey: string) => s
10
10
 
11
11
  export declare const getPaymentResponse: (r: string, signature: string) => SimplePayResult;
12
12
 
13
+ /**
14
+ * Handles IPN (Instant Payment Notification) request and generates response
15
+ *
16
+ * This function implements the IPN flow according to SimplePay requirements:
17
+ * 1. Validates the incoming signature
18
+ * 2. Adds receiveDate property to the response (preserving original field order and data types)
19
+ * 3. Generates response signature
20
+ *
21
+ * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.
22
+ * Do NOT:
23
+ * - Re-format or pretty-print the JSON
24
+ * - Parse and re-stringify the JSON
25
+ * - Add any whitespace or formatting
26
+ * - Modify the Content-Type header (must be 'application/json')
27
+ *
28
+ * The signature is calculated on the exact string that will be sent in the HTTP body.
29
+ * Any modification to the responseBody will invalidate the signature.
30
+ *
31
+ * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
32
+ * @param incomingSignature - The signature from the 'Signature' HTTP header
33
+ * @param merchantKey - The merchant secret key for signature validation and generation
34
+ * @returns Object containing the response JSON string and signature to send back
35
+ *
36
+ * @example
37
+ * // In your IPN endpoint handler:
38
+ * const ipnBody = await request.text() // Get raw body as string (IMPORTANT: use .text(), not JSON.parse())
39
+ * const incomingSignature = request.headers.get('Signature')
40
+ * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
41
+ *
42
+ * // Send response with HTTP 200 status
43
+ * // CRITICAL: Send responseBody exactly as returned, do not modify it!
44
+ * return new Response(responseBody, {
45
+ * status: 200,
46
+ * headers: {
47
+ * 'Content-Type': 'application/json',
48
+ * 'Signature': signature
49
+ * }
50
+ * })
51
+ */
52
+ export declare const handleIpnRequest: (ipnBody: string, incomingSignature: string, merchantKey: string) => {
53
+ responseBody: string;
54
+ signature: string;
55
+ };
56
+
13
57
  declare type ISO8601DateString = string;
14
58
 
15
59
  export declare type Language = typeof LANGUAGES[number];
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import S from "crypto";
1
+ import l from "crypto";
2
2
  const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
3
3
  process.env.SIMPLEPAY_LOGGER === "true" && console.log("👉 ", ...e);
4
4
  }, E = (e) => {
5
5
  if (!d.includes(e))
6
6
  throw new Error(`Unsupported currency: ${e}`);
7
- const n = "https://secure.simplepay.hu/payment/v2", r = "https://sandbox.simplepay.hu/payment/v2", t = "SimplePay_Rrd_0.11.1", s = process.env[`SIMPLEPAY_MERCHANT_KEY_${e}`], c = process.env[`SIMPLEPAY_MERCHANT_ID_${e}`], o = process.env.SIMPLEPAY_PRODUCTION === "true" ? n : r, u = o + "/start", R = o + "/dorecurring", a = o + "/cardcancel";
7
+ const n = "https://secure.simplepay.hu/payment/v2", r = "https://sandbox.simplepay.hu/payment/v2", t = "SimplePay_Rrd_0.12.0", s = process.env[`SIMPLEPAY_MERCHANT_KEY_${e}`], c = process.env[`SIMPLEPAY_MERCHANT_ID_${e}`], o = process.env.SIMPLEPAY_PRODUCTION === "true" ? n : r, u = o + "/start", R = o + "/dorecurring", a = o + "/cardcancel";
8
8
  return {
9
9
  MERCHANT_KEY: s,
10
10
  MERCHANT_ID: c,
@@ -14,16 +14,16 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
14
14
  SDK_VERSION: t
15
15
  };
16
16
  }, g = (e) => JSON.stringify(e).replace(/\//g, "\\/"), m = (e, n) => {
17
- const r = S.createHmac("sha384", n.trim());
17
+ const r = l.createHmac("sha384", n.trim());
18
18
  return r.update(e, "utf8"), r.digest("base64");
19
- }, _ = (e, n, r) => n === m(e, r), P = (e) => e.toISOString().replace(/\.\d{3}Z$/, "+00:00"), A = (e) => {
19
+ }, _ = (e, n, r) => n === m(e, r), S = (e) => e.toISOString().replace(/\.\d{3}Z$/, "+00:00"), p = (e) => {
20
20
  const n = Object.entries(process.env).find(
21
21
  ([r, t]) => r.startsWith("SIMPLEPAY_MERCHANT_ID_") && t === e
22
22
  )?.[0]?.replace("SIMPLEPAY_MERCHANT_ID_", "");
23
23
  if (!n)
24
24
  throw new Error(`Merchant id not found in the environment: ${e}`);
25
25
  return n;
26
- }, p = async (e, n, r) => l(e, n, r, "oneTime"), I = async (e, n, r) => l(e, n, r, "recurring"), f = async (e, n, r) => l(e, n, r, "token"), h = async (e, n, r) => l(e, n, r, "cancelCard"), l = async (e, n, r, t) => {
26
+ }, A = async (e, n, r) => P(e, n, r, "oneTime"), I = async (e, n, r) => P(e, n, r, "recurring"), f = async (e, n, r) => P(e, n, r, "token"), h = async (e, n, r) => P(e, n, r, "cancelCard"), P = async (e, n, r, t) => {
27
27
  const s = g(n), c = m(s, r);
28
28
  i({ function: `SimplePay/makeRequest/${t}`, bodyString: s, signature: c });
29
29
  try {
@@ -51,7 +51,7 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
51
51
  }
52
52
  }, U = (e, n) => {
53
53
  i({ function: "SimplePay/getPaymentResponse", r: e, signature: n }), n = decodeURIComponent(n);
54
- const r = Buffer.from(e, "base64").toString("utf-8"), t = JSON.parse(r), s = A(t.m), { MERCHANT_KEY: c } = E(s);
54
+ const r = Buffer.from(e, "base64").toString("utf-8"), t = JSON.parse(r), s = p(t.m), { MERCHANT_KEY: c } = E(s);
55
55
  if (!_(r, n, c || ""))
56
56
  throw i({ function: "SimplePay/getPaymentResponse", rDecoded: r, signature: n }), new Error("Invalid response signature");
57
57
  const o = JSON.parse(r);
@@ -63,13 +63,22 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
63
63
  orderRef: o.o,
64
64
  tokens: o.tokens
65
65
  };
66
- }, w = async (e, n = {}) => {
66
+ }, w = (e, n, r) => {
67
+ if (i({ function: "SimplePay/handleIpnRequest", ipnBody: e, incomingSignature: n }), !_(e, n, r))
68
+ throw new Error("Invalid IPN request signature");
69
+ JSON.parse(e);
70
+ const t = S(/* @__PURE__ */ new Date()), s = e.replace("}", `,"receiveDate":"${t}"}`), c = m(s, r);
71
+ return i({ function: "SimplePay/handleIpnRequest", responseBody: s, responseSignature: c }), {
72
+ responseBody: s,
73
+ signature: c
74
+ };
75
+ }, L = async (e, n = {}) => {
67
76
  i({ function: "SimplePay/startPayment", paymentData: e });
68
77
  const r = e.currency || "HUF", { MERCHANT_KEY: t, MERCHANT_ID: s, API_URL_PAYMENT: c, SDK_VERSION: o } = E(r);
69
78
  if (i({ function: "SimplePay/startPayment", MERCHANT_KEY: t, MERCHANT_ID: s, API_URL_PAYMENT: c }), !t || !s)
70
79
  throw new Error(`Missing SimplePay configuration for ${r}`);
71
80
  const u = {
72
- salt: S.randomBytes(16).toString("hex"),
81
+ salt: l.randomBytes(16).toString("hex"),
73
82
  merchant: s,
74
83
  orderRef: e.orderRef,
75
84
  currency: r.replace("_SZEP", ""),
@@ -78,18 +87,18 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
78
87
  sdkVersion: o,
79
88
  methods: [e.method || "CARD"],
80
89
  total: String(e.total),
81
- timeout: P(new Date(Date.now() + 1800 * 1e3)),
90
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
82
91
  url: n.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
83
92
  invoice: e.invoice
84
93
  };
85
- return p(c, u, t);
86
- }, y = 6, C = new Date(Date.now() + y * 30 * 24 * 60 * 60 * 1e3), M = 12e3, N = 3, L = async (e) => {
94
+ return A(c, u, t);
95
+ }, y = 6, C = new Date(Date.now() + y * 30 * 24 * 60 * 60 * 1e3), M = 12e3, N = 3, H = async (e) => {
87
96
  i({ function: "SimplePay/startRecurringPayment", paymentData: e });
88
97
  const n = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_PAYMENT: s, SDK_VERSION: c } = E(n);
89
98
  if (i({ function: "SimplePay/startRecurringPayment", MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_PAYMENT: s }), !r || !t)
90
99
  throw new Error(`Missing SimplePay configuration for ${n}`);
91
100
  const o = {
92
- salt: S.randomBytes(16).toString("hex"),
101
+ salt: l.randomBytes(16).toString("hex"),
93
102
  merchant: t,
94
103
  orderRef: e.orderRef,
95
104
  currency: n.replace("_SZEP", ""),
@@ -100,23 +109,23 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
100
109
  methods: ["CARD"],
101
110
  recurring: {
102
111
  times: e.recurring.times || N,
103
- until: e.recurring.until || P(C),
112
+ until: e.recurring.until || S(C),
104
113
  maxAmount: e.recurring.maxAmount || M
105
114
  },
106
115
  threeDSReqAuthMethod: "02",
107
116
  total: String(e.total),
108
- timeout: P(new Date(Date.now() + 1800 * 1e3)),
117
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
109
118
  url: process.env.SIMPLEPAY_REDIRECT_URL || "http://url.to.redirect",
110
119
  invoice: e.invoice
111
120
  };
112
121
  return I(s, o, r);
113
- }, H = async (e) => {
122
+ }, k = async (e) => {
114
123
  i({ function: "SimplePay/startTokenPayment", paymentData: e });
115
124
  const n = e.currency || "HUF", { MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_RECURRING: s, SDK_VERSION: c } = E(n);
116
125
  if (i({ function: "SimplePay/startTokenPayment", MERCHANT_KEY: r, MERCHANT_ID: t, API_URL_RECURRING: s }), !r || !t)
117
126
  throw new Error(`Missing SimplePay configuration for ${n}`);
118
127
  const o = {
119
- salt: S.randomBytes(16).toString("hex"),
128
+ salt: l.randomBytes(16).toString("hex"),
120
129
  merchant: t,
121
130
  orderRef: e.orderRef,
122
131
  currency: n.replace("_SZEP", ""),
@@ -129,18 +138,18 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
129
138
  type: "MIT",
130
139
  threeDSReqAuthMethod: "02",
131
140
  total: String(e.total),
132
- timeout: P(new Date(Date.now() + 1800 * 1e3)),
141
+ timeout: S(new Date(Date.now() + 1800 * 1e3)),
133
142
  url: process.env.SIMPLEPAY_REDIRECT_URL || "http://recurring.url.to.redirect",
134
143
  invoice: e.invoice
135
144
  };
136
145
  return f(s, o, r);
137
- }, k = async (e) => {
146
+ }, v = async (e) => {
138
147
  i({ function: "SimplePay/cancelCard", cardId: e });
139
148
  const { API_URL_CARD_CANCEL: n, MERCHANT_KEY: r, MERCHANT_ID: t, SDK_VERSION: s } = E("HUF");
140
149
  if (!r || !t)
141
150
  throw new Error("Missing SimplePay configuration for HUF");
142
151
  const c = {
143
- salt: S.randomBytes(16).toString("hex"),
152
+ salt: l.randomBytes(16).toString("hex"),
144
153
  cardId: e,
145
154
  merchant: t,
146
155
  sdkVersion: s
@@ -148,13 +157,14 @@ const d = ["HUF", "HUF_SZEP", "EUR", "USD"], i = (...e) => {
148
157
  return h(n, c, r);
149
158
  };
150
159
  export {
151
- k as cancelCard,
160
+ v as cancelCard,
152
161
  _ as checkSignature,
153
162
  m as generateSignature,
154
163
  U as getPaymentResponse,
155
- w as startPayment,
156
- L as startRecurringPayment,
157
- H as startTokenPayment,
158
- P as toISO8601DateString
164
+ w as handleIpnRequest,
165
+ L as startPayment,
166
+ H as startRecurringPayment,
167
+ k as startTokenPayment,
168
+ S as toISO8601DateString
159
169
  };
160
170
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/types.ts","../src/utils.ts","../src/oneTime.ts","../src/recurring.ts"],"sourcesContent":["export type PaymentMethod = 'CARD' | 'WIRE'\n\nexport const CURRENCIES = ['HUF', 'HUF_SZEP', 'EUR', 'USD'] as const\nexport type Currency = typeof CURRENCIES[number]\n\nexport const LANGUAGES = [\n 'AR', // Arabic\n 'BG', // Bulgarian\n 'CS', // Czech\n 'DE', // German\n 'EN', // English\n 'ES', // Spanish\n 'FR', // French\n 'IT', // Italian\n 'HR', // Croatian\n 'HU', // Hungarian\n 'PL', // Polish\n 'RO', // Romanian\n 'RU', // Russian\n 'SK', // Slovak\n 'TR', // Turkish\n 'ZH', // Chinese\n] as const\nexport type Language = typeof LANGUAGES[number]\n\nexport interface PaymentData {\n orderRef: string\n total: number | string\n customerEmail: string\n currency?: Currency\n language?: Language\n method?: PaymentMethod\n invoice?: {\n name: string\n country: string\n state: string\n city: string\n zip: string\n address: string\n address2?: string\n phone?: string\n }\n}\n\nexport interface PaymentConfig {\n redirectUrl?: string\n}\n\nexport type ISO8601DateString = string\nexport interface Recurring {\n times: number,\n until: ISO8601DateString,\n maxAmount: number\n}\nexport interface RecurringPaymentData extends PaymentData {\n customer: string,\n recurring: Recurring\n}\n\nexport interface TokenPaymentData extends Omit<PaymentData, 'method'> {\n method: 'CARD',\n customer: string,\n token: string\n}\n\nexport interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {\n total: string\n salt: string\n merchant: string\n sdkVersion: string\n methods: PaymentMethod[]\n timeout: string\n url: string\n}\n\nexport interface SimplePayRecurringRequestBody extends SimplePayRequestBody {\n customer: string\n recurring: Recurring\n threeDSReqAuthMethod: '02' // only registered users can use this\n}\n\nexport interface SimplePayTokenRequestBody extends SimplePayRequestBody {\n customer: string\n token: string\n threeDSReqAuthMethod: '02' // only registered users can use this\n type: 'MIT' // Merchant Initiated Transaction\n}\n\nexport interface SimplePayCancelCardRequestBody {\n salt: string\n cardId: string\n merchant: string\n sdkVersion: string\n}\n\nexport interface SimplePayResponse {\n salt: string\n merchant: string\n orderRef: string\n currency: Currency\n transactionId: string\n timeout: ISO8601DateString\n total: string\n paymentUrl: string\n errorCodes?: string[]\n}\n\nexport interface SimplePayRecurringResponse extends SimplePayResponse {\n tokens: string[]\n}\n\nexport interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> { }\n\nexport interface SimplePayCancelCardResponse {\n salt: string\n merchant: string\n cardId: string\n status: 'DISABLED'\n expiry: string\n}\n\nexport type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL'\n\nexport interface SimplePayAPIResult {\n r: number // response code\n t: string // transaction id\n e: SimplePayEvents // event\n m: string // merchant id\n o: string // order id\n}\n\nexport interface SimplePayResult {\n responseCode: number,\n transactionId: string,\n event: SimplePayEvents,\n merchantId: string,\n orderRef: string,\n tokens?: string[],\n}\n","import crypto from 'crypto'\nimport { CURRENCIES, Currency, ISO8601DateString, SimplePayAPIResult, SimplePayCancelCardRequestBody, SimplePayCancelCardResponse, SimplePayRecurringRequestBody, SimplePayRecurringResponse, SimplePayRequestBody, SimplePayResponse, SimplePayResult, SimplePayTokenRequestBody, SimplePayTokenResponse } from \"./types\"\n\nexport const simplepayLogger = (...args: any[]) => {\n if (process.env.SIMPLEPAY_LOGGER !== 'true') {\n return\n }\n\n console.log('👉 ', ...args)\n}\n\nexport const getSimplePayConfig = (currency: Currency) => {\n if (!CURRENCIES.includes(currency)) {\n throw new Error(`Unsupported currency: ${currency}`)\n }\n\n const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'\n const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'\n const SDK_VERSION = 'SimplePay_Rrd_0.11.1'\n const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]\n const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]\n\n const API_URL = process.env.SIMPLEPAY_PRODUCTION === 'true' ? SIMPLEPAY_API_URL : SIMPLEPAY_SANDBOX_URL\n const API_URL_PAYMENT = API_URL + '/start'\n const API_URL_RECURRING = API_URL + '/dorecurring'\n const API_URL_CARD_CANCEL = API_URL + '/cardcancel'\n return {\n MERCHANT_KEY,\n MERCHANT_ID,\n API_URL_PAYMENT,\n API_URL_RECURRING,\n API_URL_CARD_CANCEL,\n SDK_VERSION\n }\n}\n\n// escaping slashes for the request body to prevent strange SimplePay API errors (eg Missing Signature)\nexport const prepareRequestBody = (body: any) =>\n JSON.stringify(body).replace(/\\//g, '\\\\/')\n\nexport const generateSignature = (body: string, merchantKey: string) => {\n const hmac = crypto.createHmac('sha384', merchantKey.trim())\n hmac.update(body, 'utf8')\n return hmac.digest('base64')\n}\n\nexport const checkSignature = (responseText: string, signature: string, merchantKey: string) =>\n signature === generateSignature(responseText, merchantKey)\n\nexport const toISO8601DateString = (date: Date): ISO8601DateString => date.toISOString().replace(/\\.\\d{3}Z$/, '+00:00')\n\nexport const getCurrencyFromMerchantId = (merchantId: string) => {\n const currency = Object.entries(process.env)\n .find(([key, value]) =>\n key.startsWith('SIMPLEPAY_MERCHANT_ID_') && value === merchantId\n )?.[0]?.replace('SIMPLEPAY_MERCHANT_ID_', '') as Currency\n\n if (!currency) {\n throw new Error(`Merchant id not found in the environment: ${merchantId}`)\n }\n\n return currency\n}\n\nexport const makeSimplePayRequest = async (apiUrl: string, requestBody: SimplePayRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'oneTime') as Promise<SimplePayResponse>\n}\n\nexport const makeSimplePayRecurringRequest = async (apiUrl: string, requestBody: SimplePayRecurringRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'recurring') as Promise<SimplePayRecurringResponse>\n}\n\nexport const makeSimplePayTokenRequest = async (apiUrl: string, requestBody: SimplePayTokenRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'token') as Promise<SimplePayTokenResponse>\n}\n\nexport const makeSimplePayCancelCardRequest = async (apiUrl: string, requestBody: SimplePayCancelCardRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'cancelCard') as Promise<SimplePayCancelCardResponse>\n}\n\nconst makeRequest = async (apiUrl: string, requestBody: SimplePayRequestBody | SimplePayRecurringRequestBody | SimplePayTokenRequestBody | SimplePayCancelCardRequestBody, merchantKey: string, type: 'oneTime' | 'recurring' | 'token' | 'cancelCard') => {\n const bodyString = prepareRequestBody(requestBody)\n const signature = generateSignature(bodyString, merchantKey)\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, bodyString, signature })\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Signature': signature,\n },\n body: bodyString,\n })\n\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, response })\n\n if (!response.ok) {\n throw new Error(`SimplePay API error: ${response.status}`)\n }\n\n const responseSignature = response.headers.get('Signature')\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseSignature })\n if (!responseSignature) {\n throw new Error('Missing response signature')\n }\n\n const responseText = await response.text()\n const responseJSON = JSON.parse(responseText) as { errorCodes?: string[] }\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseText, responseJSON })\n\n if (responseJSON.errorCodes) {\n throw new Error(`SimplePay API error: ${responseJSON.errorCodes}`)\n }\n\n if (!checkSignature(responseText, responseSignature, merchantKey)) {\n throw new Error('Invalid response signature')\n }\n\n return responseJSON\n\n } catch (error) {\n throw error\n }\n}\n\nexport const getPaymentResponse = (r: string, signature: string) => {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', r, signature })\n signature = decodeURIComponent(signature)\n const rDecoded = Buffer.from(r, 'base64').toString('utf-8')\n const rDecodedJSON = JSON.parse(rDecoded) as SimplePayAPIResult\n const currency = getCurrencyFromMerchantId(rDecodedJSON.m)\n const { MERCHANT_KEY } = getSimplePayConfig(currency as Currency)\n\n if (!checkSignature(rDecoded, signature, MERCHANT_KEY || '')) {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', rDecoded, signature })\n throw new Error('Invalid response signature')\n }\n\n const responseJson = JSON.parse(rDecoded)\n const response: SimplePayResult = {\n responseCode: responseJson.r,\n transactionId: responseJson.t,\n event: responseJson.e,\n merchantId: responseJson.m,\n orderRef: responseJson.o,\n tokens: responseJson.tokens,\n }\n\n return response\n}\n","import crypto from 'crypto'\nimport { Currency, PaymentConfig, PaymentData, SimplePayRequestBody } from './types'\nimport { simplepayLogger, getSimplePayConfig, toISO8601DateString, makeSimplePayRequest } from './utils'\n\nconst startPayment = async (paymentData: PaymentData, config: PaymentConfig = {}) => {\n simplepayLogger({ function: 'SimplePay/startPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: [paymentData.method || 'CARD'],\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: config.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nexport { startPayment }\n","import crypto from 'crypto'\nimport { SimplePayRecurringRequestBody, RecurringPaymentData, TokenPaymentData, SimplePayTokenRequestBody, SimplePayCancelCardRequestBody, Currency} from './types'\nimport { getSimplePayConfig, simplepayLogger, toISO8601DateString, makeSimplePayTokenRequest, makeSimplePayRecurringRequest, makeSimplePayRequest, makeSimplePayCancelCardRequest} from './utils'\n\nconst INTERVAL_IN_MONTHS = 6\nconst DEFAULT_UNTIL = new Date(Date.now() + INTERVAL_IN_MONTHS * 30 * 24 * 60 * 60 * 1000)\nconst DEFAULT_MAX_AMOUNT = 12000\nconst DEFAULT_TIMES = 3\n\nconst startRecurringPayment = async (paymentData: RecurringPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRecurringRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n recurring: {\n times: paymentData.recurring.times || DEFAULT_TIMES,\n until: paymentData.recurring.until || toISO8601DateString(DEFAULT_UNTIL),\n maxAmount: paymentData.recurring.maxAmount || DEFAULT_MAX_AMOUNT\n },\n threeDSReqAuthMethod: '02', \n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRecurringRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nconst startTokenPayment = async (paymentData: TokenPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startTokenPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startTokenPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayTokenRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n token: paymentData.token,\n type: 'MIT',\n threeDSReqAuthMethod: '02',\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://recurring.url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayTokenRequest(API_URL_RECURRING, requestBody, MERCHANT_KEY)\n}\n\nconst cancelCard = async (cardId: string) => {\n simplepayLogger({ function: 'SimplePay/cancelCard', cardId })\n const {API_URL_CARD_CANCEL, MERCHANT_KEY, MERCHANT_ID, SDK_VERSION} = getSimplePayConfig('HUF')\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for HUF`)\n }\n\n const requestBody: SimplePayCancelCardRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n cardId,\n merchant: MERCHANT_ID,\n sdkVersion: SDK_VERSION,\n }\n return makeSimplePayCancelCardRequest(API_URL_CARD_CANCEL, requestBody, MERCHANT_KEY)\n}\n\nexport { startRecurringPayment, startTokenPayment , cancelCard}\n"],"names":["CURRENCIES","simplepayLogger","args","getSimplePayConfig","currency","SIMPLEPAY_API_URL","SIMPLEPAY_SANDBOX_URL","SDK_VERSION","MERCHANT_KEY","MERCHANT_ID","API_URL","API_URL_PAYMENT","API_URL_RECURRING","API_URL_CARD_CANCEL","prepareRequestBody","body","generateSignature","merchantKey","hmac","crypto","checkSignature","responseText","signature","toISO8601DateString","date","getCurrencyFromMerchantId","merchantId","key","value","makeSimplePayRequest","apiUrl","requestBody","makeRequest","makeSimplePayRecurringRequest","makeSimplePayTokenRequest","makeSimplePayCancelCardRequest","type","bodyString","response","responseSignature","responseJSON","error","getPaymentResponse","r","rDecoded","rDecodedJSON","responseJson","startPayment","paymentData","config","INTERVAL_IN_MONTHS","DEFAULT_UNTIL","DEFAULT_MAX_AMOUNT","DEFAULT_TIMES","startRecurringPayment","startTokenPayment","cancelCard","cardId"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/types.ts","../src/utils.ts","../src/oneTime.ts","../src/recurring.ts"],"sourcesContent":["export type PaymentMethod = 'CARD' | 'WIRE'\n\nexport const CURRENCIES = ['HUF', 'HUF_SZEP', 'EUR', 'USD'] as const\nexport type Currency = typeof CURRENCIES[number]\n\nexport const LANGUAGES = [\n 'AR', // Arabic\n 'BG', // Bulgarian\n 'CS', // Czech\n 'DE', // German\n 'EN', // English\n 'ES', // Spanish\n 'FR', // French\n 'IT', // Italian\n 'HR', // Croatian\n 'HU', // Hungarian\n 'PL', // Polish\n 'RO', // Romanian\n 'RU', // Russian\n 'SK', // Slovak\n 'TR', // Turkish\n 'ZH', // Chinese\n] as const\nexport type Language = typeof LANGUAGES[number]\n\nexport interface PaymentData {\n orderRef: string\n total: number | string\n customerEmail: string\n currency?: Currency\n language?: Language\n method?: PaymentMethod\n invoice?: {\n name: string\n country: string\n state: string\n city: string\n zip: string\n address: string\n address2?: string\n phone?: string\n }\n}\n\nexport interface PaymentConfig {\n redirectUrl?: string\n}\n\nexport type ISO8601DateString = string\nexport interface Recurring {\n times: number,\n until: ISO8601DateString,\n maxAmount: number\n}\nexport interface RecurringPaymentData extends PaymentData {\n customer: string,\n recurring: Recurring\n}\n\nexport interface TokenPaymentData extends Omit<PaymentData, 'method'> {\n method: 'CARD',\n customer: string,\n token: string\n}\n\nexport interface SimplePayRequestBody extends Omit<PaymentData, 'total'> {\n total: string\n salt: string\n merchant: string\n sdkVersion: string\n methods: PaymentMethod[]\n timeout: string\n url: string\n}\n\nexport interface SimplePayRecurringRequestBody extends SimplePayRequestBody {\n customer: string\n recurring: Recurring\n threeDSReqAuthMethod: '02' // only registered users can use this\n}\n\nexport interface SimplePayTokenRequestBody extends SimplePayRequestBody {\n customer: string\n token: string\n threeDSReqAuthMethod: '02' // only registered users can use this\n type: 'MIT' // Merchant Initiated Transaction\n}\n\nexport interface SimplePayCancelCardRequestBody {\n salt: string\n cardId: string\n merchant: string\n sdkVersion: string\n}\n\nexport interface SimplePayResponse {\n salt: string\n merchant: string\n orderRef: string\n currency: Currency\n transactionId: string\n timeout: ISO8601DateString\n total: string\n paymentUrl: string\n errorCodes?: string[]\n}\n\nexport interface SimplePayRecurringResponse extends SimplePayResponse {\n tokens: string[]\n}\n\nexport interface SimplePayTokenResponse extends Omit<SimplePayResponse, 'paymentUrl' | 'timeout'> { }\n\nexport interface SimplePayCancelCardResponse {\n salt: string\n merchant: string\n cardId: string\n status: 'DISABLED'\n expiry: string\n}\n\nexport type SimplePayEvents = 'SUCCESS' | 'FAIL' | 'TIMEOUT' | 'CANCEL'\n\nexport interface SimplePayAPIResult {\n r: number // response code\n t: string // transaction id\n e: SimplePayEvents // event\n m: string // merchant id\n o: string // order id\n}\n\nexport interface SimplePayResult {\n responseCode: number,\n transactionId: string,\n event: SimplePayEvents,\n merchantId: string,\n orderRef: string,\n tokens?: string[],\n}\n","import crypto from 'crypto'\nimport { CURRENCIES, Currency, ISO8601DateString, SimplePayAPIResult, SimplePayCancelCardRequestBody, SimplePayCancelCardResponse, SimplePayRecurringRequestBody, SimplePayRecurringResponse, SimplePayRequestBody, SimplePayResponse, SimplePayResult, SimplePayTokenRequestBody, SimplePayTokenResponse } from \"./types\"\n\nexport const simplepayLogger = (...args: any[]) => {\n if (process.env.SIMPLEPAY_LOGGER !== 'true') {\n return\n }\n\n console.log('👉 ', ...args)\n}\n\nexport const getSimplePayConfig = (currency: Currency) => {\n if (!CURRENCIES.includes(currency)) {\n throw new Error(`Unsupported currency: ${currency}`)\n }\n\n const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'\n const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'\n const SDK_VERSION = 'SimplePay_Rrd_0.12.0'\n const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]\n const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]\n\n const API_URL = process.env.SIMPLEPAY_PRODUCTION === 'true' ? SIMPLEPAY_API_URL : SIMPLEPAY_SANDBOX_URL\n const API_URL_PAYMENT = API_URL + '/start'\n const API_URL_RECURRING = API_URL + '/dorecurring'\n const API_URL_CARD_CANCEL = API_URL + '/cardcancel'\n return {\n MERCHANT_KEY,\n MERCHANT_ID,\n API_URL_PAYMENT,\n API_URL_RECURRING,\n API_URL_CARD_CANCEL,\n SDK_VERSION\n }\n}\n\n// escaping slashes for the request body to prevent strange SimplePay API errors (eg Missing Signature)\nexport const prepareRequestBody = (body: any) =>\n JSON.stringify(body).replace(/\\//g, '\\\\/')\n\nexport const generateSignature = (body: string, merchantKey: string) => {\n const hmac = crypto.createHmac('sha384', merchantKey.trim())\n hmac.update(body, 'utf8')\n return hmac.digest('base64')\n}\n\nexport const checkSignature = (responseText: string, signature: string, merchantKey: string) =>\n signature === generateSignature(responseText, merchantKey)\n\nexport const toISO8601DateString = (date: Date): ISO8601DateString => date.toISOString().replace(/\\.\\d{3}Z$/, '+00:00')\n\nexport const getCurrencyFromMerchantId = (merchantId: string) => {\n const currency = Object.entries(process.env)\n .find(([key, value]) =>\n key.startsWith('SIMPLEPAY_MERCHANT_ID_') && value === merchantId\n )?.[0]?.replace('SIMPLEPAY_MERCHANT_ID_', '') as Currency\n\n if (!currency) {\n throw new Error(`Merchant id not found in the environment: ${merchantId}`)\n }\n\n return currency\n}\n\nexport const makeSimplePayRequest = async (apiUrl: string, requestBody: SimplePayRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'oneTime') as Promise<SimplePayResponse>\n}\n\nexport const makeSimplePayRecurringRequest = async (apiUrl: string, requestBody: SimplePayRecurringRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'recurring') as Promise<SimplePayRecurringResponse>\n}\n\nexport const makeSimplePayTokenRequest = async (apiUrl: string, requestBody: SimplePayTokenRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'token') as Promise<SimplePayTokenResponse>\n}\n\nexport const makeSimplePayCancelCardRequest = async (apiUrl: string, requestBody: SimplePayCancelCardRequestBody, merchantKey: string) => {\n return makeRequest(apiUrl, requestBody, merchantKey, 'cancelCard') as Promise<SimplePayCancelCardResponse>\n}\n\nconst makeRequest = async (apiUrl: string, requestBody: SimplePayRequestBody | SimplePayRecurringRequestBody | SimplePayTokenRequestBody | SimplePayCancelCardRequestBody, merchantKey: string, type: 'oneTime' | 'recurring' | 'token' | 'cancelCard') => {\n const bodyString = prepareRequestBody(requestBody)\n const signature = generateSignature(bodyString, merchantKey)\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, bodyString, signature })\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Signature': signature,\n },\n body: bodyString,\n })\n\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, response })\n\n if (!response.ok) {\n throw new Error(`SimplePay API error: ${response.status}`)\n }\n\n const responseSignature = response.headers.get('Signature')\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseSignature })\n if (!responseSignature) {\n throw new Error('Missing response signature')\n }\n\n const responseText = await response.text()\n const responseJSON = JSON.parse(responseText) as { errorCodes?: string[] }\n simplepayLogger({ function: `SimplePay/makeRequest/${type}`, responseText, responseJSON })\n\n if (responseJSON.errorCodes) {\n throw new Error(`SimplePay API error: ${responseJSON.errorCodes}`)\n }\n\n if (!checkSignature(responseText, responseSignature, merchantKey)) {\n throw new Error('Invalid response signature')\n }\n\n return responseJSON\n\n } catch (error) {\n throw error\n }\n}\n\nexport const getPaymentResponse = (r: string, signature: string) => {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', r, signature })\n signature = decodeURIComponent(signature)\n const rDecoded = Buffer.from(r, 'base64').toString('utf-8')\n const rDecodedJSON = JSON.parse(rDecoded) as SimplePayAPIResult\n const currency = getCurrencyFromMerchantId(rDecodedJSON.m)\n const { MERCHANT_KEY } = getSimplePayConfig(currency as Currency)\n\n if (!checkSignature(rDecoded, signature, MERCHANT_KEY || '')) {\n simplepayLogger({ function: 'SimplePay/getPaymentResponse', rDecoded, signature })\n throw new Error('Invalid response signature')\n }\n\n const responseJson = JSON.parse(rDecoded)\n const response: SimplePayResult = {\n responseCode: responseJson.r,\n transactionId: responseJson.t,\n event: responseJson.e,\n merchantId: responseJson.m,\n orderRef: responseJson.o,\n tokens: responseJson.tokens,\n }\n\n return response\n}\n\n/**\n * Handles IPN (Instant Payment Notification) request and generates response\n * \n * This function implements the IPN flow according to SimplePay requirements:\n * 1. Validates the incoming signature\n * 2. Adds receiveDate property to the response (preserving original field order and data types)\n * 3. Generates response signature\n * \n * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.\n * Do NOT:\n * - Re-format or pretty-print the JSON\n * - Parse and re-stringify the JSON\n * - Add any whitespace or formatting\n * - Modify the Content-Type header (must be 'application/json')\n * \n * The signature is calculated on the exact string that will be sent in the HTTP body.\n * Any modification to the responseBody will invalidate the signature.\n * \n * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)\n * @param incomingSignature - The signature from the 'Signature' HTTP header\n * @param merchantKey - The merchant secret key for signature validation and generation\n * @returns Object containing the response JSON string and signature to send back\n * \n * @example\n * // In your IPN endpoint handler:\n * const ipnBody = await request.text() // Get raw body as string (IMPORTANT: use .text(), not JSON.parse())\n * const incomingSignature = request.headers.get('Signature')\n * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)\n * \n * // Send response with HTTP 200 status\n * // CRITICAL: Send responseBody exactly as returned, do not modify it!\n * return new Response(responseBody, {\n * status: 200,\n * headers: {\n * 'Content-Type': 'application/json',\n * 'Signature': signature\n * }\n * })\n */\nexport const handleIpnRequest = (ipnBody: string, incomingSignature: string, merchantKey: string) => {\n simplepayLogger({ function: 'SimplePay/handleIpnRequest', ipnBody, incomingSignature })\n \n // Step 1: Validate incoming signature\n if (!checkSignature(ipnBody, incomingSignature, merchantKey)) {\n throw new Error('Invalid IPN request signature')\n }\n \n // Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order and data types)\n JSON.parse(ipnBody) // Just validate, don't use the result\n \n // Step 3: Add receiveDate to the original JSON string while preserving:\n // - Exact field order\n // - Exact data types (numeric vs string)\n // - No whitespace (compact JSON)\n // \n // Use the same simple approach as the working solution:\n // Replace the closing brace with: ,\"receiveDate\":\"${receiveDate}\"}\n // This preserves the exact original format and field order\n const receiveDate = toISO8601DateString(new Date())\n \n // Simple string replacement: replace the first (and only) closing brace\n // This is the same approach as the working solution\n const responseBody = ipnBody.replace('}', `,\"receiveDate\":\"${receiveDate}\"}`)\n \n // Step 4: Generate response signature using SHA384 HMAC + Base64\n // The signature must be calculated on the exact string that will be sent\n const responseSignature = generateSignature(responseBody, merchantKey)\n \n simplepayLogger({ function: 'SimplePay/handleIpnRequest', responseBody, responseSignature })\n \n return {\n responseBody,\n signature: responseSignature\n }\n}\n","import crypto from 'crypto'\nimport { Currency, PaymentConfig, PaymentData, SimplePayRequestBody } from './types'\nimport { simplepayLogger, getSimplePayConfig, toISO8601DateString, makeSimplePayRequest } from './utils'\n\nconst startPayment = async (paymentData: PaymentData, config: PaymentConfig = {}) => {\n simplepayLogger({ function: 'SimplePay/startPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: [paymentData.method || 'CARD'],\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: config.redirectUrl || process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nexport { startPayment }\n","import crypto from 'crypto'\nimport { SimplePayRecurringRequestBody, RecurringPaymentData, TokenPaymentData, SimplePayTokenRequestBody, SimplePayCancelCardRequestBody, Currency} from './types'\nimport { getSimplePayConfig, simplepayLogger, toISO8601DateString, makeSimplePayTokenRequest, makeSimplePayRecurringRequest, makeSimplePayRequest, makeSimplePayCancelCardRequest} from './utils'\n\nconst INTERVAL_IN_MONTHS = 6\nconst DEFAULT_UNTIL = new Date(Date.now() + INTERVAL_IN_MONTHS * 30 * 24 * 60 * 60 * 1000)\nconst DEFAULT_MAX_AMOUNT = 12000\nconst DEFAULT_TIMES = 3\n\nconst startRecurringPayment = async (paymentData: RecurringPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startRecurringPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_PAYMENT })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayRecurringRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n recurring: {\n times: paymentData.recurring.times || DEFAULT_TIMES,\n until: paymentData.recurring.until || toISO8601DateString(DEFAULT_UNTIL),\n maxAmount: paymentData.recurring.maxAmount || DEFAULT_MAX_AMOUNT\n },\n threeDSReqAuthMethod: '02', \n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayRecurringRequest(API_URL_PAYMENT, requestBody, MERCHANT_KEY)\n}\n\nconst startTokenPayment = async (paymentData: TokenPaymentData) => {\n simplepayLogger({ function: 'SimplePay/startTokenPayment', paymentData })\n const currency = paymentData.currency || 'HUF'\n const { MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING, SDK_VERSION } = getSimplePayConfig(currency)\n simplepayLogger({ function: 'SimplePay/startTokenPayment', MERCHANT_KEY, MERCHANT_ID, API_URL_RECURRING })\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for ${currency}`)\n }\n\n const requestBody: SimplePayTokenRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n merchant: MERCHANT_ID,\n orderRef: paymentData.orderRef,\n currency: currency.replace('_SZEP', '') as Currency,\n customer: paymentData.customer,\n customerEmail: paymentData.customerEmail,\n language: paymentData.language || 'HU',\n sdkVersion: SDK_VERSION,\n methods: ['CARD'],\n token: paymentData.token,\n type: 'MIT',\n threeDSReqAuthMethod: '02',\n total: String(paymentData.total),\n timeout: toISO8601DateString(new Date(Date.now() + 30 * 60 * 1000)),\n url: process.env.SIMPLEPAY_REDIRECT_URL || 'http://recurring.url.to.redirect',\n invoice: paymentData.invoice,\n }\n\n return makeSimplePayTokenRequest(API_URL_RECURRING, requestBody, MERCHANT_KEY)\n}\n\nconst cancelCard = async (cardId: string) => {\n simplepayLogger({ function: 'SimplePay/cancelCard', cardId })\n const {API_URL_CARD_CANCEL, MERCHANT_KEY, MERCHANT_ID, SDK_VERSION} = getSimplePayConfig('HUF')\n\n if (!MERCHANT_KEY || !MERCHANT_ID) {\n throw new Error(`Missing SimplePay configuration for HUF`)\n }\n\n const requestBody: SimplePayCancelCardRequestBody = {\n salt: crypto.randomBytes(16).toString('hex'),\n cardId,\n merchant: MERCHANT_ID,\n sdkVersion: SDK_VERSION,\n }\n return makeSimplePayCancelCardRequest(API_URL_CARD_CANCEL, requestBody, MERCHANT_KEY)\n}\n\nexport { startRecurringPayment, startTokenPayment , cancelCard}\n"],"names":["CURRENCIES","simplepayLogger","args","getSimplePayConfig","currency","SIMPLEPAY_API_URL","SIMPLEPAY_SANDBOX_URL","SDK_VERSION","MERCHANT_KEY","MERCHANT_ID","API_URL","API_URL_PAYMENT","API_URL_RECURRING","API_URL_CARD_CANCEL","prepareRequestBody","body","generateSignature","merchantKey","hmac","crypto","checkSignature","responseText","signature","toISO8601DateString","date","getCurrencyFromMerchantId","merchantId","key","value","makeSimplePayRequest","apiUrl","requestBody","makeRequest","makeSimplePayRecurringRequest","makeSimplePayTokenRequest","makeSimplePayCancelCardRequest","type","bodyString","response","responseSignature","responseJSON","error","getPaymentResponse","r","rDecoded","rDecodedJSON","responseJson","handleIpnRequest","ipnBody","incomingSignature","receiveDate","responseBody","startPayment","paymentData","config","INTERVAL_IN_MONTHS","DEFAULT_UNTIL","DEFAULT_MAX_AMOUNT","DEFAULT_TIMES","startRecurringPayment","startTokenPayment","cancelCard","cardId"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplepay-js-sdk",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
4
4
  "description": "A Node.js utility for SimplePay payment integration",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { startPayment } from './oneTime'
2
2
  import { startRecurringPayment, startTokenPayment, cancelCard} from './recurring'
3
3
  import type { Currency, Language, PaymentMethod, SimplePayResponse, SimplePayRecurringResponse, SimplePayTokenResponse, SimplePayCancelCardResponse, SimplePayResult } from './types'
4
- import { checkSignature, generateSignature, getPaymentResponse, toISO8601DateString } from './utils'
4
+ import { checkSignature, generateSignature, getPaymentResponse, handleIpnRequest, toISO8601DateString } from './utils'
5
5
 
6
6
  export {
7
7
  Currency, Language, PaymentMethod,
8
8
  SimplePayResponse, SimplePayRecurringResponse, SimplePayTokenResponse, SimplePayCancelCardResponse, SimplePayResult,
9
- checkSignature, generateSignature, toISO8601DateString, getPaymentResponse,
9
+ checkSignature, generateSignature, toISO8601DateString, getPaymentResponse, handleIpnRequest,
10
10
  startPayment,
11
11
  startRecurringPayment, startTokenPayment, cancelCard
12
12
  }
package/src/utils.spec.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest'
2
2
  import { Currency } from './types'
3
- import { checkSignature, generateSignature, getCurrencyFromMerchantId, getSimplePayConfig } from './utils'
3
+ import { checkSignature, generateSignature, getCurrencyFromMerchantId, getSimplePayConfig, handleIpnRequest } from './utils'
4
4
 
5
5
  const setEnv = () => {
6
6
  process.env.SIMPLEPAY_MERCHANT_ID_HUF = 'testId'
@@ -140,4 +140,233 @@ describe('SimplePay Utils Tests', () => {
140
140
  expect(currencySzep).toBe('HUF_SZEP')
141
141
  })
142
142
  })
143
+
144
+ describe('IPN Response Signature', () => {
145
+ it('should generate valid signature for IPN response after adding receiveDate', () => {
146
+ setEnv()
147
+ const merchantKey = 'testKey'
148
+
149
+ // Simulate incoming IPN request body (compact JSON, no whitespace)
150
+ const ipnRequestBody = {
151
+ r: 0,
152
+ t: '504233881',
153
+ e: 'SUCCESS',
154
+ m: 'testId',
155
+ o: 'test-order-123'
156
+ }
157
+
158
+ // JSON.stringify produces compact JSON by default (no whitespace)
159
+ const ipnBodyString = JSON.stringify(ipnRequestBody)
160
+
161
+ // Generate valid incoming signature (as if from SimplePay)
162
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
163
+
164
+ // Step 1: Verify incoming signature is valid
165
+ const isIncomingSignatureValid = checkSignature(ipnBodyString, incomingSignature, merchantKey)
166
+ expect(isIncomingSignatureValid).toBeTruthy()
167
+
168
+ // Step 2: Add receiveDate property (as per IPN flow)
169
+ const ipnResponseBody = {
170
+ ...ipnRequestBody,
171
+ receiveDate: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00')
172
+ }
173
+
174
+ // JSON.stringify produces compact JSON (no whitespace) - required by SimplePay
175
+ const ipnResponseBodyString = JSON.stringify(ipnResponseBody)
176
+
177
+ // Step 3: Generate response signature
178
+ const responseSignature = generateSignature(ipnResponseBodyString, merchantKey)
179
+
180
+ // Step 4: Verify the response signature is valid
181
+ const isResponseSignatureValid = checkSignature(ipnResponseBodyString, responseSignature, merchantKey)
182
+ expect(isResponseSignatureValid).toBeTruthy()
183
+
184
+ // Verify response signature is different from incoming signature (since body changed)
185
+ expect(responseSignature).not.toBe(incomingSignature)
186
+
187
+ // Verify JSON is compact (no whitespace)
188
+ expect(ipnResponseBodyString).not.toMatch(/\s/)
189
+ })
190
+
191
+ it('should handle IPN request using handleIpnRequest function', () => {
192
+ setEnv()
193
+ const merchantKey = 'testKey'
194
+
195
+ // Simulate incoming IPN request body (compact JSON, no whitespace)
196
+ // Use a specific field order to test that it's preserved
197
+ const ipnBodyString = '{"r":0,"t":"504233881","e":"SUCCESS","m":"testId","o":"test-order-123"}'
198
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
199
+
200
+ // Use handleIpnRequest function
201
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
202
+
203
+ // Verify response contains receiveDate
204
+ const responseData = JSON.parse(responseBody)
205
+ expect(responseData).toHaveProperty('receiveDate')
206
+ expect(responseData.r).toBe(0)
207
+ expect(responseData.t).toBe('504233881')
208
+ expect(responseData.e).toBe('SUCCESS')
209
+ expect(responseData.m).toBe('testId')
210
+ expect(responseData.o).toBe('test-order-123')
211
+
212
+ // Verify response signature is valid
213
+ const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
214
+ expect(isSignatureValid).toBeTruthy()
215
+
216
+ // Verify JSON is compact (no whitespace) - required by SimplePay
217
+ expect(responseBody).not.toMatch(/\s/)
218
+
219
+ // Verify receiveDate is in ISO 8601 format (e.g., 2025-10-06T07:00:34+02:00)
220
+ expect(responseData.receiveDate).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/)
221
+
222
+ // Verify field order is preserved: original fields come first, receiveDate is last
223
+ const responseBodyString = responseBody
224
+ expect(responseBodyString).toMatch(/^\{.*"r":0,.*"t":".*",.*"e":".*",.*"m":".*",.*"o":".*",.*"receiveDate":".*"\}$/)
225
+ // Verify receiveDate is the last field before closing brace
226
+ expect(responseBodyString).toMatch(/"receiveDate":"[^"]*"\}$/)
227
+ })
228
+
229
+ it('should preserve field order in IPN response', () => {
230
+ setEnv()
231
+ const merchantKey = 'testKey'
232
+
233
+ // Test with a specific field order
234
+ const ipnBodyString = '{"r":0,"t":"123","e":"SUCCESS","m":"merchantId","o":"order123"}'
235
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
236
+
237
+ const { responseBody } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
238
+
239
+ // Verify the original fields are in the same order
240
+ const rIndex = responseBody.indexOf('"r":')
241
+ const tIndex = responseBody.indexOf('"t":')
242
+ const eIndex = responseBody.indexOf('"e":')
243
+ const mIndex = responseBody.indexOf('"m":')
244
+ const oIndex = responseBody.indexOf('"o":')
245
+ const receiveDateIndex = responseBody.indexOf('"receiveDate":')
246
+
247
+ // Verify order: r < t < e < m < o < receiveDate
248
+ expect(rIndex).toBeLessThan(tIndex)
249
+ expect(tIndex).toBeLessThan(eIndex)
250
+ expect(eIndex).toBeLessThan(mIndex)
251
+ expect(mIndex).toBeLessThan(oIndex)
252
+ expect(oIndex).toBeLessThan(receiveDateIndex)
253
+
254
+ // Verify receiveDate is the last field
255
+ const lastFieldIndex = Math.max(rIndex, tIndex, eIndex, mIndex, oIndex, receiveDateIndex)
256
+ expect(lastFieldIndex).toBe(receiveDateIndex)
257
+ })
258
+
259
+ it('should preserve numeric data types in IPN response', () => {
260
+ setEnv()
261
+ const merchantKey = 'testKey'
262
+
263
+ // Test with numeric transactionId (not string)
264
+ // This is critical: SimplePay sends numeric values, they must stay numeric
265
+ const ipnBodyString = '{"r":0,"t":508163884,"e":"SUCCESS","m":"testId","o":"order123"}'
266
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
267
+
268
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
269
+
270
+ // Verify numeric transactionId stays numeric (no quotes around the number)
271
+ expect(responseBody).toMatch(/"t":508163884,/)
272
+ expect(responseBody).not.toMatch(/"t":"508163884",/)
273
+
274
+ // Verify response signature is valid
275
+ const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
276
+ expect(isSignatureValid).toBeTruthy()
277
+
278
+ // Verify the response can be parsed and numeric value is preserved
279
+ const parsed = JSON.parse(responseBody)
280
+ expect(typeof parsed.t).toBe('number')
281
+ expect(parsed.t).toBe(508163884)
282
+ })
283
+
284
+ it('should preserve string data types in IPN response', () => {
285
+ setEnv()
286
+ const merchantKey = 'testKey'
287
+
288
+ // Test with string transactionId
289
+ const ipnBodyString = '{"r":0,"t":"504233881","e":"SUCCESS","m":"testId","o":"order123"}'
290
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
291
+
292
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
293
+
294
+ // Verify string transactionId stays string (with quotes)
295
+ expect(responseBody).toMatch(/"t":"504233881",/)
296
+ expect(responseBody).not.toMatch(/"t":504233881,/)
297
+
298
+ // Verify response signature is valid
299
+ const isSignatureValid = checkSignature(responseBody, signature, merchantKey)
300
+ expect(isSignatureValid).toBeTruthy()
301
+
302
+ // Verify the response can be parsed and string value is preserved
303
+ const parsed = JSON.parse(responseBody)
304
+ expect(typeof parsed.t).toBe('string')
305
+ expect(parsed.t).toBe('504233881')
306
+ })
307
+
308
+ it('should throw error for invalid incoming IPN signature', () => {
309
+ setEnv()
310
+ const merchantKey = 'testKey'
311
+
312
+ const ipnBodyString = JSON.stringify({ r: 0, t: '123', e: 'SUCCESS', m: 'testId', o: 'test-order' })
313
+ const invalidSignature = 'invalid-signature'
314
+
315
+ expect(() => {
316
+ handleIpnRequest(ipnBodyString, invalidSignature, merchantKey)
317
+ }).toThrow('Invalid IPN request signature')
318
+ })
319
+
320
+ it('should generate valid signature for IPN response with different currencies', () => {
321
+ setEnv()
322
+
323
+ const testCases = [
324
+ { currency: 'HUF' as Currency, merchantKey: 'testKey', merchantId: 'testId' },
325
+ { currency: 'EUR' as Currency, merchantKey: 'secretEuroKey', merchantId: 'merchantEuroId' },
326
+ { currency: 'HUF_SZEP' as Currency, merchantKey: 'testKeySzep', merchantId: 'testIdSzep' }
327
+ ]
328
+
329
+ testCases.forEach(({ currency, merchantKey, merchantId }) => {
330
+ // Use specific field order to test preservation
331
+ const ipnBodyString = `{"r":0,"t":"504233881","e":"SUCCESS","m":"${merchantId}","o":"test-order-${currency}"}`
332
+ const incomingSignature = generateSignature(ipnBodyString, merchantKey)
333
+
334
+ // Verify incoming signature
335
+ expect(checkSignature(ipnBodyString, incomingSignature, merchantKey)).toBeTruthy()
336
+
337
+ // Use handleIpnRequest function
338
+ const { responseBody, signature } = handleIpnRequest(ipnBodyString, incomingSignature, merchantKey)
339
+
340
+ // Verify response signature
341
+ expect(checkSignature(responseBody, signature, merchantKey)).toBeTruthy()
342
+
343
+ // Verify JSON is compact (no whitespace)
344
+ expect(responseBody).not.toMatch(/\s/)
345
+
346
+ // Verify field order is preserved
347
+ expect(responseBody).toMatch(/^\{.*"r":0,.*"t":".*",.*"e":".*",.*"m":".*",.*"o":".*",.*"receiveDate":".*"\}$/)
348
+ })
349
+ })
350
+
351
+ it('should reject IPN response with invalid signature', () => {
352
+ setEnv()
353
+ const merchantKey = 'testKey'
354
+
355
+ const ipnRequestBody = {
356
+ r: 0,
357
+ t: '504233881',
358
+ e: 'SUCCESS',
359
+ m: 'testId',
360
+ o: 'test-order-123',
361
+ receiveDate: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00')
362
+ }
363
+
364
+ // JSON.stringify produces compact JSON (no whitespace)
365
+ const ipnResponseBodyString = JSON.stringify(ipnRequestBody)
366
+ const invalidSignature = 'invalid-signature'
367
+
368
+ const isValid = checkSignature(ipnResponseBodyString, invalidSignature, merchantKey)
369
+ expect(isValid).toBeFalsy()
370
+ })
371
+ })
143
372
  })
package/src/utils.ts CHANGED
@@ -16,7 +16,7 @@ export const getSimplePayConfig = (currency: Currency) => {
16
16
 
17
17
  const SIMPLEPAY_API_URL = 'https://secure.simplepay.hu/payment/v2'
18
18
  const SIMPLEPAY_SANDBOX_URL = 'https://sandbox.simplepay.hu/payment/v2'
19
- const SDK_VERSION = 'SimplePay_Rrd_0.11.2'
19
+ const SDK_VERSION = 'SimplePay_Rrd_0.12.1'
20
20
  const MERCHANT_KEY = process.env[`SIMPLEPAY_MERCHANT_KEY_${currency}`]
21
21
  const MERCHANT_ID = process.env[`SIMPLEPAY_MERCHANT_ID_${currency}`]
22
22
 
@@ -149,3 +149,79 @@ export const getPaymentResponse = (r: string, signature: string) => {
149
149
 
150
150
  return response
151
151
  }
152
+
153
+ /**
154
+ * Handles IPN (Instant Payment Notification) request and generates response
155
+ *
156
+ * This function implements the IPN flow according to SimplePay requirements:
157
+ * 1. Validates the incoming signature
158
+ * 2. Adds receiveDate property to the response (preserving original field order and data types)
159
+ * 3. Generates response signature
160
+ *
161
+ * **IMPORTANT**: The responseBody must be sent EXACTLY as returned, without any modifications.
162
+ * Do NOT:
163
+ * - Re-format or pretty-print the JSON
164
+ * - Parse and re-stringify the JSON
165
+ * - Add any whitespace or formatting
166
+ * - Modify the Content-Type header (must be 'application/json')
167
+ *
168
+ * The signature is calculated on the exact string that will be sent in the HTTP body.
169
+ * Any modification to the responseBody will invalidate the signature.
170
+ *
171
+ * @param ipnBody - The raw IPN request body as string (must be the exact string received, not parsed JSON)
172
+ * @param incomingSignature - The signature from the 'Signature' HTTP header
173
+ * @param merchantKey - The merchant secret key for signature validation and generation
174
+ * @returns Object containing the response JSON string and signature to send back
175
+ *
176
+ * @example
177
+ * // In your IPN endpoint handler:
178
+ * const ipnBody = await request.text() // Get raw body as string (IMPORTANT: use .text(), not JSON.parse())
179
+ * const incomingSignature = request.headers.get('Signature')
180
+ * const { responseBody, signature } = handleIpnRequest(ipnBody, incomingSignature, MERCHANT_KEY)
181
+ *
182
+ * // Send response with HTTP 200 status
183
+ * // CRITICAL: Send responseBody exactly as returned, do not modify it!
184
+ * return new Response(responseBody, {
185
+ * status: 200,
186
+ * headers: {
187
+ * 'Content-Type': 'application/json',
188
+ * 'Signature': signature
189
+ * }
190
+ * })
191
+ */
192
+ export const handleIpnRequest = (ipnBody: string, incomingSignature: string, merchantKey: string) => {
193
+ simplepayLogger({ function: 'SimplePay/handleIpnRequest', ipnBody, incomingSignature })
194
+
195
+ // Step 1: Validate incoming signature
196
+ if (!checkSignature(ipnBody, incomingSignature, merchantKey)) {
197
+ throw new Error('Invalid IPN request signature')
198
+ }
199
+
200
+ // Step 2: Validate it's valid JSON (but don't use parsed object to preserve field order and data types)
201
+ JSON.parse(ipnBody) // Just validate, don't use the result
202
+
203
+ // Step 3: Add receiveDate to the original JSON string while preserving:
204
+ // - Exact field order
205
+ // - Exact data types (numeric vs string)
206
+ // - No whitespace (compact JSON)
207
+ //
208
+ // Use the same simple approach as the working solution:
209
+ // Replace the closing brace with: ,"receiveDate":"${receiveDate}"}
210
+ // This preserves the exact original format and field order
211
+ const receiveDate = toISO8601DateString(new Date())
212
+
213
+ // Simple string replacement: replace the first (and only) closing brace
214
+ // This is the same approach as the working solution
215
+ const responseBody = ipnBody.replace('}', `,"receiveDate":"${receiveDate}"}`)
216
+
217
+ // Step 4: Generate response signature using SHA384 HMAC + Base64
218
+ // The signature must be calculated on the exact string that will be sent
219
+ const responseSignature = generateSignature(responseBody, merchantKey)
220
+
221
+ simplepayLogger({ function: 'SimplePay/handleIpnRequest', responseBody, responseSignature })
222
+
223
+ return {
224
+ responseBody,
225
+ signature: responseSignature
226
+ }
227
+ }