@p2pdotme/sdk 1.2.19 → 1.2.21

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.
@@ -71,13 +71,16 @@ type ParseResult = Result<ParsedQR, QRParserError>;
71
71
  * a signed JWT whose `valor.original` field holds the amount. All other parsers
72
72
  * (UPI, QRIS, QR Ph, MercadoPago, PagoMovil, Transfermóvil) and static PIX are synchronous and
73
73
  * resolve immediately.
74
+ *
75
+ * When the country has `validateQr` (PEN, VEN, BOB), Scan & Pay must pass the
76
+ * same check as SELL upload — not a looser envelope.
74
77
  */
75
78
  declare function parseQR(params: ParseQRParams): Promise<ParseResult>;
76
79
 
77
80
  /**
78
- * Envelope check for a Pago Móvil / Suiche 7B scan: opaque `base64?…`.
79
- * PAY stores this whole string as the payment ID so the merchant can
80
- * re-encode it as a QR. Stricter SELL upload rules live in `validateVenezuelanQr`.
81
+ * Same envelope as SELL upload: opaque `base64?merchantId=NNNN` (blob ≥ 40).
82
+ * Prefer `validateVenezuelanQr` from `@p2pdotme/sdk/country`; this alias stays
83
+ * for `@p2pdotme/sdk/qr-parsers` callers.
81
84
  */
82
85
  declare function isPagoMovilQr(qrData: string): boolean;
83
86
 
@@ -71,13 +71,16 @@ type ParseResult = Result<ParsedQR, QRParserError>;
71
71
  * a signed JWT whose `valor.original` field holds the amount. All other parsers
72
72
  * (UPI, QRIS, QR Ph, MercadoPago, PagoMovil, Transfermóvil) and static PIX are synchronous and
73
73
  * resolve immediately.
74
+ *
75
+ * When the country has `validateQr` (PEN, VEN, BOB), Scan & Pay must pass the
76
+ * same check as SELL upload — not a looser envelope.
74
77
  */
75
78
  declare function parseQR(params: ParseQRParams): Promise<ParseResult>;
76
79
 
77
80
  /**
78
- * Envelope check for a Pago Móvil / Suiche 7B scan: opaque `base64?…`.
79
- * PAY stores this whole string as the payment ID so the merchant can
80
- * re-encode it as a QR. Stricter SELL upload rules live in `validateVenezuelanQr`.
81
+ * Same envelope as SELL upload: opaque `base64?merchantId=NNNN` (blob ≥ 40).
82
+ * Prefer `validateVenezuelanQr` from `@p2pdotme/sdk/country`; this alias stays
83
+ * for `@p2pdotme/sdk/qr-parsers` callers.
81
84
  */
82
85
  declare function isPagoMovilQr(qrData: string): boolean;
83
86
 
@@ -120,6 +120,39 @@ function validateVenezuelanQr(payload) {
120
120
  if (!/^[A-Za-z0-9+/=]+$/.test(blob)) return false;
121
121
  return /(?:^|[?&])merchantId=\d{3,4}(?:&|$)/.test(trimmed.substring(qIdx));
122
122
  }
123
+ function payQrCandidate(paymentId) {
124
+ const trimmed = paymentId.trim();
125
+ const sep = trimmed.indexOf(PACKED_PAYMENT_ID_SEP);
126
+ return (sep >= 0 ? trimmed.slice(0, sep) : trimmed).trim();
127
+ }
128
+ function isPeruvianPayQr(payload) {
129
+ if (!payload.startsWith("0002")) return false;
130
+ return payload.includes("5802PE") && payload.includes("5303604");
131
+ }
132
+ function isVenezuelanPayQr(payload) {
133
+ if (!payload || payload.includes(PACKED_PAYMENT_ID_SEP)) return false;
134
+ const qIdx = payload.indexOf("?");
135
+ if (qIdx === -1) return false;
136
+ const blob = payload.slice(0, qIdx);
137
+ return blob.length > 0 && /^[A-Za-z0-9+/=]+$/.test(blob);
138
+ }
139
+ function isPhilippinePayQr(payload) {
140
+ if (!payload.startsWith("0002")) return false;
141
+ return payload.includes("5802PH") && payload.includes("5303608");
142
+ }
143
+ function isBolivianPayQr(payload) {
144
+ if (!payload || payload.includes(PACKED_PAYMENT_ID_SEP)) return false;
145
+ if (payload.startsWith("0002") && payload.includes("5802BO") && payload.includes("5303068")) {
146
+ return true;
147
+ }
148
+ const sep = payload.lastIndexOf("|");
149
+ if (sep < 40) return false;
150
+ const blob = payload.slice(0, sep);
151
+ const tag = payload.slice(sep + 1);
152
+ if (blob.length % 4 !== 0) return false;
153
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(blob)) return false;
154
+ return /^[0-9A-Fa-f]{16,64}$/.test(tag);
155
+ }
123
156
 
124
157
  // src/country/currencies/bob.ts
