@garuhq/node 4.1.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/index.cjs +99 -51
- package/dist/index.d.cts +93 -33
- package/dist/index.d.ts +93 -33
- package/dist/index.js +100 -53
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
All notable changes to `@garuhq/node` are documented in this file. Format:
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [5.0.0] — 2026-09-09
|
|
7
|
+
|
|
8
|
+
**Breaking:** requests that omit `idempotencyKey` no longer carry an
|
|
9
|
+
`X-Idempotency-Key` header. No API contract changed and no gateway endpoint
|
|
10
|
+
requires the header, so most integrations need no code change — but if you were
|
|
11
|
+
relying on a key always being present, you must now pass one, which is the only
|
|
12
|
+
way it ever gave you real protection. See below.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The SDK no longer invents an idempotency key.** Every write that takes
|
|
17
|
+
`idempotencyKey` (`charges.create`, `charges.refund`, `customers.create`,
|
|
18
|
+
`products.create`, `scheduledCharges.create`, `installmentPlans.create`,
|
|
19
|
+
`installmentPlans.requestRefund`, `webhookEvents.resend`) used to generate a
|
|
20
|
+
fresh UUIDv4 when you omitted one. That protected nothing: an idempotency key
|
|
21
|
+
only works if the SAME key comes back on a retry, and a key invented per call
|
|
22
|
+
is different every time. The header is now sent only when you pass a key.
|
|
23
|
+
|
|
24
|
+
This was not theoretical. On 2026-09-08 an integrator's HTTP client timed out
|
|
25
|
+
at 30s on a card charge that was still being created, retried, drew a fresh
|
|
26
|
+
UUIDv4, and charged a real buyer twice.
|
|
27
|
+
|
|
28
|
+
Docstrings that promised "Safe to retry: the same key returns the original
|
|
29
|
+
charge for 24h" were describing a guarantee the code did not provide. They now
|
|
30
|
+
say what actually happens.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Behaviour change, no API change: requests where you omit `idempotencyKey` now
|
|
35
|
+
carry no `X-Idempotency-Key` header. Nothing rejects a missing key — the header
|
|
36
|
+
is optional on every gateway endpoint — and the gateway also gained a duplicate
|
|
37
|
+
guard that replays the original charge when the same buyer, product, rail and
|
|
38
|
+
instalment count arrive again within 60 seconds.
|
|
39
|
+
|
|
40
|
+
**To get real protection, pass a key derived from your own domain** so a retry
|
|
41
|
+
reproduces it:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
await garu.charges.create({
|
|
45
|
+
productId,
|
|
46
|
+
paymentMethod: 'creditCard',
|
|
47
|
+
customer,
|
|
48
|
+
idempotencyKey: `booking:${booking.id}:charge`
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `generateIdempotencyKey()` is still exported and unchanged. It is only useful
|
|
53
|
+
if you store the result and reuse it across retries of the same operation.
|
|
54
|
+
|
|
6
55
|
## [4.1.0] — 2026-08-22
|
|
7
56
|
|
|
8
57
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -76,6 +76,15 @@ var GaruRateLimitError = class extends GaruAPIError {
|
|
|
76
76
|
this.retryAfterSec = retryAfterSec;
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
|
+
var GaruDuplicateChargeError = class extends GaruAPIError {
|
|
80
|
+
/** How long to wait before sending the same request again. */
|
|
81
|
+
retryAfterSec;
|
|
82
|
+
constructor(code, message, status, requestId, body, retryAfterSec) {
|
|
83
|
+
super(code, message, status, requestId, body);
|
|
84
|
+
this.name = "GaruDuplicateChargeError";
|
|
85
|
+
this.retryAfterSec = retryAfterSec;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
79
88
|
var GaruServerError = class extends GaruAPIError {
|
|
80
89
|
constructor(message, status, requestId, body) {
|
|
81
90
|
super("server_error", message, status, requestId, body);
|
|
@@ -93,6 +102,13 @@ function mapApiError(status, body, requestId, retryAfterSec) {
|
|
|
93
102
|
if (status === 429) {
|
|
94
103
|
return new GaruRateLimitError(message, status, requestId, body, retryAfterSec);
|
|
95
104
|
}
|
|
105
|
+
if (status === 409) {
|
|
106
|
+
const code = readDuplicateChargeCode(body);
|
|
107
|
+
if (code) {
|
|
108
|
+
const wait = retryAfterSec ?? readRetryAfterFromBody(body) ?? DEFAULT_DUPLICATE_RETRY_SEC;
|
|
109
|
+
return new GaruDuplicateChargeError(code, message, status, requestId, body, wait);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
96
112
|
if (status >= 500) return new GaruServerError(message, status, requestId, body);
|
|
97
113
|
return new GaruAPIError("api_error", message, status, requestId, body);
|
|
98
114
|
}
|
|
@@ -105,6 +121,15 @@ function extractMessage(body) {
|
|
|
105
121
|
}
|
|
106
122
|
return null;
|
|
107
123
|
}
|
|
124
|
+
var DEFAULT_DUPLICATE_RETRY_SEC = 5;
|
|
125
|
+
function readDuplicateChargeCode(body) {
|
|
126
|
+
const code = body?.error;
|
|
127
|
+
return code === "charge_in_progress" || code === "charge_already_processed" ? code : null;
|
|
128
|
+
}
|
|
129
|
+
function readRetryAfterFromBody(body) {
|
|
130
|
+
const value = body?.retryAfter;
|
|
131
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
132
|
+
}
|
|
108
133
|
|
|
109
134
|
// src/http.ts
|
|
110
135
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
@@ -188,8 +213,8 @@ function backoffDelay(attempt, retryAfterSec) {
|
|
|
188
213
|
function sleep(ms) {
|
|
189
214
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
190
215
|
}
|
|
191
|
-
function
|
|
192
|
-
return
|
|
216
|
+
function idempotencyHeaders(key) {
|
|
217
|
+
return key ? { "X-Idempotency-Key": key } : {};
|
|
193
218
|
}
|
|
194
219
|
|
|
195
220
|
// src/resources/charges.ts
|
|
@@ -201,9 +226,11 @@ var Charges = class {
|
|
|
201
226
|
/**
|
|
202
227
|
* Create a charge (PIX, boleto, or credit card).
|
|
203
228
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
229
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
230
|
+
* original charge for 24h. Derive it from something stable in your own
|
|
231
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
232
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
233
|
+
* call is different every time and protects nothing.
|
|
207
234
|
*
|
|
208
235
|
* @example
|
|
209
236
|
* // PIX — render charge.pix.code as a QR in your own checkout
|
|
@@ -234,9 +261,22 @@ var Charges = class {
|
|
|
234
261
|
* }
|
|
235
262
|
* });
|
|
236
263
|
* // charge.amount is the base price; charge.chargedTotal is what was charged.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // Handling a duplicate. A 409 means an identical charge is already being
|
|
267
|
+
* // processed, or already went through — NOT that this one failed. Send the
|
|
268
|
+
* // same request again after the wait and you get the ORIGINAL charge back.
|
|
269
|
+
* // The SDK will not retry it for you.
|
|
270
|
+
* try {
|
|
271
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
|
|
272
|
+
* } catch (err) {
|
|
273
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
274
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
275
|
+
* // retry the same call
|
|
276
|
+
* }
|
|
277
|
+
* }
|
|
237
278
|
*/
|
|
238
279
|
async create(params) {
|
|
239
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
240
280
|
const body = {
|
|
241
281
|
productId: params.productId,
|
|
242
282
|
paymentMethod: params.paymentMethod,
|
|
@@ -245,7 +285,7 @@ var Charges = class {
|
|
|
245
285
|
if (params.card) body.card = params.card;
|
|
246
286
|
if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
|
|
247
287
|
if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
|
|
248
|
-
return this.post("/api/v1/charges", body,
|
|
288
|
+
return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
|
|
249
289
|
}
|
|
250
290
|
/**
|
|
251
291
|
* Retrieve a charge by uuid.
|
|
@@ -286,23 +326,24 @@ var Charges = class {
|
|
|
286
326
|
* settles.
|
|
287
327
|
*
|
|
288
328
|
* For Pix/boleto (which open a refund request instead of an automated
|
|
289
|
-
* reversal),
|
|
290
|
-
*
|
|
291
|
-
* card, which reverses automatically and has no manual request
|
|
292
|
-
* duplicate.
|
|
329
|
+
* reversal), pass `idempotencyKey` to make a retry return the original
|
|
330
|
+
* request rather than opening a second one; omit it and no key is sent.
|
|
331
|
+
* Ignored for card, which reverses automatically and has no manual request
|
|
332
|
+
* to duplicate.
|
|
293
333
|
*
|
|
294
334
|
* @example
|
|
295
335
|
* await garu.charges.refund('6f1c9b2e-...'); // full
|
|
296
336
|
* await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
|
|
297
337
|
*/
|
|
298
338
|
async refund(uuid, params = {}) {
|
|
299
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
300
339
|
const body = {};
|
|
301
340
|
if (params.amount !== void 0) body.amount = params.amount;
|
|
302
341
|
if (params.reason !== void 0) body.reason = params.reason;
|
|
303
|
-
return this.post(
|
|
304
|
-
|
|
305
|
-
|
|
342
|
+
return this.post(
|
|
343
|
+
`/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
|
|
344
|
+
body,
|
|
345
|
+
idempotencyHeaders(params.idempotencyKey)
|
|
346
|
+
);
|
|
306
347
|
}
|
|
307
348
|
/**
|
|
308
349
|
* Cancel an unpaid charge.
|
|
@@ -343,10 +384,16 @@ var InstallmentPlans = class {
|
|
|
343
384
|
}
|
|
344
385
|
http;
|
|
345
386
|
/**
|
|
346
|
-
* Sell a product as a carnê.
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
387
|
+
* Sell a product as a carnê.
|
|
388
|
+
*
|
|
389
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
390
|
+
* original plan for 24h. Derive it from something stable in your own
|
|
391
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
392
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
393
|
+
* call is different every time and protects nothing.
|
|
394
|
+
* This matters more here than anywhere else in the API: the call registers a
|
|
395
|
+
* REAL boleto at the bank, so a retry without a key can put two payable
|
|
396
|
+
* barcodes in one buyer's hands.
|
|
350
397
|
*
|
|
351
398
|
* @example
|
|
352
399
|
* const carne = await garu.installmentPlans.create({
|
|
@@ -372,12 +419,11 @@ var InstallmentPlans = class {
|
|
|
372
419
|
* });
|
|
373
420
|
*/
|
|
374
421
|
async create(params) {
|
|
375
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
376
422
|
const { idempotencyKey: _omit, ...body } = params;
|
|
377
423
|
return this.http.call(
|
|
378
424
|
(signal) => this.http.client.POST("/api/v1/installment-plans", {
|
|
379
425
|
body,
|
|
380
|
-
headers:
|
|
426
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
381
427
|
signal
|
|
382
428
|
}).then((r) => r)
|
|
383
429
|
);
|
|
@@ -520,10 +566,9 @@ var InstallmentPlans = class {
|
|
|
520
566
|
* team. Transfer the money to the buyer yourself, then close it with
|
|
521
567
|
* `garu.refundRequests.confirm`.
|
|
522
568
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* defense-in-depth for the request-in-flight window.
|
|
569
|
+
* Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
|
|
570
|
+
* key is sent. The backend already dedupes a second pending request for the
|
|
571
|
+
* same carnê, so this is defense-in-depth rather than the main guard.
|
|
527
572
|
*
|
|
528
573
|
* @example
|
|
529
574
|
* const request = await garu.installmentPlans.requestRefund(uuid, {
|
|
@@ -533,12 +578,11 @@ var InstallmentPlans = class {
|
|
|
533
578
|
* request.amount; // defaults to everything the carnê collected
|
|
534
579
|
*/
|
|
535
580
|
async requestRefund(uuid, params = {}) {
|
|
536
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
537
581
|
const { idempotencyKey: _omit, ...body } = params;
|
|
538
582
|
return this.http.call(
|
|
539
583
|
(signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
|
|
540
584
|
body,
|
|
541
|
-
headers:
|
|
585
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
542
586
|
signal
|
|
543
587
|
}).then((r) => r)
|
|
544
588
|
);
|
|
@@ -646,9 +690,11 @@ var Customers = class {
|
|
|
646
690
|
/**
|
|
647
691
|
* Register a customer for the current seller.
|
|
648
692
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
693
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
694
|
+
* original customer for 24h. Derive it from something stable in your own
|
|
695
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
696
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
697
|
+
* call is different every time and protects nothing.
|
|
652
698
|
*
|
|
653
699
|
* @example
|
|
654
700
|
* const customer = await garu.customers.create({
|
|
@@ -661,12 +707,11 @@ var Customers = class {
|
|
|
661
707
|
* customer.uuid;
|
|
662
708
|
*/
|
|
663
709
|
async create(params) {
|
|
664
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
665
710
|
const { idempotencyKey: _omit, ...body } = params;
|
|
666
711
|
return this.http.call(
|
|
667
712
|
(signal) => this.http.client.POST("/api/v1/customers", {
|
|
668
713
|
body,
|
|
669
|
-
headers:
|
|
714
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
670
715
|
signal
|
|
671
716
|
}).then((r) => r)
|
|
672
717
|
);
|
|
@@ -911,10 +956,11 @@ var Products = class {
|
|
|
911
956
|
* product (HTTP 201). Only `name` is required; everything else falls back
|
|
912
957
|
* to seller/server defaults.
|
|
913
958
|
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
959
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
960
|
+
* original product for 24h. Derive it from something stable in your own
|
|
961
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
962
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
963
|
+
* call is different every time and protects nothing.
|
|
918
964
|
*
|
|
919
965
|
* @example
|
|
920
966
|
* const product = await garu.products.create({
|
|
@@ -930,11 +976,10 @@ var Products = class {
|
|
|
930
976
|
*/
|
|
931
977
|
async create(params) {
|
|
932
978
|
const { idempotencyKey, ...body } = params;
|
|
933
|
-
const key = idempotencyKey ?? generateIdempotencyKey();
|
|
934
979
|
return this.http.call(
|
|
935
980
|
(signal) => this.http.client.POST("/api/v1/products", {
|
|
936
981
|
body,
|
|
937
|
-
headers:
|
|
982
|
+
headers: idempotencyHeaders(idempotencyKey),
|
|
938
983
|
signal
|
|
939
984
|
}).then((r) => r)
|
|
940
985
|
);
|
|
@@ -970,10 +1015,15 @@ var ScheduledCharges = class {
|
|
|
970
1015
|
}
|
|
971
1016
|
http;
|
|
972
1017
|
/**
|
|
973
|
-
* Create a new scheduled charge.
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
* series for 24h
|
|
1018
|
+
* Create a new scheduled charge.
|
|
1019
|
+
*
|
|
1020
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1021
|
+
* original series for 24h. Derive it from something stable in your own
|
|
1022
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1023
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1024
|
+
* call is different every time and protects nothing.
|
|
1025
|
+
* Worth the effort here: without a key, a retry double-books recurring
|
|
1026
|
+
* billing for the same customer.
|
|
977
1027
|
*
|
|
978
1028
|
* @example
|
|
979
1029
|
* const charge = await garu.scheduledCharges.create({
|
|
@@ -1000,12 +1050,11 @@ var ScheduledCharges = class {
|
|
|
1000
1050
|
* });
|
|
1001
1051
|
*/
|
|
1002
1052
|
async create(params) {
|
|
1003
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
1004
1053
|
const { idempotencyKey: _omit, ...body } = params;
|
|
1005
1054
|
return this.http.call(
|
|
1006
1055
|
(signal) => this.http.client.POST("/api/v1/scheduled-charges", {
|
|
1007
1056
|
body,
|
|
1008
|
-
headers:
|
|
1057
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
1009
1058
|
signal
|
|
1010
1059
|
}).then((r) => r)
|
|
1011
1060
|
);
|
|
@@ -1394,11 +1443,10 @@ var WebhookEvents = class {
|
|
|
1394
1443
|
* original — distinguishable both by the `resend_` prefix and by reading
|
|
1395
1444
|
* the response payload's `manualResendOf` field.
|
|
1396
1445
|
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
* your own retry-suppression if that matters for your integration.
|
|
1446
|
+
* `idempotencyKey` is forwarded when you pass one and omitted when you don't.
|
|
1447
|
+
* Either way the gateway does not currently deduplicate `/resend` calls
|
|
1448
|
+
* against it, so retrying after a network failure can create more than one
|
|
1449
|
+
* clone — pair it with your own retry-suppression if that matters.
|
|
1402
1450
|
*
|
|
1403
1451
|
* Returns the *clone* event (new uuid), not the original. The original is
|
|
1404
1452
|
* unchanged on the server.
|
|
@@ -1410,11 +1458,10 @@ var WebhookEvents = class {
|
|
|
1410
1458
|
* clone.manualResendOf === event.uuid; // true — points back at the source
|
|
1411
1459
|
*/
|
|
1412
1460
|
async resend(uuid, params = {}) {
|
|
1413
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
1414
1461
|
return this.http.call(
|
|
1415
1462
|
(signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
|
|
1416
1463
|
body: {},
|
|
1417
|
-
headers:
|
|
1464
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
1418
1465
|
signal
|
|
1419
1466
|
}).then((r) => r)
|
|
1420
1467
|
);
|
|
@@ -1521,6 +1568,7 @@ exports.Garu = Garu;
|
|
|
1521
1568
|
exports.GaruAPIError = GaruAPIError;
|
|
1522
1569
|
exports.GaruAuthenticationError = GaruAuthenticationError;
|
|
1523
1570
|
exports.GaruConnectionError = GaruConnectionError;
|
|
1571
|
+
exports.GaruDuplicateChargeError = GaruDuplicateChargeError;
|
|
1524
1572
|
exports.GaruError = GaruError;
|
|
1525
1573
|
exports.GaruNotFoundError = GaruNotFoundError;
|
|
1526
1574
|
exports.GaruPermissionError = GaruPermissionError;
|
package/dist/index.d.cts
CHANGED
|
@@ -1100,9 +1100,11 @@ declare class Charges {
|
|
|
1100
1100
|
/**
|
|
1101
1101
|
* Create a charge (PIX, boleto, or credit card).
|
|
1102
1102
|
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1103
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1104
|
+
* original charge for 24h. Derive it from something stable in your own
|
|
1105
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1106
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1107
|
+
* call is different every time and protects nothing.
|
|
1106
1108
|
*
|
|
1107
1109
|
* @example
|
|
1108
1110
|
* // PIX — render charge.pix.code as a QR in your own checkout
|
|
@@ -1133,6 +1135,20 @@ declare class Charges {
|
|
|
1133
1135
|
* }
|
|
1134
1136
|
* });
|
|
1135
1137
|
* // charge.amount is the base price; charge.chargedTotal is what was charged.
|
|
1138
|
+
*
|
|
1139
|
+
* @example
|
|
1140
|
+
* // Handling a duplicate. A 409 means an identical charge is already being
|
|
1141
|
+
* // processed, or already went through — NOT that this one failed. Send the
|
|
1142
|
+
* // same request again after the wait and you get the ORIGINAL charge back.
|
|
1143
|
+
* // The SDK will not retry it for you.
|
|
1144
|
+
* try {
|
|
1145
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
|
|
1146
|
+
* } catch (err) {
|
|
1147
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
1148
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
1149
|
+
* // retry the same call
|
|
1150
|
+
* }
|
|
1151
|
+
* }
|
|
1136
1152
|
*/
|
|
1137
1153
|
create(params: CreateChargeParams): Promise<Charge>;
|
|
1138
1154
|
/**
|
|
@@ -1159,10 +1175,10 @@ declare class Charges {
|
|
|
1159
1175
|
* settles.
|
|
1160
1176
|
*
|
|
1161
1177
|
* For Pix/boleto (which open a refund request instead of an automated
|
|
1162
|
-
* reversal),
|
|
1163
|
-
*
|
|
1164
|
-
* card, which reverses automatically and has no manual request
|
|
1165
|
-
* duplicate.
|
|
1178
|
+
* reversal), pass `idempotencyKey` to make a retry return the original
|
|
1179
|
+
* request rather than opening a second one; omit it and no key is sent.
|
|
1180
|
+
* Ignored for card, which reverses automatically and has no manual request
|
|
1181
|
+
* to duplicate.
|
|
1166
1182
|
*
|
|
1167
1183
|
* @example
|
|
1168
1184
|
* await garu.charges.refund('6f1c9b2e-...'); // full
|
|
@@ -1196,10 +1212,16 @@ declare class InstallmentPlans {
|
|
|
1196
1212
|
private readonly http;
|
|
1197
1213
|
constructor(http: HttpClient);
|
|
1198
1214
|
/**
|
|
1199
|
-
* Sell a product as a carnê.
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1215
|
+
* Sell a product as a carnê.
|
|
1216
|
+
*
|
|
1217
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1218
|
+
* original plan for 24h. Derive it from something stable in your own
|
|
1219
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1220
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1221
|
+
* call is different every time and protects nothing.
|
|
1222
|
+
* This matters more here than anywhere else in the API: the call registers a
|
|
1223
|
+
* REAL boleto at the bank, so a retry without a key can put two payable
|
|
1224
|
+
* barcodes in one buyer's hands.
|
|
1203
1225
|
*
|
|
1204
1226
|
* @example
|
|
1205
1227
|
* const carne = await garu.installmentPlans.create({
|
|
@@ -1310,10 +1332,9 @@ declare class InstallmentPlans {
|
|
|
1310
1332
|
* team. Transfer the money to the buyer yourself, then close it with
|
|
1311
1333
|
* `garu.refundRequests.confirm`.
|
|
1312
1334
|
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* defense-in-depth for the request-in-flight window.
|
|
1335
|
+
* Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
|
|
1336
|
+
* key is sent. The backend already dedupes a second pending request for the
|
|
1337
|
+
* same carnê, so this is defense-in-depth rather than the main guard.
|
|
1317
1338
|
*
|
|
1318
1339
|
* @example
|
|
1319
1340
|
* const request = await garu.installmentPlans.requestRefund(uuid, {
|
|
@@ -1404,9 +1425,11 @@ declare class Customers {
|
|
|
1404
1425
|
/**
|
|
1405
1426
|
* Register a customer for the current seller.
|
|
1406
1427
|
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1428
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1429
|
+
* original customer for 24h. Derive it from something stable in your own
|
|
1430
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1431
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1432
|
+
* call is different every time and protects nothing.
|
|
1410
1433
|
*
|
|
1411
1434
|
* @example
|
|
1412
1435
|
* const customer = await garu.customers.create({
|
|
@@ -1576,10 +1599,11 @@ declare class Products {
|
|
|
1576
1599
|
* product (HTTP 201). Only `name` is required; everything else falls back
|
|
1577
1600
|
* to seller/server defaults.
|
|
1578
1601
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1602
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1603
|
+
* original product for 24h. Derive it from something stable in your own
|
|
1604
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1605
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1606
|
+
* call is different every time and protects nothing.
|
|
1583
1607
|
*
|
|
1584
1608
|
* @example
|
|
1585
1609
|
* const product = await garu.products.create({
|
|
@@ -1629,10 +1653,15 @@ declare class ScheduledCharges {
|
|
|
1629
1653
|
private readonly http;
|
|
1630
1654
|
constructor(http: HttpClient);
|
|
1631
1655
|
/**
|
|
1632
|
-
* Create a new scheduled charge.
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
* series for 24h
|
|
1656
|
+
* Create a new scheduled charge.
|
|
1657
|
+
*
|
|
1658
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1659
|
+
* original series for 24h. Derive it from something stable in your own
|
|
1660
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1661
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1662
|
+
* call is different every time and protects nothing.
|
|
1663
|
+
* Worth the effort here: without a key, a retry double-books recurring
|
|
1664
|
+
* billing for the same customer.
|
|
1636
1665
|
*
|
|
1637
1666
|
* @example
|
|
1638
1667
|
* const charge = await garu.scheduledCharges.create({
|
|
@@ -1899,11 +1928,10 @@ declare class WebhookEvents {
|
|
|
1899
1928
|
* original — distinguishable both by the `resend_` prefix and by reading
|
|
1900
1929
|
* the response payload's `manualResendOf` field.
|
|
1901
1930
|
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
1906
|
-
* your own retry-suppression if that matters for your integration.
|
|
1931
|
+
* `idempotencyKey` is forwarded when you pass one and omitted when you don't.
|
|
1932
|
+
* Either way the gateway does not currently deduplicate `/resend` calls
|
|
1933
|
+
* against it, so retrying after a network failure can create more than one
|
|
1934
|
+
* clone — pair it with your own retry-suppression if that matters.
|
|
1907
1935
|
*
|
|
1908
1936
|
* Returns the *clone* event (new uuid), not the original. The original is
|
|
1909
1937
|
* unchanged on the server.
|
|
@@ -1982,7 +2010,7 @@ declare class Garu {
|
|
|
1982
2010
|
* without parsing messages. Non-2xx API responses are mapped to the most specific
|
|
1983
2011
|
* subclass of `GaruAPIError` by {@link mapApiError}.
|
|
1984
2012
|
*/
|
|
1985
|
-
type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
|
|
2013
|
+
type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'charge_in_progress' | 'charge_already_processed' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
|
|
1986
2014
|
declare class GaruError extends Error {
|
|
1987
2015
|
readonly code: GaruErrorCode;
|
|
1988
2016
|
constructor(code: GaruErrorCode, message: string);
|
|
@@ -2016,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
|
|
|
2016
2044
|
readonly retryAfterSec: number | null;
|
|
2017
2045
|
constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
|
|
2018
2046
|
}
|
|
2047
|
+
/**
|
|
2048
|
+
* A charge identical to this one — same buyer, product, rail, amount and
|
|
2049
|
+
* instalment count — is already being processed, or already went through inside
|
|
2050
|
+
* the gateway's duplicate window.
|
|
2051
|
+
*
|
|
2052
|
+
* **This is not a failure.** The buyer's money is either on its way or already
|
|
2053
|
+
* taken. Wait `retryAfterSec` and send the same request again: the retry is
|
|
2054
|
+
* answered with the ORIGINAL charge rather than creating a second one.
|
|
2055
|
+
*
|
|
2056
|
+
* The SDK does not retry this for you. Re-POSTing to a money endpoint on your
|
|
2057
|
+
* behalf is exactly the kind of hidden behaviour 5.0.0 removed, and if your own
|
|
2058
|
+
* client also retries, the two stack.
|
|
2059
|
+
*
|
|
2060
|
+
* The real fix is upstream: pass `idempotencyKey` derived from something stable
|
|
2061
|
+
* in your domain, so a retry reproduces it. This error is the backstop for when
|
|
2062
|
+
* that has not happened.
|
|
2063
|
+
*
|
|
2064
|
+
* @example
|
|
2065
|
+
* try {
|
|
2066
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer });
|
|
2067
|
+
* } catch (err) {
|
|
2068
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
2069
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
2070
|
+
* // Sending it again returns the original charge.
|
|
2071
|
+
* }
|
|
2072
|
+
* }
|
|
2073
|
+
*/
|
|
2074
|
+
declare class GaruDuplicateChargeError extends GaruAPIError {
|
|
2075
|
+
/** How long to wait before sending the same request again. */
|
|
2076
|
+
readonly retryAfterSec: number;
|
|
2077
|
+
constructor(code: 'charge_in_progress' | 'charge_already_processed', message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number);
|
|
2078
|
+
}
|
|
2019
2079
|
declare class GaruServerError extends GaruAPIError {
|
|
2020
2080
|
constructor(message: string, status: number, requestId: string | null, body: unknown);
|
|
2021
2081
|
}
|
|
2022
2082
|
|
|
2023
|
-
export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
|
|
2083
|
+
export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
|
package/dist/index.d.ts
CHANGED
|
@@ -1100,9 +1100,11 @@ declare class Charges {
|
|
|
1100
1100
|
/**
|
|
1101
1101
|
* Create a charge (PIX, boleto, or credit card).
|
|
1102
1102
|
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1103
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1104
|
+
* original charge for 24h. Derive it from something stable in your own
|
|
1105
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1106
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1107
|
+
* call is different every time and protects nothing.
|
|
1106
1108
|
*
|
|
1107
1109
|
* @example
|
|
1108
1110
|
* // PIX — render charge.pix.code as a QR in your own checkout
|
|
@@ -1133,6 +1135,20 @@ declare class Charges {
|
|
|
1133
1135
|
* }
|
|
1134
1136
|
* });
|
|
1135
1137
|
* // charge.amount is the base price; charge.chargedTotal is what was charged.
|
|
1138
|
+
*
|
|
1139
|
+
* @example
|
|
1140
|
+
* // Handling a duplicate. A 409 means an identical charge is already being
|
|
1141
|
+
* // processed, or already went through — NOT that this one failed. Send the
|
|
1142
|
+
* // same request again after the wait and you get the ORIGINAL charge back.
|
|
1143
|
+
* // The SDK will not retry it for you.
|
|
1144
|
+
* try {
|
|
1145
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
|
|
1146
|
+
* } catch (err) {
|
|
1147
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
1148
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
1149
|
+
* // retry the same call
|
|
1150
|
+
* }
|
|
1151
|
+
* }
|
|
1136
1152
|
*/
|
|
1137
1153
|
create(params: CreateChargeParams): Promise<Charge>;
|
|
1138
1154
|
/**
|
|
@@ -1159,10 +1175,10 @@ declare class Charges {
|
|
|
1159
1175
|
* settles.
|
|
1160
1176
|
*
|
|
1161
1177
|
* For Pix/boleto (which open a refund request instead of an automated
|
|
1162
|
-
* reversal),
|
|
1163
|
-
*
|
|
1164
|
-
* card, which reverses automatically and has no manual request
|
|
1165
|
-
* duplicate.
|
|
1178
|
+
* reversal), pass `idempotencyKey` to make a retry return the original
|
|
1179
|
+
* request rather than opening a second one; omit it and no key is sent.
|
|
1180
|
+
* Ignored for card, which reverses automatically and has no manual request
|
|
1181
|
+
* to duplicate.
|
|
1166
1182
|
*
|
|
1167
1183
|
* @example
|
|
1168
1184
|
* await garu.charges.refund('6f1c9b2e-...'); // full
|
|
@@ -1196,10 +1212,16 @@ declare class InstallmentPlans {
|
|
|
1196
1212
|
private readonly http;
|
|
1197
1213
|
constructor(http: HttpClient);
|
|
1198
1214
|
/**
|
|
1199
|
-
* Sell a product as a carnê.
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1215
|
+
* Sell a product as a carnê.
|
|
1216
|
+
*
|
|
1217
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1218
|
+
* original plan for 24h. Derive it from something stable in your own
|
|
1219
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1220
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1221
|
+
* call is different every time and protects nothing.
|
|
1222
|
+
* This matters more here than anywhere else in the API: the call registers a
|
|
1223
|
+
* REAL boleto at the bank, so a retry without a key can put two payable
|
|
1224
|
+
* barcodes in one buyer's hands.
|
|
1203
1225
|
*
|
|
1204
1226
|
* @example
|
|
1205
1227
|
* const carne = await garu.installmentPlans.create({
|
|
@@ -1310,10 +1332,9 @@ declare class InstallmentPlans {
|
|
|
1310
1332
|
* team. Transfer the money to the buyer yourself, then close it with
|
|
1311
1333
|
* `garu.refundRequests.confirm`.
|
|
1312
1334
|
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* defense-in-depth for the request-in-flight window.
|
|
1335
|
+
* Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
|
|
1336
|
+
* key is sent. The backend already dedupes a second pending request for the
|
|
1337
|
+
* same carnê, so this is defense-in-depth rather than the main guard.
|
|
1317
1338
|
*
|
|
1318
1339
|
* @example
|
|
1319
1340
|
* const request = await garu.installmentPlans.requestRefund(uuid, {
|
|
@@ -1404,9 +1425,11 @@ declare class Customers {
|
|
|
1404
1425
|
/**
|
|
1405
1426
|
* Register a customer for the current seller.
|
|
1406
1427
|
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1428
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1429
|
+
* original customer for 24h. Derive it from something stable in your own
|
|
1430
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1431
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1432
|
+
* call is different every time and protects nothing.
|
|
1410
1433
|
*
|
|
1411
1434
|
* @example
|
|
1412
1435
|
* const customer = await garu.customers.create({
|
|
@@ -1576,10 +1599,11 @@ declare class Products {
|
|
|
1576
1599
|
* product (HTTP 201). Only `name` is required; everything else falls back
|
|
1577
1600
|
* to seller/server defaults.
|
|
1578
1601
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1602
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1603
|
+
* original product for 24h. Derive it from something stable in your own
|
|
1604
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1605
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1606
|
+
* call is different every time and protects nothing.
|
|
1583
1607
|
*
|
|
1584
1608
|
* @example
|
|
1585
1609
|
* const product = await garu.products.create({
|
|
@@ -1629,10 +1653,15 @@ declare class ScheduledCharges {
|
|
|
1629
1653
|
private readonly http;
|
|
1630
1654
|
constructor(http: HttpClient);
|
|
1631
1655
|
/**
|
|
1632
|
-
* Create a new scheduled charge.
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
* series for 24h
|
|
1656
|
+
* Create a new scheduled charge.
|
|
1657
|
+
*
|
|
1658
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1659
|
+
* original series for 24h. Derive it from something stable in your own
|
|
1660
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1661
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1662
|
+
* call is different every time and protects nothing.
|
|
1663
|
+
* Worth the effort here: without a key, a retry double-books recurring
|
|
1664
|
+
* billing for the same customer.
|
|
1636
1665
|
*
|
|
1637
1666
|
* @example
|
|
1638
1667
|
* const charge = await garu.scheduledCharges.create({
|
|
@@ -1899,11 +1928,10 @@ declare class WebhookEvents {
|
|
|
1899
1928
|
* original — distinguishable both by the `resend_` prefix and by reading
|
|
1900
1929
|
* the response payload's `manualResendOf` field.
|
|
1901
1930
|
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
1906
|
-
* your own retry-suppression if that matters for your integration.
|
|
1931
|
+
* `idempotencyKey` is forwarded when you pass one and omitted when you don't.
|
|
1932
|
+
* Either way the gateway does not currently deduplicate `/resend` calls
|
|
1933
|
+
* against it, so retrying after a network failure can create more than one
|
|
1934
|
+
* clone — pair it with your own retry-suppression if that matters.
|
|
1907
1935
|
*
|
|
1908
1936
|
* Returns the *clone* event (new uuid), not the original. The original is
|
|
1909
1937
|
* unchanged on the server.
|
|
@@ -1982,7 +2010,7 @@ declare class Garu {
|
|
|
1982
2010
|
* without parsing messages. Non-2xx API responses are mapped to the most specific
|
|
1983
2011
|
* subclass of `GaruAPIError` by {@link mapApiError}.
|
|
1984
2012
|
*/
|
|
1985
|
-
type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
|
|
2013
|
+
type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'charge_in_progress' | 'charge_already_processed' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
|
|
1986
2014
|
declare class GaruError extends Error {
|
|
1987
2015
|
readonly code: GaruErrorCode;
|
|
1988
2016
|
constructor(code: GaruErrorCode, message: string);
|
|
@@ -2016,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
|
|
|
2016
2044
|
readonly retryAfterSec: number | null;
|
|
2017
2045
|
constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
|
|
2018
2046
|
}
|
|
2047
|
+
/**
|
|
2048
|
+
* A charge identical to this one — same buyer, product, rail, amount and
|
|
2049
|
+
* instalment count — is already being processed, or already went through inside
|
|
2050
|
+
* the gateway's duplicate window.
|
|
2051
|
+
*
|
|
2052
|
+
* **This is not a failure.** The buyer's money is either on its way or already
|
|
2053
|
+
* taken. Wait `retryAfterSec` and send the same request again: the retry is
|
|
2054
|
+
* answered with the ORIGINAL charge rather than creating a second one.
|
|
2055
|
+
*
|
|
2056
|
+
* The SDK does not retry this for you. Re-POSTing to a money endpoint on your
|
|
2057
|
+
* behalf is exactly the kind of hidden behaviour 5.0.0 removed, and if your own
|
|
2058
|
+
* client also retries, the two stack.
|
|
2059
|
+
*
|
|
2060
|
+
* The real fix is upstream: pass `idempotencyKey` derived from something stable
|
|
2061
|
+
* in your domain, so a retry reproduces it. This error is the backstop for when
|
|
2062
|
+
* that has not happened.
|
|
2063
|
+
*
|
|
2064
|
+
* @example
|
|
2065
|
+
* try {
|
|
2066
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer });
|
|
2067
|
+
* } catch (err) {
|
|
2068
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
2069
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
2070
|
+
* // Sending it again returns the original charge.
|
|
2071
|
+
* }
|
|
2072
|
+
* }
|
|
2073
|
+
*/
|
|
2074
|
+
declare class GaruDuplicateChargeError extends GaruAPIError {
|
|
2075
|
+
/** How long to wait before sending the same request again. */
|
|
2076
|
+
readonly retryAfterSec: number;
|
|
2077
|
+
constructor(code: 'charge_in_progress' | 'charge_already_processed', message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number);
|
|
2078
|
+
}
|
|
2019
2079
|
declare class GaruServerError extends GaruAPIError {
|
|
2020
2080
|
constructor(message: string, status: number, requestId: string | null, body: unknown);
|
|
2021
2081
|
}
|
|
2022
2082
|
|
|
2023
|
-
export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
|
|
2083
|
+
export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createClient from 'openapi-fetch';
|
|
2
|
-
import { createHmac, timingSafeEqual
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
3
3
|
|
|
4
4
|
// src/http.ts
|
|
5
5
|
|
|
@@ -70,6 +70,15 @@ var GaruRateLimitError = class extends GaruAPIError {
|
|
|
70
70
|
this.retryAfterSec = retryAfterSec;
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
+
var GaruDuplicateChargeError = class extends GaruAPIError {
|
|
74
|
+
/** How long to wait before sending the same request again. */
|
|
75
|
+
retryAfterSec;
|
|
76
|
+
constructor(code, message, status, requestId, body, retryAfterSec) {
|
|
77
|
+
super(code, message, status, requestId, body);
|
|
78
|
+
this.name = "GaruDuplicateChargeError";
|
|
79
|
+
this.retryAfterSec = retryAfterSec;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
73
82
|
var GaruServerError = class extends GaruAPIError {
|
|
74
83
|
constructor(message, status, requestId, body) {
|
|
75
84
|
super("server_error", message, status, requestId, body);
|
|
@@ -87,6 +96,13 @@ function mapApiError(status, body, requestId, retryAfterSec) {
|
|
|
87
96
|
if (status === 429) {
|
|
88
97
|
return new GaruRateLimitError(message, status, requestId, body, retryAfterSec);
|
|
89
98
|
}
|
|
99
|
+
if (status === 409) {
|
|
100
|
+
const code = readDuplicateChargeCode(body);
|
|
101
|
+
if (code) {
|
|
102
|
+
const wait = retryAfterSec ?? readRetryAfterFromBody(body) ?? DEFAULT_DUPLICATE_RETRY_SEC;
|
|
103
|
+
return new GaruDuplicateChargeError(code, message, status, requestId, body, wait);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
if (status >= 500) return new GaruServerError(message, status, requestId, body);
|
|
91
107
|
return new GaruAPIError("api_error", message, status, requestId, body);
|
|
92
108
|
}
|
|
@@ -99,6 +115,15 @@ function extractMessage(body) {
|
|
|
99
115
|
}
|
|
100
116
|
return null;
|
|
101
117
|
}
|
|
118
|
+
var DEFAULT_DUPLICATE_RETRY_SEC = 5;
|
|
119
|
+
function readDuplicateChargeCode(body) {
|
|
120
|
+
const code = body?.error;
|
|
121
|
+
return code === "charge_in_progress" || code === "charge_already_processed" ? code : null;
|
|
122
|
+
}
|
|
123
|
+
function readRetryAfterFromBody(body) {
|
|
124
|
+
const value = body?.retryAfter;
|
|
125
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
// src/http.ts
|
|
104
129
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
@@ -182,8 +207,8 @@ function backoffDelay(attempt, retryAfterSec) {
|
|
|
182
207
|
function sleep(ms) {
|
|
183
208
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
209
|
}
|
|
185
|
-
function
|
|
186
|
-
return
|
|
210
|
+
function idempotencyHeaders(key) {
|
|
211
|
+
return key ? { "X-Idempotency-Key": key } : {};
|
|
187
212
|
}
|
|
188
213
|
|
|
189
214
|
// src/resources/charges.ts
|
|
@@ -195,9 +220,11 @@ var Charges = class {
|
|
|
195
220
|
/**
|
|
196
221
|
* Create a charge (PIX, boleto, or credit card).
|
|
197
222
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
223
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
224
|
+
* original charge for 24h. Derive it from something stable in your own
|
|
225
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
226
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
227
|
+
* call is different every time and protects nothing.
|
|
201
228
|
*
|
|
202
229
|
* @example
|
|
203
230
|
* // PIX — render charge.pix.code as a QR in your own checkout
|
|
@@ -228,9 +255,22 @@ var Charges = class {
|
|
|
228
255
|
* }
|
|
229
256
|
* });
|
|
230
257
|
* // charge.amount is the base price; charge.chargedTotal is what was charged.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* // Handling a duplicate. A 409 means an identical charge is already being
|
|
261
|
+
* // processed, or already went through — NOT that this one failed. Send the
|
|
262
|
+
* // same request again after the wait and you get the ORIGINAL charge back.
|
|
263
|
+
* // The SDK will not retry it for you.
|
|
264
|
+
* try {
|
|
265
|
+
* await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
|
|
266
|
+
* } catch (err) {
|
|
267
|
+
* if (err instanceof GaruDuplicateChargeError) {
|
|
268
|
+
* await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
|
|
269
|
+
* // retry the same call
|
|
270
|
+
* }
|
|
271
|
+
* }
|
|
231
272
|
*/
|
|
232
273
|
async create(params) {
|
|
233
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
234
274
|
const body = {
|
|
235
275
|
productId: params.productId,
|
|
236
276
|
paymentMethod: params.paymentMethod,
|
|
@@ -239,7 +279,7 @@ var Charges = class {
|
|
|
239
279
|
if (params.card) body.card = params.card;
|
|
240
280
|
if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
|
|
241
281
|
if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
|
|
242
|
-
return this.post("/api/v1/charges", body,
|
|
282
|
+
return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
|
|
243
283
|
}
|
|
244
284
|
/**
|
|
245
285
|
* Retrieve a charge by uuid.
|
|
@@ -280,23 +320,24 @@ var Charges = class {
|
|
|
280
320
|
* settles.
|
|
281
321
|
*
|
|
282
322
|
* For Pix/boleto (which open a refund request instead of an automated
|
|
283
|
-
* reversal),
|
|
284
|
-
*
|
|
285
|
-
* card, which reverses automatically and has no manual request
|
|
286
|
-
* duplicate.
|
|
323
|
+
* reversal), pass `idempotencyKey` to make a retry return the original
|
|
324
|
+
* request rather than opening a second one; omit it and no key is sent.
|
|
325
|
+
* Ignored for card, which reverses automatically and has no manual request
|
|
326
|
+
* to duplicate.
|
|
287
327
|
*
|
|
288
328
|
* @example
|
|
289
329
|
* await garu.charges.refund('6f1c9b2e-...'); // full
|
|
290
330
|
* await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
|
|
291
331
|
*/
|
|
292
332
|
async refund(uuid, params = {}) {
|
|
293
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
294
333
|
const body = {};
|
|
295
334
|
if (params.amount !== void 0) body.amount = params.amount;
|
|
296
335
|
if (params.reason !== void 0) body.reason = params.reason;
|
|
297
|
-
return this.post(
|
|
298
|
-
|
|
299
|
-
|
|
336
|
+
return this.post(
|
|
337
|
+
`/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
|
|
338
|
+
body,
|
|
339
|
+
idempotencyHeaders(params.idempotencyKey)
|
|
340
|
+
);
|
|
300
341
|
}
|
|
301
342
|
/**
|
|
302
343
|
* Cancel an unpaid charge.
|
|
@@ -337,10 +378,16 @@ var InstallmentPlans = class {
|
|
|
337
378
|
}
|
|
338
379
|
http;
|
|
339
380
|
/**
|
|
340
|
-
* Sell a product as a carnê.
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
381
|
+
* Sell a product as a carnê.
|
|
382
|
+
*
|
|
383
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
384
|
+
* original plan for 24h. Derive it from something stable in your own
|
|
385
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
386
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
387
|
+
* call is different every time and protects nothing.
|
|
388
|
+
* This matters more here than anywhere else in the API: the call registers a
|
|
389
|
+
* REAL boleto at the bank, so a retry without a key can put two payable
|
|
390
|
+
* barcodes in one buyer's hands.
|
|
344
391
|
*
|
|
345
392
|
* @example
|
|
346
393
|
* const carne = await garu.installmentPlans.create({
|
|
@@ -366,12 +413,11 @@ var InstallmentPlans = class {
|
|
|
366
413
|
* });
|
|
367
414
|
*/
|
|
368
415
|
async create(params) {
|
|
369
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
370
416
|
const { idempotencyKey: _omit, ...body } = params;
|
|
371
417
|
return this.http.call(
|
|
372
418
|
(signal) => this.http.client.POST("/api/v1/installment-plans", {
|
|
373
419
|
body,
|
|
374
|
-
headers:
|
|
420
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
375
421
|
signal
|
|
376
422
|
}).then((r) => r)
|
|
377
423
|
);
|
|
@@ -514,10 +560,9 @@ var InstallmentPlans = class {
|
|
|
514
560
|
* team. Transfer the money to the buyer yourself, then close it with
|
|
515
561
|
* `garu.refundRequests.confirm`.
|
|
516
562
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* defense-in-depth for the request-in-flight window.
|
|
563
|
+
* Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
|
|
564
|
+
* key is sent. The backend already dedupes a second pending request for the
|
|
565
|
+
* same carnê, so this is defense-in-depth rather than the main guard.
|
|
521
566
|
*
|
|
522
567
|
* @example
|
|
523
568
|
* const request = await garu.installmentPlans.requestRefund(uuid, {
|
|
@@ -527,12 +572,11 @@ var InstallmentPlans = class {
|
|
|
527
572
|
* request.amount; // defaults to everything the carnê collected
|
|
528
573
|
*/
|
|
529
574
|
async requestRefund(uuid, params = {}) {
|
|
530
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
531
575
|
const { idempotencyKey: _omit, ...body } = params;
|
|
532
576
|
return this.http.call(
|
|
533
577
|
(signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
|
|
534
578
|
body,
|
|
535
|
-
headers:
|
|
579
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
536
580
|
signal
|
|
537
581
|
}).then((r) => r)
|
|
538
582
|
);
|
|
@@ -640,9 +684,11 @@ var Customers = class {
|
|
|
640
684
|
/**
|
|
641
685
|
* Register a customer for the current seller.
|
|
642
686
|
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
687
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
688
|
+
* original customer for 24h. Derive it from something stable in your own
|
|
689
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
690
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
691
|
+
* call is different every time and protects nothing.
|
|
646
692
|
*
|
|
647
693
|
* @example
|
|
648
694
|
* const customer = await garu.customers.create({
|
|
@@ -655,12 +701,11 @@ var Customers = class {
|
|
|
655
701
|
* customer.uuid;
|
|
656
702
|
*/
|
|
657
703
|
async create(params) {
|
|
658
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
659
704
|
const { idempotencyKey: _omit, ...body } = params;
|
|
660
705
|
return this.http.call(
|
|
661
706
|
(signal) => this.http.client.POST("/api/v1/customers", {
|
|
662
707
|
body,
|
|
663
|
-
headers:
|
|
708
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
664
709
|
signal
|
|
665
710
|
}).then((r) => r)
|
|
666
711
|
);
|
|
@@ -905,10 +950,11 @@ var Products = class {
|
|
|
905
950
|
* product (HTTP 201). Only `name` is required; everything else falls back
|
|
906
951
|
* to seller/server defaults.
|
|
907
952
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
953
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
954
|
+
* original product for 24h. Derive it from something stable in your own
|
|
955
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
956
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
957
|
+
* call is different every time and protects nothing.
|
|
912
958
|
*
|
|
913
959
|
* @example
|
|
914
960
|
* const product = await garu.products.create({
|
|
@@ -924,11 +970,10 @@ var Products = class {
|
|
|
924
970
|
*/
|
|
925
971
|
async create(params) {
|
|
926
972
|
const { idempotencyKey, ...body } = params;
|
|
927
|
-
const key = idempotencyKey ?? generateIdempotencyKey();
|
|
928
973
|
return this.http.call(
|
|
929
974
|
(signal) => this.http.client.POST("/api/v1/products", {
|
|
930
975
|
body,
|
|
931
|
-
headers:
|
|
976
|
+
headers: idempotencyHeaders(idempotencyKey),
|
|
932
977
|
signal
|
|
933
978
|
}).then((r) => r)
|
|
934
979
|
);
|
|
@@ -964,10 +1009,15 @@ var ScheduledCharges = class {
|
|
|
964
1009
|
}
|
|
965
1010
|
http;
|
|
966
1011
|
/**
|
|
967
|
-
* Create a new scheduled charge.
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
* series for 24h
|
|
1012
|
+
* Create a new scheduled charge.
|
|
1013
|
+
*
|
|
1014
|
+
* Pass `idempotencyKey` to make this safe to retry: the same key returns the
|
|
1015
|
+
* original series for 24h. Derive it from something stable in your own
|
|
1016
|
+
* domain (an order id, a booking id) so a retry reproduces it. Omit it and
|
|
1017
|
+
* no key is sent — the SDK does NOT invent one, because a key generated per
|
|
1018
|
+
* call is different every time and protects nothing.
|
|
1019
|
+
* Worth the effort here: without a key, a retry double-books recurring
|
|
1020
|
+
* billing for the same customer.
|
|
971
1021
|
*
|
|
972
1022
|
* @example
|
|
973
1023
|
* const charge = await garu.scheduledCharges.create({
|
|
@@ -994,12 +1044,11 @@ var ScheduledCharges = class {
|
|
|
994
1044
|
* });
|
|
995
1045
|
*/
|
|
996
1046
|
async create(params) {
|
|
997
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
998
1047
|
const { idempotencyKey: _omit, ...body } = params;
|
|
999
1048
|
return this.http.call(
|
|
1000
1049
|
(signal) => this.http.client.POST("/api/v1/scheduled-charges", {
|
|
1001
1050
|
body,
|
|
1002
|
-
headers:
|
|
1051
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
1003
1052
|
signal
|
|
1004
1053
|
}).then((r) => r)
|
|
1005
1054
|
);
|
|
@@ -1388,11 +1437,10 @@ var WebhookEvents = class {
|
|
|
1388
1437
|
* original — distinguishable both by the `resend_` prefix and by reading
|
|
1389
1438
|
* the response payload's `manualResendOf` field.
|
|
1390
1439
|
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* your own retry-suppression if that matters for your integration.
|
|
1440
|
+
* `idempotencyKey` is forwarded when you pass one and omitted when you don't.
|
|
1441
|
+
* Either way the gateway does not currently deduplicate `/resend` calls
|
|
1442
|
+
* against it, so retrying after a network failure can create more than one
|
|
1443
|
+
* clone — pair it with your own retry-suppression if that matters.
|
|
1396
1444
|
*
|
|
1397
1445
|
* Returns the *clone* event (new uuid), not the original. The original is
|
|
1398
1446
|
* unchanged on the server.
|
|
@@ -1404,11 +1452,10 @@ var WebhookEvents = class {
|
|
|
1404
1452
|
* clone.manualResendOf === event.uuid; // true — points back at the source
|
|
1405
1453
|
*/
|
|
1406
1454
|
async resend(uuid, params = {}) {
|
|
1407
|
-
const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
|
|
1408
1455
|
return this.http.call(
|
|
1409
1456
|
(signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
|
|
1410
1457
|
body: {},
|
|
1411
|
-
headers:
|
|
1458
|
+
headers: idempotencyHeaders(params.idempotencyKey),
|
|
1412
1459
|
signal
|
|
1413
1460
|
}).then((r) => r)
|
|
1414
1461
|
);
|
|
@@ -1511,4 +1558,4 @@ var Garu = class {
|
|
|
1511
1558
|
}
|
|
1512
1559
|
};
|
|
1513
1560
|
|
|
1514
|
-
export { Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, GaruNotFoundError, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, webhooks };
|
|
1561
|
+
export { Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, GaruNotFoundError, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, webhooks };
|