strapi-plugin-payone-provider 5.6.9 → 5.6.10

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
@@ -1566,3 +1566,135 @@ For wallet payments (PayPal, Google Pay, Apple Pay), you can specify:
1566
1566
 
1567
1567
  - `capturemode: "full"`: Capture the entire preauthorized amount
1568
1568
  - `capturemode: "partial"`: Capture less than the preauthorized amount
1569
+
1570
+ ---
1571
+
1572
+ ## đŸ“ĸ TransactionStatus Notifications
1573
+
1574
+ The Payone platform provides an asynchronous way of notifying your system of changes to a transaction. These notifications are called "TransactionStatus" and are automatically handled by this plugin.
1575
+
1576
+ ### What are TransactionStatus Notifications?
1577
+
1578
+ TransactionStatus notifications are POST requests sent from Payone's servers to your endpoint when transaction status changes occur. This is especially important for:
1579
+
1580
+ - **Redirect Payment Methods**: Verifying that payments were actually completed (prevents fraud)
1581
+ - **Chargeback Processes**: Being notified when customers initiate chargebacks
1582
+ - **Real-time Tracking**: Keeping your system updated with the latest transaction status
1583
+
1584
+ ### How It Works
1585
+
1586
+ 1. **Payone sends notification** → Your Strapi endpoint receives POST request
1587
+ 2. **Plugin verifies request** → Checks IP address, User-Agent, and hash signature
1588
+ 3. **Plugin processes notification** → Updates transaction history automatically
1589
+ 4. **Plugin responds** → Returns `TSOK` to confirm receipt
1590
+
1591
+ ### Endpoint Configuration
1592
+
1593
+ The plugin automatically provides the TransactionStatus endpoint at:
1594
+
1595
+ **URL**: `POST /api/strapi-plugin-payone-provider/transaction-status`
1596
+
1597
+ **No authentication required** - The endpoint is secured by:
1598
+
1599
+ - IP address verification (only Payone IPs allowed)
1600
+ - User-Agent verification (must be "PAYONE FinanceGate")
1601
+ - Hash signature verification (MD5 hash of transaction data)
1602
+
1603
+ ### PMI Configuration
1604
+
1605
+ You need to configure this endpoint in your Payone Merchant Interface (PMI):
1606
+
1607
+ 1. Log into your Payone Merchant Interface (PMI)
1608
+ 2. Navigate to **Configuration** → **Payment Portals** → **[Your Portal]**
1609
+ 3. Find the **TransactionStatus Endpoint** setting
1610
+ 4. Enter your endpoint URL: `https://yourdomain.com/api/strapi-plugin-payone-provider/transaction-status`
1611
+ 5. Save the configuration
1612
+
1613
+ > âš ī¸ **Important**: The endpoint must be accessible via HTTPS. Payone will not send notifications to HTTP endpoints.
1614
+
1615
+ ### Security Features
1616
+
1617
+ The plugin automatically verifies:
1618
+
1619
+ 1. **IP Address**: Only accepts requests from Payone's IP ranges:
1620
+
1621
+ - `185.60.20.0/24`
1622
+ - `54.246.203.105`
1623
+
1624
+ 2. **User-Agent**: Must be exactly `"PAYONE FinanceGate"`
1625
+
1626
+ 3. **Hash Signature**: Verifies MD5 hash using your Portal Key:
1627
+
1628
+ ```
1629
+ MD5(portalid + aid + txid + sequencenumber + price + currency + mode + key)
1630
+ ```
1631
+
1632
+ 4. **Credentials**: Verifies that `portalid` and `aid` match your configured settings
1633
+
1634
+ ### Notification Parameters
1635
+
1636
+ Payone sends the following parameters (among others):
1637
+
1638
+ - `txaction`: Transaction action (appointed, paid, cancelation, etc.)
1639
+ - `txid`: Transaction ID
1640
+ - `reference`: Your reference number
1641
+ - `sequencenumber`: Sequence number for this transaction
1642
+ - `transaction_status`: Current transaction status
1643
+ - `price`: Transaction amount
1644
+ - `balance`: Current balance
1645
+ - `receivable`: Receivable amount
1646
+ - `currency`: Currency code
1647
+ - `key`: MD5 hash for verification
1648
+
1649
+ ### Response Requirements
1650
+
1651
+ The endpoint **must** respond with exactly `TSOK` (4 characters, no HTML):
1652
+
1653
+ - Response time: Must be within 10 seconds
1654
+ - Response format: Plain text `TSOK`
1655
+ - No other characters allowed
1656
+
1657
+ > â„šī¸ **Note**: Even if processing fails, the endpoint must return `TSOK` to prevent Payone from retrying. The plugin handles this automatically.
1658
+
1659
+ ### Retry Mechanism
1660
+
1661
+ If Payone doesn't receive `TSOK` within 10 seconds:
1662
+
1663
+ - Payone will retry up to **12 times**
1664
+ - Retries occur over **48 hours**
1665
+ - First retry after ~1 hour
1666
+ - Retry intervals increase over time
1667
+
1668
+ ### Transaction Updates
1669
+
1670
+ When a TransactionStatus notification is received:
1671
+
1672
+ 1. The plugin finds the transaction in history by `txid`
1673
+ 2. Updates the transaction with new status information
1674
+ 3. Stores the full notification data for reference
1675
+ 4. Updates `balance`, `receivable`, and `sequencenumber` fields
1676
+
1677
+ If the transaction is not found in history, a new entry is created.
1678
+
1679
+ ### Example Notification Flow
1680
+
1681
+ **Scenario**: Customer completes PayPal payment
1682
+
1683
+ 1. Customer redirected to PayPal → Completes payment
1684
+ 2. Payone sends TransactionStatus: `txaction=appointed`, `transaction_status=completed`
1685
+ 3. Plugin receives notification → Verifies and updates transaction
1686
+ 4. Plugin responds: `TSOK`
1687
+ 5. Customer redirected back to your `successurl`
1688
+ 6. Later, Payone sends: `txaction=paid` (payment confirmed)
1689
+ 7. Plugin updates transaction status to "paid"
1690
+
1691
+ ### Testing
1692
+
1693
+ To test TransactionStatus notifications:
1694
+
1695
+ 1. Configure the endpoint in PMI
1696
+ 2. Make a test payment
1697
+ 3. Check your Strapi logs for notification processing
1698
+ 4. Verify transaction history is updated correctly
1699
+
1700
+ > 📖 **Reference**: For more details, see [Payone TransactionStatus Notification Documentation](https://docs.payone.com/integration/response-handling/transactionstatus-notification)
@@ -89,7 +89,6 @@ export const getBaseParams = (options = {}) => {
89
89
  currency: currency.toUpperCase(),
90
90
  reference: reference || `REF-${Date.now()}`,
91
91
  customerid: finalCustomerId,
92
-
93
92
  firstname,
94
93
  lastname,
95
94
  street,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-payone-provider",
3
- "version": "5.6.9",
3
+ "version": "5.6.10",
4
4
  "description": "Strapi plugin for Payone payment gateway integration",
5
5
  "license": "MIT",
6
6
  "maintainers": [
@@ -295,5 +295,23 @@ module.exports = ({ strapi }) => ({
295
295
  }
296
296
  };
297
297
  }
298
+ },
299
+
300
+ async handleTransactionStatus(ctx) {
301
+ try {
302
+ const notificationData = ctx.request.body || {};
303
+ await getPayoneService(strapi).processTransactionStatus(notificationData);
304
+
305
+ ctx.status = 200;
306
+ ctx.body = "TSOK";
307
+ ctx.type = "text/plain";
308
+ console.log(`[Payone TransactionStatus] Responded TSOK`);
309
+ } catch (error) {
310
+ console.log("[Payone TransactionStatus] Error handling notification:", error);
311
+ ctx.status = 200;
312
+ ctx.body = "TSOK";
313
+ ctx.type = "text/plain";
314
+ }
298
315
  }
316
+
299
317
  });