125
158
  var BOB_COUNTRY_OPTION = {
@@ -135,7 +168,7 @@ var BOB_COUNTRY_OPTION = {
135
168
  flag: "\u{1F1E7}\u{1F1F4}",
136
169
  flagUrl: "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f1e7-1f1f4.png",
137
170
  phoneCode: "+591",
138
- telegramSupportChannel: "https://t.me/p2pmebolivia",
171
+ telegramSupportChannel: "https://t.me/p2pme_bolivia_merchants",
139
172
  twitterUsername: "p2pmebolivia",
140
173
  smsCountryCodes: ["BO"],
141
174
  precision: 2,
@@ -143,7 +176,11 @@ var BOB_COUNTRY_OPTION = {
143
176
  disabled: false,
144
177
  disabledPaymentTypes: [],
145
178
  uploadPaymentQR: true,
146
- validateQr: validateBolivianQr
179
+ validateQr: validateBolivianQr,
180
+ getPayQrPayload: (paymentId) => {
181
+ const candidate = payQrCandidate(paymentId);
182
+ return isBolivianPayQr(candidate) ? candidate : null;
183
+ }
147
184
  };
148
185
 
149
186
  // src/country/currencies/brl.ts
@@ -209,7 +246,8 @@ var CUP_COUNTRY_OPTION = {
209
246
  precision: 2,
210
247
  isAlpha: true,
211
248
  disabled: false,
212
- disabledPaymentTypes: []
249
+ disabledPaymentTypes: [],
250
+ transferWarning: "CUP_BANDEC_WARNING"
213
251
  };
214
252
 
215
253
  // src/country/currencies/ecu.ts
@@ -417,6 +455,10 @@ var PEN_COUNTRY_OPTION = {
417
455
  disabledPaymentTypes: [],
418
456
  uploadPaymentQR: true,
419
457
  validateQr: validatePeruvianQr,
458
+ getPayQrPayload: (paymentId) => {
459
+ const candidate = payQrCandidate(paymentId);
460
+ return isPeruvianPayQr(candidate) ? candidate : null;
461
+ },
420
462
  hydrateFieldsFromQr: (qr) => {
421
463
  const phone = extractPeruvianPhoneFromQr(qr);
422
464
  return phone ? { phone } : {};
@@ -442,7 +484,11 @@ var PHP_COUNTRY_OPTION = {
442
484
  precision: 2,
443
485
  isAlpha: true,
444
486
  disabled: false,
445
- disabledPaymentTypes: []
487
+ disabledPaymentTypes: [],
488
+ getPayQrPayload: (paymentId) => {
489
+ const candidate = payQrCandidate(paymentId);
490
+ return isPhilippinePayQr(candidate) ? candidate : null;
491
+ }
446
492
  };
447
493
 
448
494
  // src/country/currencies/usd.ts
@@ -489,7 +535,11 @@ var VEN_COUNTRY_OPTION = {
489
535
  disabled: false,
490
536
  disabledPaymentTypes: [],
491
537
  uploadPaymentQR: true,
492
- validateQr: validateVenezuelanQr
538
+ validateQr: validateVenezuelanQr,
539
+ getPayQrPayload: (paymentId) => {
540
+ const candidate = payQrCandidate(paymentId);
541
+ return isVenezuelanPayQr(candidate) ? candidate : null;
542
+ }
493
543
  };
494
544
 
495
545
  // src/country/countries.ts
@@ -510,6 +560,10 @@ var COUNTRY_OPTIONS = [
510
560
  EUR_COUNTRY_OPTION,
511
561
  USD_COUNTRY_OPTION
512
562
  ];
563
+ function getCountryOption(currency) {
564
+ if (!currency) return void 0;
565
+ return COUNTRY_OPTIONS.find((c) => c.currency === currency);
566
+ }
513
567
 
514
568
  // src/qr-parsers/types.ts
515
569
  import { err, ok } from "neverthrow";
@@ -1035,16 +1089,16 @@ function parseNGN(qrData, sellPrice) {
1035
1089
  }
1036
1090
 
1037
1091
  // src/qr-parsers/parsers/pen.ts
1038
- var PEN_TAGS = { AMOUNT: "54", CURRENCY: "53", COUNTRY: "58" };
1092
+ var PEN_TAGS = { AMOUNT: "54" };
1039
1093
  function parsePeru(qrData, sellPrice) {
1040
1094
  if (!qrData || typeof qrData !== "string" || qrData.trim().length === 0) {
1041
1095
  return failure("INVALID_QR", "QR data is empty or invalid");
1042
1096
  }
1043
1097
  const trimmed = qrData.trim();
1044
- const tags = extractTags(trimmed, [PEN_TAGS.AMOUNT, PEN_TAGS.CURRENCY, PEN_TAGS.COUNTRY]);
1045
- if (tags[PEN_TAGS.COUNTRY] !== "PE" || tags[PEN_TAGS.CURRENCY] !== "604") {
1098
+ if (!validatePeruvianQr(trimmed)) {
1046
1099
  return failure("INVALID_QR", "Not a valid Peru Yape/Plin QR");
1047
1100
  }
1101
+ const tags = extractTags(trimmed, [PEN_TAGS.AMOUNT]);
1048
1102
  const result = { paymentAddress: trimmed };
1049
1103
  const amountStr = tags[PEN_TAGS.AMOUNT];
1050
1104
  if (amountStr) {
@@ -1086,21 +1140,14 @@ function parseQRPh(qrData, sellPrice) {
1086
1140
 
1087
1141
  // src/qr-parsers/parsers/ven.ts
1088
1142
  function isPagoMovilQr(qrData) {
1089
- if (!qrData || typeof qrData !== "string" || qrData.trim().length === 0) {
1090
- return false;
1091
- }
1092
- const trimmed = qrData.trim();
1093
- const qIdx = trimmed.indexOf("?");
1094
- if (qIdx === -1) return false;
1095
- const payload = trimmed.substring(0, qIdx);
1096
- return !!payload && /^[A-Za-z0-9+/=]+$/.test(payload);
1143
+ return validateVenezuelanQr(qrData);
1097
1144
  }
1098
1145
  function parsePagoMovil(qrData, _sellPrice) {
1099
1146
  if (!qrData || typeof qrData !== "string" || qrData.trim().length === 0) {
1100
1147
  return failure("INVALID_QR", "QR data is empty or invalid");
1101
1148
  }
1102
1149
  const trimmed = qrData.trim();
1103
- if (!isPagoMovilQr(trimmed)) {
1150
+ if (!validateVenezuelanQr(trimmed)) {
1104
1151
  return failure("INVALID_QR", "Not a valid Venezuelan QR code");
1105
1152
  }
1106
1153
  return success({ paymentAddress: trimmed });
@@ -1112,34 +1159,52 @@ async function parseQR(params) {
1112
1159
  if (!qrData || typeof qrData !== "string" || qrData.trim().length === 0) {
1113
1160
  return failure("INVALID_QR", "QR data is empty or invalid");
1114
1161
  }
1162
+ let result;
1115
1163
  switch (currency) {
1116
1164
  case CURRENCY.INR:
1117
- return parseUPI(qrData, sellPrice);
1165
+ result = parseUPI(qrData, sellPrice);
1166
+ break;
1118
1167
  case CURRENCY.IDR:
1119
- return parseQRIS(qrData, sellPrice);
1168
+ result = parseQRIS(qrData, sellPrice);
1169
+ break;
1120
1170
  case CURRENCY.BRL:
1121
- return parsePIX(qrData, sellPrice, { proxyUrl, orderId });
1171
+ result = await parsePIX(qrData, sellPrice, { proxyUrl, orderId });
1172
+ break;
1122
1173
  case CURRENCY.ARS:
1123
- return parseMercadoPago(qrData, sellPrice);
1174
+ result = parseMercadoPago(qrData, sellPrice);
1175
+ break;
1124
1176
  case CURRENCY.VEN:
1125
- return parsePagoMovil(qrData, sellPrice);
1177
+ result = parsePagoMovil(qrData, sellPrice);
1178
+ break;
1126
1179
  case CURRENCY.BOB:
1127
- return parseBolivia(qrData, sellPrice);
1180
+ result = parseBolivia(qrData, sellPrice);
1181
+ break;
1128
1182
  case CURRENCY.NGN:
1129
- return parseNGN(qrData, sellPrice);
1183
+ result = parseNGN(qrData, sellPrice);
1184
+ break;
1130
1185
  case CURRENCY.COP:
1131
- return parseCOP(qrData, sellPrice);
1186
+ result = parseCOP(qrData, sellPrice);
1187
+ break;
1132
1188
  case CURRENCY.CUP:
1133
- return parseTransfermovil(qrData, sellPrice);
1189
+ result = parseTransfermovil(qrData, sellPrice);
1190
+ break;
1134
1191
  case CURRENCY.ECU:
1135
- return parseDeUna(qrData, sellPrice);
1192
+ result = parseDeUna(qrData, sellPrice);
1193
+ break;
1136
1194
  case CURRENCY.PEN:
1137
- return parsePeru(qrData, sellPrice);
1195
+ result = parsePeru(qrData, sellPrice);
1196
+ break;
1138
1197
  case CURRENCY.PHP:
1139
- return parseQRPh(qrData, sellPrice);
1198
+ result = parseQRPh(qrData, sellPrice);
1199
+ break;
1140
1200
  default:
1141
1201
  return failure("INVALID_CURRENCY", `Currency "${currency}" is not supported`);
1142
1202
  }
1203
+ const validateQr = getCountryOption(currency)?.validateQr;
1204
+ if (result.isOk() && validateQr && !validateQr(qrData.trim())) {
1205
+ return failure("INVALID_QR", "QR does not match catalog validateQr");
1206
+ }
1207
+ return result;
1143
1208
  }
1144
1209
  export {
1145
1210
  QRParserError,