@nile-squad/nylonpay-ts 1.0.9 → 1.0.10

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.d.cts CHANGED
@@ -172,6 +172,12 @@ type Transaction = {
172
172
  type: TransactionType;
173
173
  method: PaymentMethod;
174
174
  description: string;
175
+ /**
176
+ * Present (true) when this response replayed an existing transaction because
177
+ * the reference was already used — same reference, same transaction. No new
178
+ * payment was initiated; use a fresh reference to start a new one.
179
+ */
180
+ duplicate?: boolean;
175
181
  phone: string;
176
182
  email: string | null;
177
183
  failureReason: string | null;
@@ -321,12 +327,15 @@ type NylonPayConfig = {
321
327
  * - `rate_limit` — too many requests.
322
328
  * - `account` — merchant account missing or not active.
323
329
  * - `provider` — payment provider/engine rejected the operation.
330
+ * - `duplicate` — the reference was already used for another transaction.
331
+ * Retrying with a NEW reference will pass; reusing the same reference
332
+ * replays the existing transaction instead of charging again.
324
333
  * - `not_found` — referenced transaction does not exist.
325
334
  * - `internal` — unexpected server-side failure.
326
335
  * - `network` — request never reached the server (DNS, TLS, connection).
327
336
  * - `timeout` — request exceeded the configured timeout.
328
337
  */
329
- type SdkErrorCategory = "auth" | "validation" | "limit" | "rate_limit" | "account" | "provider" | "not_found" | "internal" | "network" | "timeout";
338
+ type SdkErrorCategory = "auth" | "validation" | "limit" | "rate_limit" | "account" | "provider" | "duplicate" | "not_found" | "internal" | "network" | "timeout";
330
339
  /**
331
340
  * Structured error returned by SDK operations. `category` is machine-readable
332
341
  * for branching logic; `message` is human-readable for logs and alerts.
package/dist/index.d.ts CHANGED
@@ -172,6 +172,12 @@ type Transaction = {
172
172
  type: TransactionType;
173
173
  method: PaymentMethod;
174
174
  description: string;
175
+ /**
176
+ * Present (true) when this response replayed an existing transaction because
177
+ * the reference was already used — same reference, same transaction. No new
178
+ * payment was initiated; use a fresh reference to start a new one.
179
+ */
180
+ duplicate?: boolean;
175
181
  phone: string;
176
182
  email: string | null;
177
183
  failureReason: string | null;
@@ -321,12 +327,15 @@ type NylonPayConfig = {
321
327
  * - `rate_limit` — too many requests.
322
328
  * - `account` — merchant account missing or not active.
323
329
  * - `provider` — payment provider/engine rejected the operation.
330
+ * - `duplicate` — the reference was already used for another transaction.
331
+ * Retrying with a NEW reference will pass; reusing the same reference
332
+ * replays the existing transaction instead of charging again.
324
333
  * - `not_found` — referenced transaction does not exist.
325
334
  * - `internal` — unexpected server-side failure.
326
335
  * - `network` — request never reached the server (DNS, TLS, connection).
327
336
  * - `timeout` — request exceeded the configured timeout.
328
337
  */
329
- type SdkErrorCategory = "auth" | "validation" | "limit" | "rate_limit" | "account" | "provider" | "not_found" | "internal" | "network" | "timeout";
338
+ type SdkErrorCategory = "auth" | "validation" | "limit" | "rate_limit" | "account" | "provider" | "duplicate" | "not_found" | "internal" | "network" | "timeout";
330
339
  /**
331
340
  * Structured error returned by SDK operations. `category` is machine-readable
332
341
  * for branching logic; `message` is human-readable for logs and alerts.
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
147
147
  "rate_limit",
148
148
  "account",
149
149
  "provider",
150
+ "duplicate",
150
151
  "not_found",
151
152
  "internal",
152
153
  "network",
@@ -593,6 +594,15 @@ function createPaymentInstance(initialResponse, deps) {
593
594
  }
594
595
  return paymentInstance;
595
596
  }
597
+
598
+ // src/phone.ts
599
+ function normalizePhone(phone) {
600
+ let normalized = phone.replace(/\s+/g, "").replace(/^\+/, "");
601
+ if (normalized.startsWith("0") && normalized.length === 10) {
602
+ normalized = `256${normalized.slice(1)}`;
603
+ }
604
+ return normalized;
605
+ }
596
606
  var DEFAULT_TOLERANCE_SECONDS = 300;
597
607
  function decodePayload(payload) {
598
608
  return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
@@ -668,10 +678,21 @@ async function runHook(hook, ...args) {
668
678
  function throwValidation(message) {
669
679
  throw createSdkError({ category: "validation", message });
670
680
  }
671
- function validateAmount(amount) {
681
+ function validateCollectionAmount(amount) {
682
+ if (!Number.isInteger(amount) || amount <= 0) {
683
+ throwValidation("amount must be a positive integer");
684
+ }
685
+ if (amount < 500) {
686
+ throwValidation("Collection amount must be at least 500 UGX");
687
+ }
688
+ }
689
+ function validatePayoutAmount(amount) {
672
690
  if (!Number.isInteger(amount) || amount <= 0) {
673
691
  throwValidation("amount must be a positive integer");
674
692
  }
693
+ if (amount < 5e3) {
694
+ throwValidation("Payout amount must be at least 5000 UGX");
695
+ }
675
696
  }
676
697
  function validateNonEmpty(value, fieldName) {
677
698
  if (!value || value.trim() === "") {
@@ -702,14 +723,19 @@ function createSdkInstance(config) {
702
723
  };
703
724
  async function collectPayment(input) {
704
725
  const reference = resolveReference(input.reference);
705
- validateAmount(input.amount);
726
+ validateCollectionAmount(input.amount);
706
727
  validateNonEmpty(input.customer.name, "customer.name");
707
728
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
729
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
708
730
  validateNonEmpty(input.description, "description");
709
731
  if (input.method === "bank" && !input.bank) {
710
732
  throwValidation('bank details are required when method is "bank"');
711
733
  }
712
- let payload = { ...input, reference };
734
+ let payload = {
735
+ ...input,
736
+ reference,
737
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
738
+ };
713
739
  const mutated = await runHook(config.hooks?.beforeCollect, payload);
714
740
  if (mutated != null)
715
741
  payload = { ...mutated, reference: mutated.reference ?? reference };
@@ -733,14 +759,19 @@ function createSdkInstance(config) {
733
759
  }
734
760
  async function collectPaymentAndResolve(input) {
735
761
  const reference = resolveReference(input.reference);
736
- validateAmount(input.amount);
762
+ validateCollectionAmount(input.amount);
737
763
  validateNonEmpty(input.customer.name, "customer.name");
738
764
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
765
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
739
766
  validateNonEmpty(input.description, "description");
740
767
  if (input.method === "bank" && !input.bank) {
741
768
  throwValidation('bank details are required when method is "bank"');
742
769
  }
743
- let payload = { ...input, reference };
770
+ let payload = {
771
+ ...input,
772
+ reference,
773
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
774
+ };
744
775
  const mutated = await runHook(config.hooks?.beforeCollect, payload);
745
776
  if (mutated != null)
746
777
  payload = { ...mutated, reference: mutated.reference ?? reference };
@@ -760,9 +791,10 @@ function createSdkInstance(config) {
760
791
  }
761
792
  async function makePayout(input) {
762
793
  const reference = resolveReference(input.reference);
763
- validateAmount(input.amount);
794
+ validatePayoutAmount(input.amount);
764
795
  validateNonEmpty(input.customer.name, "customer.name");
765
796
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
797
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
766
798
  validateNonEmpty(input.description, "description");
767
799
  validateNonEmpty(
768
800
  input.destination.accountHolderName,
@@ -772,7 +804,11 @@ function createSdkInstance(config) {
772
804
  input.destination.accountNumber,
773
805
  "destination.accountNumber"
774
806
  );
775
- let payload = { ...input, reference };
807
+ let payload = {
808
+ ...input,
809
+ reference,
810
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
811
+ };
776
812
  const mutated = await runHook(config.hooks?.beforePayout, payload);
777
813
  if (mutated != null)
778
814
  payload = { ...mutated, reference: mutated.reference ?? reference };
@@ -796,9 +832,10 @@ function createSdkInstance(config) {
796
832
  }
797
833
  async function makePayoutAndResolve(input) {
798
834
  const reference = resolveReference(input.reference);
799
- validateAmount(input.amount);
835
+ validatePayoutAmount(input.amount);
800
836
  validateNonEmpty(input.customer.name, "customer.name");
801
837
  validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
838
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
802
839
  validateNonEmpty(input.description, "description");
803
840
  validateNonEmpty(
804
841
  input.destination.accountHolderName,
@@ -808,7 +845,11 @@ function createSdkInstance(config) {
808
845
  input.destination.accountNumber,
809
846
  "destination.accountNumber"
810
847
  );
811
- let payload = { ...input, reference };
848
+ let payload = {
849
+ ...input,
850
+ reference,
851
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
852
+ };
812
853
  const mutated = await runHook(config.hooks?.beforePayout, payload);
813
854
  if (mutated != null)
814
855
  payload = { ...mutated, reference: mutated.reference ?? reference };
@@ -852,9 +893,10 @@ function createSdkInstance(config) {
852
893
  }
853
894
  async function verifyPhone(input) {
854
895
  validateNonEmpty(input.phoneNumber, "phoneNumber");
896
+ const normalizedPhone = normalizePhone(input.phoneNumber);
855
897
  const result = await transport.send({
856
898
  action: SDK_ACTIONS.verifyPhone,
857
- payload: input
899
+ payload: { ...input, phoneNumber: normalizedPhone }
858
900
  });
859
901
  if (result.isOk) {
860
902
  return Ok(result.value);
@@ -863,7 +905,7 @@ function createSdkInstance(config) {
863
905
  }
864
906
  async function createInvoice(input) {
865
907
  const reference = resolveReference(input.reference);
866
- validateAmount(input.amount);
908
+ validateCollectionAmount(input.amount);
867
909
  validateNonEmpty(input.description, "description");
868
910
  if (input.items) {
869
911
  if (input.items.length > 50) {