@@ -2,5 +2,6 @@
2
2
 
3
3
  module.exports = {
4
4
  "is-auth": require("./is-auth"),
5
- "is-super-admin": require("./isSuperAdmin")
5
+ "is-super-admin": require("./isSuperAdmin"),
6
+ "is-payone-notification": require("./is-payone-notification")
6
7
  };
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ module.exports = async (policyContext, config, { strapi }) => {
4
+ const { request } = policyContext;
5
+ const userAgent = request.header["user-agent"] || request.header["User-Agent"] || "";
6
+ const clientIp = request.ip || request.connection?.remoteAddress || "";
7
+
8
+ if (userAgent !== "PAYONE FinanceGate") {
9
+ console.log(`[Payone TransactionStatus] Invalid User-Agent: ${userAgent}, IP: ${clientIp}`);
10
+ return false;
11
+ }
12
+
13
+
14
+ const isValidIp = (ip) => {
15
+ if (ip.startsWith("185.60.20.")) {
16
+ return true;
17
+ }
18
+
19
+ if (ip === "54.246.203.105") {
20
+ return true;
21
+ }
22
+ return false;
23
+ };
24
+
25
+ if (!isValidIp(clientIp)) {
26
+ console.log(`[Payone TransactionStatus] Invalid IP address: ${clientIp}, User-Agent: ${userAgent}`);
27
+ return false;
28
+ }
29
+
30
+ return true;
31
+ };
@@ -162,6 +162,17 @@ module.exports = {
162
162
  auth: false
163
163
  }
