@myzonerocks/pact 0.1.7 → 0.1.8

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.
@@ -153,12 +153,12 @@ export class MpesaLeg {
153
153
  },
154
154
  ];
155
155
  }
156
- // The amount Daraja collected must equal the amount we authorized; a partial
157
- // or tampered collection settles nothing.
158
- const paid = metadataInt(cb.CallbackMetadata?.Item ?? [], "Amount");
159
- if (paid !== rec.amount) {
160
- throw new Error(`mpesa: confirmed amount ${paid} does not match the authorized ${rec.amount}`);
161
- }
156
+ // The STK push fixed the amount we authorized; a payer approves that exact amount
157
+ // or cancels, so an approved collection is always the full authorized amount. The
158
+ // authenticated query above confirms success, so the settled amount is the one we
159
+ // recorded it is never read from the unsigned callback, since a forged callback
160
+ // (the checkout id is not a secret) could otherwise carry a wrong amount and block
161
+ // a collection the payer completed. The receipt is an audit reference only.
162
162
  const receipt = metadataString(cb.CallbackMetadata?.Item ?? [], "MpesaReceiptNumber");
163
163
  return [
164
164
  {
@@ -185,19 +185,6 @@ function wholeShillings(m) {
185
185
  }
186
186
  return Number(amount);
187
187
  }
188
- // metadataInt pulls a named numeric value out of the callback metadata items,
189
- // used to read the paid Amount. M-Pesa amounts are whole shillings but may arrive
190
- // as a number with a fractional part, so it is floored to the shilling.
191
- function metadataInt(items, name) {
192
- for (const item of items) {
193
- if (item.Name !== name) {
194
- continue;
195
- }
196
- const n = Number(item.Value);
197
- return Number.isFinite(n) ? Math.floor(n) : 0;
198
- }
199
- return 0;
200
- }
201
188
  // metadataString pulls a named string value out of the callback metadata items.
202
189
  function metadataString(items, name) {
203
190
  for (const item of items) {
@@ -201,13 +201,17 @@ describe("mpesa pay-in", () => {
201
201
  const events = await leg.parseWebhook(successCallback(collected.providerRef, "FORGEDRCPT"), {});
202
202
  expect(events).toHaveLength(0);
203
203
  });
204
- it("settles nothing when the callback amount differs from the authorized quote", async () => {
204
+ it("a forged callback amount cannot block a confirmed collection", async () => {
205
205
  const api = new FakeDaraja(counter("tx"));
206
206
  const leg = buildLeg(api);
207
207
  const q = quote({ srcAmount: kes("5000"), fees: kes("0") });
208
208
  const collected = await leg.collect("intent-1", q, emptyAuth, "0711000111");
209
- // Daraja confirms success, but the callback body claims only 1500 was paid.
209
+ // Daraja's authenticated query confirms success; the callback body claims a
210
+ // different (forged) amount, which must be ignored — the collection still settles.
210
211
  api.queryResults.set(collected.providerRef, { resultCode: 0, resultDesc: "", pending: false });
211
- await expect(leg.parseWebhook(successCallback(collected.providerRef, "QGR7XYZ123"), {})).rejects.toThrow();
212
+ const events = await leg.parseWebhook(successCallback(collected.providerRef, "QGR7XYZ123"), {});
213
+ expect(events).toHaveLength(1);
214
+ expect(events[0].state).toBe(State.Settled);
215
+ expect(events[0].providerTxRef).toBe("QGR7XYZ123");
212
216
  });
213
217
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myzonerocks/pact",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "TypeScript SDK for the PACT payment abstraction protocol",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -270,13 +270,12 @@ export class MpesaLeg implements PayInLeg, PayOutLeg {
270
270
  },
271
271
  ];
272
272
  }
273
- // The amount Daraja collected must equal the amount we authorized; a partial
274
- // or tampered collection settles nothing.
275
- const paid = metadataInt(cb.CallbackMetadata?.Item ?? [], "Amount");
276
- if (paid !== rec.amount) {
277
- throw new Error(`mpesa: confirmed amount ${paid} does not match the authorized ${rec.amount}`);
278
- }
279
-
273
+ // The STK push fixed the amount we authorized; a payer approves that exact amount
274
+ // or cancels, so an approved collection is always the full authorized amount. The
275
+ // authenticated query above confirms success, so the settled amount is the one we
276
+ // recorded it is never read from the unsigned callback, since a forged callback
277
+ // (the checkout id is not a secret) could otherwise carry a wrong amount and block
278
+ // a collection the payer completed. The receipt is an audit reference only.
280
279
  const receipt = metadataString(cb.CallbackMetadata?.Item ?? [], "MpesaReceiptNumber");
281
280
  return [
282
281
  {
@@ -321,20 +320,6 @@ function wholeShillings(m: Money): number {
321
320
  return Number(amount);
322
321
  }
323
322
 
324
- // metadataInt pulls a named numeric value out of the callback metadata items,
325
- // used to read the paid Amount. M-Pesa amounts are whole shillings but may arrive
326
- // as a number with a fractional part, so it is floored to the shilling.
327
- function metadataInt(items: StkCallbackItem[], name: string): number {
328
- for (const item of items) {
329
- if (item.Name !== name) {
330
- continue;
331
- }
332
- const n = Number(item.Value);
333
- return Number.isFinite(n) ? Math.floor(n) : 0;
334
- }
335
- return 0;
336
- }
337
-
338
323
  // metadataString pulls a named string value out of the callback metadata items.
339
324
  function metadataString(items: StkCallbackItem[], name: string): string {
340
325
  for (const item of items) {