@thepayulink/server 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,12 @@ toRupees(19950); // 199.5
75
75
 
76
76
  ### Payouts
77
77
 
78
+ Payouts, `balance.fetch()` and `transactions.list()` need a **live key with Payouts access**
79
+ and an **IP allowlist** (merchant panel → API Key → *Edit access*). Otherwise the API
80
+ answers `403 KEY_NOT_PERMITTED`, `403 IP_NOT_ALLOWED`, or `400 TEST_MODE_UNSUPPORTED` for a
81
+ test key. Keep that key only on the server that sends payouts; use a separate Checkout key
82
+ for orders.
83
+
78
84
  ```js
79
85
  const payout = await pl.payouts.create({
80
86
  amount: rupees(2500), // PAISE; UPI payouts must be whole rupees
package/dist/index.d.ts CHANGED
@@ -172,6 +172,7 @@ export default class PayuLink {
172
172
  list(params?: ListParams & { status?: string; receipt?: string; utr?: string }): Promise<Collection<Order>>;
173
173
  };
174
174
 
175
+ /** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
175
176
  payouts: {
176
177
  create(params: CreatePayoutParams): Promise<Payout>;
177
178
  fetch(payoutId: string): Promise<Payout>;
@@ -180,10 +181,12 @@ export default class PayuLink {
180
181
  cancel(payoutId: string, params?: { reason?: string }): Promise<Payout>;
181
182
  };
182
183
 
184
+ /** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
183
185
  balance: {
184
186
  fetch(): Promise<Balance>;
185
187
  };
186
188
 
189
+ /** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
187
190
  transactions: {
188
191
  /** type: comma-separated names, e.g. "payout_lock,payout_debit" */
189
192
  list(params?: ListParams & { type?: string }): Promise<Collection<Transaction>>;
@@ -37,7 +37,7 @@ __export(src_exports, {
37
37
  module.exports = __toCommonJS(src_exports);
38
38
  var import_node_crypto = __toESM(require("node:crypto"), 1);
39
39
  var rupees = (r) => Math.round(Number(r) * 100);
40
- var toRupees = (paise) => Number(paise) / 100;
40
+ var toRupees = (paise2) => Number(paise2) / 100;
41
41
  var PayuLinkError = class extends Error {
42
42
  constructor(message, { status, code, body } = {}) {
43
43
  super(message);
@@ -62,6 +62,15 @@ function safeEqual(a, b) {
62
62
  const bb = Buffer.from(String(b ?? ""));
63
63
  return ba.length === bb.length && ba.length > 0 && import_node_crypto.default.timingSafeEqual(ba, bb);
64
64
  }
65
+ function paise(value) {
66
+ const n = Number(value);
67
+ const whole = Math.round(n);
68
+ if (!Number.isFinite(n) || Math.abs(n - whole) > 1e-6) {
69
+ throw new PayuLinkError("`amount` must be a whole number of PAISE (19950 = \u20B9199.50). Tip: use the `rupees()` helper \u2014 rupees(199.5) === 19950.");
70
+ }
71
+ if (whole < 100) throw new PayuLinkError("`amount` must be at least 100 paise (\u20B91).");
72
+ return whole;
73
+ }
65
74
  var PayuLink = class {
66
75
  /**
67
76
  * @param {object} opts
@@ -161,10 +170,7 @@ var PayuLink = class {
161
170
  * backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
162
171
  */
163
172
  _createOrder(p = {}) {
164
- const amount = Math.round(Number(p.amount));
165
- if (!Number.isFinite(amount) || amount < 100) {
166
- throw new PayuLinkError("`amount` is required, in PAISE, and must be >= 100 (\u20B91). Tip: use the `rupees()` helper \u2014 rupees(199.5) === 19950.");
167
- }
173
+ const amount = paise(p.amount);
168
174
  return this._request("POST", "/v1/orders", {
169
175
  body: {
170
176
  amount,
@@ -188,10 +194,7 @@ var PayuLink = class {
188
194
  * @param {string} [p.narration]
189
195
  */
190
196
  _createPayout(p = {}) {
191
- const amount = Math.round(Number(p.amount));
192
- if (!Number.isFinite(amount) || amount < 100) {
193
- throw new PayuLinkError("`amount` is required, in PAISE, and must be >= 100 (\u20B91).");
194
- }
197
+ const amount = paise(p.amount);
195
198
  if (p.mode !== "bank" && p.mode !== "upi") throw new PayuLinkError('`mode` must be "bank" or "upi".');
196
199
  if (!p.beneficiary) throw new PayuLinkError("`beneficiary` is required.");
197
200
  return this._request("POST", "/v1/payouts", {
@@ -289,4 +292,4 @@ var PayuLink = class {
289
292
  rupees,
290
293
  toRupees
291
294
  });
292
- (()=>{const ns=module.exports,C=ns.default;if(C){C.PayulinkError=ns.PayulinkError;C.Payulink=C;C.default=C;module.exports=C;}})();
295
+ (()=>{const ns=module.exports,C=ns.default;if(C){for(const k of Object.keys(ns))if(k!=="default")C[k]=ns[k];C.PayuLink=C;C.Payulink=C;C.PayulinkError=ns.PayuLinkError;C.default=C;module.exports=C;}})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepayulink/server",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "PayuLink server SDK — create orders and verify payments/webhooks with your key_secret.",
5
5
  "type": "module",
6
6
  "main": "./dist/payulink-server.cjs",
package/src/index.js CHANGED
@@ -47,6 +47,22 @@ function safeEqual(a, b) {
47
47
  return ba.length === bb.length && ba.length > 0 && crypto.timingSafeEqual(ba, bb);
48
48
  }
49
49
 
50
+ /**
51
+ * A whole number of paise, >= 100. Floating-point noise (19.99 * 100 === 1998.9999999999998)
52
+ * is rounded away; a real fraction (199.5) is refused rather than silently rounded, since it
53
+ * usually means rupees were passed where paise were expected. The API refuses it too.
54
+ */
55
+ function paise(value) {
56
+ const n = Number(value);
57
+ const whole = Math.round(n);
58
+ if (!Number.isFinite(n) || Math.abs(n - whole) > 1e-6) {
59
+ throw new PayuLinkError('`amount` must be a whole number of PAISE (19950 = ₹199.50). '
60
+ + 'Tip: use the `rupees()` helper — rupees(199.5) === 19950.');
61
+ }
62
+ if (whole < 100) throw new PayuLinkError('`amount` must be at least 100 paise (₹1).');
63
+ return whole;
64
+ }
65
+
50
66
  export default class PayuLink {
51
67
  /**
52
68
  * @param {object} opts
@@ -147,11 +163,7 @@ export default class PayuLink {
147
163
  * backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
148
164
  */
149
165
  _createOrder(p = {}) {
150
- const amount = Math.round(Number(p.amount));
151
- if (!Number.isFinite(amount) || amount < 100) {
152
- throw new PayuLinkError('`amount` is required, in PAISE, and must be >= 100 (₹1). '
153
- + 'Tip: use the `rupees()` helper — rupees(199.5) === 19950.');
154
- }
166
+ const amount = paise(p.amount);
155
167
  return this._request('POST', '/v1/orders', {
156
168
  body: {
157
169
  amount,
@@ -176,10 +188,7 @@ export default class PayuLink {
176
188
  * @param {string} [p.narration]
177
189
  */
178
190
  _createPayout(p = {}) {
179
- const amount = Math.round(Number(p.amount));
180
- if (!Number.isFinite(amount) || amount < 100) {
181
- throw new PayuLinkError('`amount` is required, in PAISE, and must be >= 100 (₹1).');
182
- }
191
+ const amount = paise(p.amount);
183
192
  if (p.mode !== 'bank' && p.mode !== 'upi') throw new PayuLinkError('`mode` must be "bank" or "upi".');
184
193
  if (!p.beneficiary) throw new PayuLinkError('`beneficiary` is required.');
185
194
  return this._request('POST', '/v1/payouts', {