164
164
  },
165
+
166
+ {
167
+ method: "POST",
168
+ path: "/transaction-status",
169
+ handler: "payone.handleTransactionStatus",
170
+ config: {
171
+ policies: ["plugin::strapi-plugin-payone-provider.is-payone-notification"],
172
+ auth: false
173
+ }
174
+ },
175
+
165
176
  ]
166
177
  }
167
178
  };
@@ -5,6 +5,7 @@ const transactionService = require("./transactionService");
5
5
  const paymentService = require("./paymentService");
6
6
  const testConnectionService = require("./testConnectionService");
7
7
  const applePayService = require("./applePayService");
8
+ const transactionStatusService = require("./transactionStatusService");
8
9
 
9
10
  module.exports = ({ strapi }) => ({
10
11
  // Settings
@@ -59,5 +60,11 @@ module.exports = ({ strapi }) => ({
59
60
 
60
61
  async initializeApplePaySession(params) {
61
62
  return await applePayService.initializeApplePaySession(strapi, params);
63
+ },
64
+
65
+ // TransactionStatus Notification
66
+ async processTransactionStatus(notificationData) {
67
+ return await transactionStatusService.processTransactionStatus(strapi, notificationData);
62
68
  }
69
+
63
70
  });
@@ -29,8 +29,6 @@ const logTransaction = async (strapi, transactionData) => {
29
29
  transactionData.Error?.CustomerMessage ||
30
30
  null,
31
31
  body: transactionData || null,
32
- raw_request: transactionData.raw_request || null,
33
- raw_response: transactionData.raw_response || transactionData,
34
32
  created_at: new Date().toISOString(),
35
33
  updated_at: new Date().toISOString()
36
34
  };
@@ -46,7 +44,7 @@ const logTransaction = async (strapi, transactionData) => {
46
44
  value: transactionHistory
47
45
  });
48
46
 
49
- strapi.log.info("Transaction logged:", logEntry);
47
+ console.log(`Transaction logged: ${logEntry}`);
50
48
  };
51
49
 
