@garuhq/node 5.0.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/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]);
@@ -236,6 +261,20 @@ var Charges = class {
236
261
  * }
237
262
  * });
238
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
+ * }
239
278
  */
240
279
  async create(params) {
241
280
  const body = {
@@ -300,7 +339,11 @@ var Charges = class {
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(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, idempotencyHeaders(params.idempotencyKey));
342
+ return this.post(
343
+ `/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
344
+ body,
345
+ idempotencyHeaders(params.idempotencyKey)
346
+ );
304
347
  }
305
348
  /**
306
349
  * Cancel an unpaid charge.
@@ -1525,6 +1568,7 @@ exports.Garu = Garu;
1525
1568
  exports.GaruAPIError = GaruAPIError;
1526
1569
  exports.GaruAuthenticationError = GaruAuthenticationError;
1527
1570
  exports.GaruConnectionError = GaruConnectionError;
1571
+ exports.GaruDuplicateChargeError = GaruDuplicateChargeError;
1528
1572
  exports.GaruError = GaruError;
1529
1573
  exports.GaruNotFoundError = GaruNotFoundError;
1530
1574
  exports.GaruPermissionError = GaruPermissionError;
package/dist/index.d.cts CHANGED
@@ -1135,6 +1135,20 @@ declare class Charges {
1135
1135
  * }
1136
1136
  * });
1137
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
+ * }
1138
1152
  */
1139
1153
  create(params: CreateChargeParams): Promise<Charge>;
1140
1154
  /**
@@ -1996,7 +2010,7 @@ declare class Garu {
1996
2010
  * without parsing messages. Non-2xx API responses are mapped to the most specific
1997
2011
  * subclass of `GaruAPIError` by {@link mapApiError}.
1998
2012
  */
1999
- 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';
2000
2014
  declare class GaruError extends Error {
2001
2015
  readonly code: GaruErrorCode;
2002
2016
  constructor(code: GaruErrorCode, message: string);
@@ -2030,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
2030
2044
  readonly retryAfterSec: number | null;
2031
2045
  constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
2032
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
+ }
2033
2079
  declare class GaruServerError extends GaruAPIError {
2034
2080
  constructor(message: string, status: number, requestId: string | null, body: unknown);
2035
2081
  }
2036
2082
 
2037
- 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
@@ -1135,6 +1135,20 @@ declare class Charges {
1135
1135
  * }
1136
1136
  * });
1137
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
+ * }
1138
1152
  */
1139
1153
  create(params: CreateChargeParams): Promise<Charge>;
1140
1154
  /**
@@ -1996,7 +2010,7 @@ declare class Garu {
1996
2010
  * without parsing messages. Non-2xx API responses are mapped to the most specific
1997
2011
  * subclass of `GaruAPIError` by {@link mapApiError}.
1998
2012
  */
1999
- 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';
2000
2014
  declare class GaruError extends Error {
2001
2015
  readonly code: GaruErrorCode;
2002
2016
  constructor(code: GaruErrorCode, message: string);
@@ -2030,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
2030
2044
  readonly retryAfterSec: number | null;
2031
2045
  constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
2032
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
+ }
2033
2079
  declare class GaruServerError extends GaruAPIError {
2034
2080
  constructor(message: string, status: number, requestId: string | null, body: unknown);
2035
2081
  }
2036
2082
 
2037
- 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
@@ -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]);
@@ -230,6 +255,20 @@ var Charges = class {
230
255
  * }
231
256
  * });
232
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
+ * }
233
272
  */
234
273
  async create(params) {
235
274
  const body = {
@@ -294,7 +333,11 @@ var Charges = class {
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(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, idempotencyHeaders(params.idempotencyKey));
336
+ return this.post(
337
+ `/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
338
+ body,
339
+ idempotencyHeaders(params.idempotencyKey)
340
+ );
298
341
  }
299
342
  /**
300
343
  * Cancel an unpaid charge.
@@ -1515,4 +1558,4 @@ var Garu = class {
1515
1558
  }
1516
1559
  };
1517
1560
 
1518
- 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garuhq/node",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Official Node.js / TypeScript SDK for the Garu payment gateway.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://garu.com.br",