@payware/mcp-server 1.3.2 → 1.3.3

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.
@@ -8,7 +8,7 @@ import { apiErrorResult } from '../../shared/api-errors.js';
8
8
  * @param {Object} params - Parameters for set price request
9
9
  * @returns {Object} Set price response
10
10
  */
11
- export async function setPOIPrice({ poiId, amount, currency, reasonL1, reasonL2, timeToLive, callbackUrl, passbackParams, merchantPartnerId, oauth2Token, useSandbox = true }) {
11
+ export async function setPOIPrice({ poiId, amount, currency, ttlSeconds, merchantPartnerId, oauth2Token, useSandbox = true }) {
12
12
  if (!poiId) {
13
13
  throw new Error('POI ID is required');
14
14
  }
@@ -21,10 +21,6 @@ export async function setPOIPrice({ poiId, amount, currency, reasonL1, reasonL2,
21
21
  throw new Error('Currency is required');
22
22
  }
23
23
 
24
- if (!reasonL1) {
25
- throw new Error('Reason (reasonL1) is required');
26
- }
27
-
28
24
  if (!merchantPartnerId) {
29
25
  throw new Error('Merchant Partner ID is required for ISV operations');
30
26
  }
@@ -36,17 +32,16 @@ export async function setPOIPrice({ poiId, amount, currency, reasonL1, reasonL2,
36
32
  const isvPartnerId = getPartnerIdSafe();
37
33
  const privateKey = getPrivateKeySafe(useSandbox);
38
34
 
39
- // Build request body
35
+ // amount, currency and ttlSeconds are the only fields the server reads (POIPriceRequest); anything
36
+ // else is ignored without an error. This used to send reasonL1 (and demand it), reasonL2,
37
+ // timeToLive, callbackUrl and passbackParams - none of which the server has - so a caller relying
38
+ // on the per-price callbackUrl got no callbacks at all. The callback URL belongs to the POI.
40
39
  const requestBody = {
41
40
  amount: String(amount),
42
- currency: currency.toUpperCase(),
43
- reasonL1
41
+ currency: currency.toUpperCase()
44
42
  };
45
43
 
46
- if (reasonL2) requestBody.reasonL2 = reasonL2;
47
- if (timeToLive) requestBody.timeToLive = timeToLive;
48
- if (callbackUrl) requestBody.callbackUrl = callbackUrl;
49
- if (passbackParams) requestBody.passbackParams = passbackParams;
44
+ if (ttlSeconds) requestBody.ttlSeconds = ttlSeconds;
50
45
 
51
46
  const jwtData = await createJWTForPartner({
52
47
  partnerId: isvPartnerId,
@@ -88,13 +83,21 @@ export const setPOIPriceTool = {
88
83
  **Endpoint:** PUT /poi/{poiId}/price
89
84
  **Use Case:** Set the amount a customer should pay when they scan the POI.
90
85
 
91
- The POI will transition to READY state and wait for a customer scan until the TTL expires.
86
+ The POI moves to READY and waits for a scan until the TTL expires. The scan creates the transaction and
87
+ the POI is BUSY until the sale is final.
88
+
89
+ **Only amount, currency and ttlSeconds are accepted.** There is no description, callback URL or passback
90
+ field on a price - the callback URL is set on the POI when it is created, and a POI without one sends no
91
+ callbacks.
92
+
93
+ **Keep the returned sessionToken** until the sale has a final status: pass it to
94
+ payware_poi_get_status to read that sale's transaction and outcome.
92
95
 
93
- **Required:** POI ID, amount, currency, reason, Merchant Partner ID, and OAuth2 token.`,
96
+ **Required:** POI ID, amount, currency, Merchant Partner ID, and OAuth2 token.`,
94
97
 
95
98
  inputSchema: {
96
99
  type: "object",
97
- required: ["poiId", "amount", "currency", "reasonL1", "merchantPartnerId", "oauth2Token"],
100
+ required: ["poiId", "amount", "currency", "merchantPartnerId", "oauth2Token"],
98
101
  properties: {
99
102
  poiId: {
100
103
  type: "string",
@@ -102,31 +105,15 @@ The POI will transition to READY state and wait for a customer scan until the TT
102
105
  },
103
106
  amount: {
104
107
  type: "string",
105
- description: "Payment amount (e.g., '25.50')"
108
+ description: "Payment amount (e.g., '25.50'), positive and no finer than the currency's smallest unit"
106
109
  },
107
110
  currency: {
108
111
  type: "string",
109
112
  description: "ISO 4217 currency code (e.g., 'EUR', 'USD', 'GBP')"
110
113
  },
111
- reasonL1: {
112
- type: "string",
113
- description: "Payment description (e.g., 'Table 5', 'Order #123')"
114
- },
115
- reasonL2: {
116
- type: "string",
117
- description: "Additional description (optional)"
118
- },
119
- timeToLive: {
114
+ ttlSeconds: {
120
115
  type: "integer",
121
- description: "Seconds until price expires (60-600, default: 120)"
122
- },
123
- callbackUrl: {
124
- type: "string",
125
- description: "Override callback URL for this payment (optional)"
126
- },
127
- passbackParams: {
128
- type: "string",
129
- description: "Data to pass back in callback (optional, max 200 chars)"
116
+ description: "Seconds until the price expires if nobody scans it (60-600). Defaults to the POI's own ttlSeconds."
130
117
  },
131
118
  merchantPartnerId: {
132
119
  type: "string",
@@ -145,18 +132,16 @@ The POI will transition to READY state and wait for a customer scan until the TT
145
132
  },
146
133
 
147
134
  async handler(args) {
148
- const { poiId, amount, currency, reasonL1, reasonL2, timeToLive, callbackUrl, passbackParams, merchantPartnerId, oauth2Token, useSandbox = true } = args;
135
+ const { poiId, amount, currency, ttlSeconds, merchantPartnerId, oauth2Token, useSandbox = true } = args;
149
136
 
150
137
  if (!poiId) throw new Error("POI ID is required");
151
138
  if (!amount) throw new Error("Amount is required");
152
139
  if (!currency) throw new Error("Currency is required");
153
- if (!reasonL1) throw new Error("Reason (reasonL1) is required");
154
140
  if (!merchantPartnerId) throw new Error("Merchant Partner ID is required");
155
141
  if (!oauth2Token) throw new Error("OAuth2 token is required");
156
142
 
157
143
  const result = await setPOIPrice({
158
- poiId, amount, currency, reasonL1, reasonL2, timeToLive, callbackUrl, passbackParams,
159
- merchantPartnerId, oauth2Token, useSandbox
144
+ poiId, amount, currency, ttlSeconds, merchantPartnerId, oauth2Token, useSandbox
160
145
  });
161
146
 
162
147
  if (result.success) {
@@ -169,19 +154,17 @@ The POI will transition to READY state and wait for a customer scan until the TT
169
154
 
170
155
  **POI ID:** ${data.poiId || poiId}
171
156
  **Status:** 🟡 ${data.status || 'READY'}
172
-
173
- **Payment Details:**
174
- - Amount: **${data.amount || amount} ${data.currency || currency}**
175
- - Reason: ${reasonL1}${reasonL2 ? ` - ${reasonL2}` : ''}
157
+ **Amount:** ${amount} ${currency.toUpperCase()}
176
158
 
177
159
  **Session:**
178
- - Token: ${data.sessionToken || 'N/A'}
179
- - Expires: ${data.sessionExpiresAt || 'N/A'}
160
+ - Token: ${data.sessionToken || 'N/A'} - keep it until the sale is final
161
+ - Price expires if not scanned: ${data.expiresAt || 'N/A'}
180
162
 
181
163
  **Next Steps:**
182
- 1. Customer scans the POI (QR code, NFC tag, or barcode)
183
- 2. Customer sees the amount and confirms payment
184
- 3. You receive a callback when payment completes
164
+ 1. The customer scans the POI (QR code, NFC tag or BLE); the transaction is created and the POI is BUSY
165
+ 2. Outcome by callback, if the POI has a callbackUrl: poi.scanned with the transaction id, then TRANSACTION_FINALIZED with the final status (both carry the poiId)
166
+ 3. Or poll payware_poi_get_status every 2-3 seconds with this sessionToken until transactionStatus is final
167
+ 4. To cancel, use payware_poi_cancel_price - possible only before the scan
185
168
 
186
169
  **ISV -> Merchant:** ${getPartnerIdSafe()} -> ${merchantPartnerId}
187
170
  **Request ID:** ${result.requestId || 'N/A'}
@@ -202,12 +185,14 @@ The POI will transition to READY state and wait for a customer scan until the TT
202
185
  **Status:** ${result.error.status || 'N/A'}
203
186
 
204
187
  **Common Issues:**
205
- - INVALID_POI_ID (400): POI ID format invalid (must be pi + 8 alphanumeric chars)
206
- - POI_NOT_FOUND (404): POI doesn't exist or doesn't belong to merchant
207
- - POI_DISABLED (409): POI is disabled
208
- - POI_NOT_IDLE (409): POI already has a pending price (cancel it first)
209
- - INVALID_AMOUNT (400): Check amount format (e.g., '25.50')
210
- - UNKNOWN_CURRENCY (400): Invalid currency code
188
+ - ERR_INVALID_POI_ID (400): POI ID format invalid (must be pi + 8 alphanumeric chars)
189
+ - ERR_POI_NOT_FOUND (404): POI doesn't exist or doesn't belong to the merchant
190
+ - ERR_SHOP_NOT_IN_SCOPE (403): the POI's shop is not assigned to you
191
+ - ERR_POI_DISABLED (409): POI is disabled
192
+ - ERR_POI_ALREADY_HAS_PENDING_PRICE (409): POI already has a pending price (cancel it first)
193
+ - ERR_INVALID_AMOUNT (400): amount finer than the currency's smallest unit
194
+ - ERR_MISSING_CURRENCY (400): currency missing or unknown
195
+ - ERR_VALIDATION_FAILED (400): amount missing or not positive, or ttlSeconds outside 60-600
211
196
 
212
197
  **Timestamp:** ${result.timestamp}`
213
198
  }]
@@ -23,10 +23,12 @@ export const simulateCallbackTool = {
23
23
  so nothing here reaches the network. What you get back is the exact payload shape a real
24
24
  TRANSACTION_FINALIZED callback carries, which is what a handler needs in order to be written.
25
25
 
26
- **Callbacks go to the MERCHANT's callbackUrl, not to yours.** An ISV that needs to know a transaction
27
- finished either has the merchant forward it, or polls
28
- \`payware_operations_get_transaction_history\` for the ids it created. Simulating the payload here
29
- tells you what the merchant's endpoint will receive; it does not route anything to you.
26
+ **Where the callback goes.** A transaction you create on a merchant's behalf calls back to the
27
+ \`callbackUrl\` you give when creating it; a POI sale calls back to the POI's \`callbackUrl\`. Both are
28
+ signed with your ISV callback key, so verify them with your own payware public key. Give no callbackUrl
29
+ and that transaction sends no callbacks at all - the merchant's default URL is not used. Without a backend, follow the
30
+ transaction with \`payware_operations_get_transaction_status\` while it is ACTIVE and then
31
+ \`payware_operations_get_transaction_history\` once, or a POI sale with \`payware_poi_get_status\`.
30
32
 
31
33
  Four properties of the real payload that are easy to get wrong:
32
34
 
@@ -38,9 +40,11 @@ Four properties of the real payload that are easy to get wrong:
38
40
  **3. \`EXPIRED\` is a non-payment, not a failure.** The payer never paid within \`timeToLive\`. Release
39
41
  the order or the stock; do not treat it as an error to retry.
40
42
 
41
- **4. Deduplicate on \`(transactionId, callbackType)\`.** payware retries once a second, up to 15
42
- times, until the endpoint answers 200. That pair is stable across retries and is the key payware
43
- guarantees.
43
+ **4. Deduplicate on \`(transactionId, callbackType)\`.** payware retries up to 15 times, backing off
44
+ from about a second to at most 5 minutes between attempts, until the endpoint answers any 2xx. That
45
+ pair is stable across retries and is the key payware guarantees.
46
+
47
+ A POI sale's callback also carries \`poiId\`, the POI that created the transaction.
44
48
 
45
49
  **Not on this payload:** \`deliveryAddress\` (removed 2026-08-07 - fetch it from
46
50
  \`payware_operations_get_transaction_history\`), \`producerPartnerId\`, \`transactionType\` and
@@ -99,6 +103,10 @@ guarantees.
99
103
  enum: ['A2A', 'CARD_FUNDED', 'BNPL', 'INSTANT_CREDIT'],
100
104
  description: 'Payment method chosen by the customer. A2A = direct transfer. CARD_FUNDED = card-linked account. BNPL = buy now pay later. INSTANT_CREDIT = credit line.'
101
105
  },
106
+ poiId: {
107
+ type: 'string',
108
+ description: 'Set to simulate a POI sale: the callback then carries the poiId of the POI that created the transaction'
109
+ },
102
110
  merchantPartnerId: {
103
111
  type: 'string',
104
112
  description: 'Partner ID of the merchant whose callback this simulates. Used for labelling only - no call is made on their behalf.'
@@ -123,6 +131,7 @@ guarantees.
123
131
  feeRate = '0.0150',
124
132
  statusMessage,
125
133
  paymentMethod,
134
+ poiId,
126
135
  merchantPartnerId
127
136
  } = args;
128
137
 
@@ -131,7 +140,7 @@ guarantees.
131
140
  }
132
141
 
133
142
  const payload = generateMockCallback(transactionId, status, {
134
- amount, currency, fee, feeFixed, feeRate, statusMessage, paymentMethod
143
+ amount, currency, fee, feeFixed, feeRate, statusMessage, paymentMethod, poiId
135
144
  });
136
145
 
137
146
  const delivery = callbackUrl ? await simulateCallbackDelivery(callbackUrl, payload) : null;
@@ -141,7 +150,7 @@ guarantees.
141
150
  type: 'text',
142
151
  text: `🔔 **Simulated Callback Payload**${merchantPartnerId ? ` (ISV ${getPartnerIdSafe()} -> Merchant ${merchantPartnerId})` : ''}
143
152
 
144
- This is what payware would POST to the **merchant's** \`callbackUrl\`. No request was made.
153
+ This is what payware would POST to the transaction's (or POI's) \`callbackUrl\`. No request was made.
145
154
 
146
155
  \`\`\`json
147
156
  ${JSON.stringify(payload, null, 2)}
@@ -153,7 +162,7 @@ ${delivery.note}
153
162
  ` : '**No callbackUrl given** - payload only.'}
154
163
 
155
164
  **Building the handler:**
156
- 1. Answer **200** or payware retries - once a second, 15 times.
165
+ 1. Answer any **2xx** or payware retries - up to 15 times, backing off from about a second to 5 minutes.
157
166
  2. Deduplicate on \`(transactionId, callbackType)\`; retries repeat the same pair.
158
167
  3. Read properties defensively: absent means absent, never zero.
159
168
  4. Treat \`EXPIRED\` as a non-payment, not an error to retry.