52
50
 
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const { getPluginStore, getSettings } = require("./settingsService");
5
+
6
+ const verifyHash = (notificationData, portalKey) => {
7
+ const {
8
+ portalid = "",
9
+ aid = "",
10
+ txid = "",
11
+ sequencenumber = "",
12
+ price = "",
13
+ currency = "",
14
+ mode = "",
15
+ } = notificationData;
16
+
17
+ const hashString = `${portalid}${aid}${txid}${sequencenumber}${price}${currency}${mode}${portalKey}`;
18
+ const expectedHash = crypto.createHash("md5").update(hashString).digest("hex");
19
+
20
+ return expectedHash.toLowerCase() === (notificationData.key || "").toLowerCase();
21
+ };
22
+
23
+ const processTransactionStatus = async (strapi, notificationData) => {
24
+ try {
25
+ const settings = await getSettings(strapi);
26
+ const txid = notificationData.txid;
27
+
28
+ if (!settings || !settings.key) {
29
+ console.log("[Payone TransactionStatus] Settings not found or key missing");
30
+ return;
31
+ }
32
+
33
+ const isValid = verifyHash(notificationData, settings.key);
34
+ if (!isValid) {
35
+ console.log(`[Payone TransactionStatus] Hash verification failed txid: ${txid}`);
36
+ return;
37
+ }
38
+
39
+ if (notificationData.portalid !== settings.portalid || notificationData.aid !== settings.aid) {
40
+ console.log(`[Payone TransactionStatus] Portal ID or AID mismatch txid: ${txid}`);
41
+ return;
42
+ }
43
+
44
+ const pluginStore = getPluginStore(strapi);
45
+ let transactionHistory = (await pluginStore.get({ key: "transactionHistory" })) || [];
46
+
47
+ const transaction = transactionHistory.find((t) => t.txid === txid || t.id === txid);
48
+
49
+ if (transaction) {
50
+ Object.assign(transaction, {
51
+ ...notificationData,
52
+ status: notificationData?.transaction_status,
53
+ txaction: notificationData?.txaction,
54
+ txtime: notificationData?.txtime,
55
+ sequencenumber: notificationData?.sequencenumber,
56
+ balance: notificationData?.balance,
57
+ receivable: notificationData?.receivable,
58
+ price: notificationData?.price,
59
+ amount: notificationData?.price ? parseFloat(notificationData?.price) * 100 : transaction?.amount,
60
+ userid: notificationData?.userid,
61
+ updated_at: new Date().toISOString(),
62
+ body: {
63
+ ...transaction?.body,
64
+ ...notificationData,
65
+ status: notificationData?.transaction_status
66
+ }
67
+ });
68
+
69
+ await pluginStore.set({
70
+ key: "transactionHistory",
71
+ value: transactionHistory,
72
+ });
73
+
74
+ console.log(`[Payone TransactionStatus] Successfully updated transaction txid: ${txid}`);
75
+ } else {
76
+ console.log(`[Payone TransactionStatus] Transaction ${txid} not found in history. Notification ignored.`);
77
+ }
78
+
79
+ } catch (error) {
80
+ console.log(`[Payone TransactionStatus] Error processing notification: ${error}`);
81
+ }
82
+ };
83
+
84
+ module.exports = {
85
+ verifyHash,
86
+ processTransactionStatus,
87
+ };
@@ -2,6 +2,38 @@
2
2
 
3
3
  const crypto = require("crypto");
4
4
  const { normalizeCustomerId } = require("./normalize");
5
+ const calculateKeyHash = (settings, params) => {
6
+ const portalKey = settings.portalKey || settings.key;
7
+ const portalid = String(settings.portalid || "");
8
+ const aid = String(settings.aid || "");
9
+ const mode = String(settings.mode || "test");
10
+
11
+ const requestType = params.request || "";
12
+
13
+ // For Capture and Refund operations
14
+ if (requestType === "capture" || requestType === "refund") {
15
+ const txid = String(params.txid || "");
16
+ const sequencenumber = String(params.sequencenumber || "");
17
+ const amount = String(params.amount || "");
18
+ const currency = String(params.currency || "EUR");
19
+
20
+ const hashString = `${portalid}${aid}${txid}${sequencenumber}${amount}${currency}${mode}${portalKey}`;
21
+ return crypto.createHash("md5").update(hashString).digest("hex");
22
+ }
23
+
24
+ // For Preauthorization and Authorization operations
25
+ if (requestType === "preauthorization" || requestType === "authorization") {
26
+ const amount = String(params.amount || "");
27
+ const currency = String(params.currency || "EUR");
28
+ const reference = String(params.reference || "");
29
+
30
+ const hashString = `${portalid}${aid}${amount}${currency}${reference}${mode}${portalKey}`;
31
+ return crypto.createHash("md5").update(hashString).digest("hex");
32
+ }
33
+
34
+ const hashString = `${portalid}${aid}${mode}${portalKey}`;
35
+ return crypto.createHash("md5").update(hashString).digest("hex");
36
+ };
5
37
 
6
38
  const buildClientRequestParams = (settings, params, logger = null) => {
7
39
  const requestParams = {
@@ -14,16 +46,13 @@ const buildClientRequestParams = (settings, params, logger = null) => {
14
46
  ...params
15
47
  };
16
48
 
17
- requestParams.key = crypto
18
- .createHash("md5")
19
- .update(settings.portalKey || settings.key)
20
- .digest("hex");
21
-
22
49
  requestParams.customerid = normalizeCustomerId(
23
50
  requestParams.customerid,
24
51
  logger
25
52
  );
26
53
 
54
+ requestParams.key = calculateKeyHash(settings, requestParams);
55
+
27
56
  const isCreditCard = requestParams.clearingtype === "cc";
28
57
  const enable3DSecure = settings.enable3DSecure !== false;
29
58