@riocrypto/common-server 1.0.2927 → 1.0.2929

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.
@@ -8,72 +8,125 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
11
18
  Object.defineProperty(exports, "__esModule", { value: true });
12
19
  exports.getOrderFromReference = void 0;
13
20
  const order_1 = require("../models/order");
14
21
  const common_1 = require("@riocrypto/common");
22
+ // A reference is 8 hex characters of the order id (two from the random bytes
23
+ // plus the last six); older orders were referenced by the last six alone. The
24
+ // payer types it into a concepto that can carry anything else around it, so
25
+ // matching has always been "does the text contain the token", and that is what
26
+ // is preserved here.
27
+ const NEW_FORMAT_LENGTH = 8;
28
+ const OLD_FORMAT_LENGTH = 6;
29
+ // Statuses a payment can no longer be applied to. Anything failed is included
30
+ // by name so that a status added later is excluded without a change here.
31
+ const excludedStatuses = Object.values(common_1.OrderStatus).filter((status) => {
32
+ const statusLower = status.toLowerCase();
33
+ return (statusLower === "complete" ||
34
+ statusLower === "cancelled" ||
35
+ statusLower === "expired" ||
36
+ statusLower === "refundcomplete" ||
37
+ statusLower.includes("failed"));
38
+ });
39
+ // Every window of `length` hex characters in the text, lowercase and in the
40
+ // order they appear. A reference has to be a run of hex, so anything else in
41
+ // the concepto is a boundary rather than a candidate.
42
+ const hexWindows = (text, length) => {
43
+ var _a;
44
+ const windows = new Set();
45
+ for (const run of (_a = text.toLowerCase().match(/[0-9a-f]+/g)) !== null && _a !== void 0 ? _a : []) {
46
+ for (let i = 0; i + length <= run.length; i += 1) {
47
+ windows.add(run.slice(i, i + length));
48
+ }
49
+ }
50
+ return Array.from(windows);
51
+ };
15
52
  const getOrderFromReference = (reference, mongoose) => __awaiter(void 0, void 0, void 0, function* () {
16
- // Ensure reference is defined
53
+ var _a, e_1, _b, _c;
17
54
  if (!reference) {
18
55
  throw new Error("Invalid reference. Reference cannot be empty.");
19
56
  }
20
- // Convert reference to lowercase to ensure case-insensitive comparison
21
- const referenceText = reference.toLowerCase();
22
57
  const Order = (0, order_1.buildOrder)(mongoose);
23
- // Define statuses to exclude (completed, cancelled, and any failed status)
24
- const excludedStatuses = Object.values(common_1.OrderStatus).filter((status) => {
25
- const statusLower = status.toLowerCase();
26
- return (statusLower === "complete" ||
27
- statusLower === "cancelled" ||
28
- statusLower === "expired" ||
29
- statusLower === "refundcomplete" ||
30
- statusLower.includes("failed"));
31
- });
32
- // Process in batches to avoid memory issues
33
- const batchSize = 1000;
34
- let skip = 0;
35
- let foundOrder = null;
36
- let foundOrderOldFormat = null;
37
- while (!foundOrder) {
38
- // Query only active orders (exclude completed/cancelled/failed) in batches
39
- const orders = yield Order.find({
40
- status: { $nin: excludedStatuses },
41
- })
42
- .select("_id") // Only select the ID field to minimize memory usage
43
- .skip(skip)
44
- .limit(batchSize)
45
- .lean(); // Use lean() for better performance
46
- // If no more orders, break the loop
47
- if (orders.length === 0) {
48
- break;
58
+ const activeStatus = { $nin: excludedStatuses };
59
+ // New format, exact and indexed. The stored reference is what the customer was
60
+ // shown, upper case, so the windows are compared the same way.
61
+ const newFormatCandidates = hexWindows(reference, NEW_FORMAT_LENGTH).map((window) => window.toUpperCase());
62
+ const oldFormatWindows = hexWindows(reference, OLD_FORMAT_LENGTH);
63
+ // A reference is a run of hex, so text without one cannot contain any order's
64
+ // token in either format. Static references (SPR…) and free text end here
65
+ // instead of walking the active orders to confirm what is already known.
66
+ if (!newFormatCandidates.length && !oldFormatWindows.length) {
67
+ throw new Error("Order not found");
68
+ }
69
+ if (newFormatCandidates.length) {
70
+ const order = yield Order.findOne({
71
+ "paymentInstructions.reference": { $in: newFormatCandidates },
72
+ status: activeStatus,
73
+ }).sort({ createdAt: 1 });
74
+ if (order) {
75
+ return order;
49
76
  }
50
- // Check each order ID against the reference
51
- // Priority 1: New format - 2 chars from random portion (8-9) + last 6 chars
52
- // Priority 2: Old format - just last 6 chars (backwards compatibility)
53
- for (const order of orders) {
54
- const orderId = order._id.toString().toLowerCase();
55
- const randomPrefix = orderId.slice(8, 10);
56
- const last6 = orderId.slice(-6);
57
- const reference8Chars = randomPrefix + last6;
58
- // Check new 8-char format first (takes priority)
59
- if (referenceText.includes(reference8Chars)) {
60
- foundOrder = yield Order.findById(order._id);
61
- break;
77
+ }
78
+ // The indexed lookup missed, so the token is compared against each active buy
79
+ // order's id the way it always was. This still catches an order whose stored
80
+ // reference is not its own token, such as one paid through a bulk, and one
81
+ // that was never given a stored reference at all. It runs only on a miss, so a
82
+ // burst of valid payments never reaches it, and it is a cursor with an _id
83
+ // projection rather than skip pagination over the same set. Placed before the
84
+ // old-format lookup so a full match here still beats a six-character one
85
+ // anywhere, as it always has.
86
+ const newFormatWindowSet = new Set(newFormatCandidates.map((candidate) => candidate.toLowerCase()));
87
+ const oldFormatWindowSet = new Set(oldFormatWindows);
88
+ let scannedOldFormatMatchId = null;
89
+ const activeBuyOrders = Order.find({
90
+ status: activeStatus,
91
+ side: common_1.Side.Buy,
92
+ })
93
+ .select("_id")
94
+ .lean()
95
+ .cursor();
96
+ try {
97
+ for (var _d = true, activeBuyOrders_1 = __asyncValues(activeBuyOrders), activeBuyOrders_1_1; activeBuyOrders_1_1 = yield activeBuyOrders_1.next(), _a = activeBuyOrders_1_1.done, !_a;) {
98
+ _c = activeBuyOrders_1_1.value;
99
+ _d = false;
100
+ try {
101
+ const candidate = _c;
102
+ const orderId = candidate._id.toString().toLowerCase();
103
+ const reference8Chars = orderId.slice(8, 10) + orderId.slice(-6);
104
+ if (newFormatWindowSet.has(reference8Chars)) {
105
+ return Order.findById(candidate._id);
106
+ }
107
+ if (!scannedOldFormatMatchId &&
108
+ oldFormatWindowSet.has(orderId.slice(-6))) {
109
+ scannedOldFormatMatchId = candidate._id;
110
+ }
62
111
  }
63
- // Track old 6-char format match as fallback (only if no 8-char match yet)
64
- if (!foundOrderOldFormat && referenceText.includes(last6)) {
65
- foundOrderOldFormat = yield Order.findById(order._id);
112
+ finally {
113
+ _d = true;
66
114
  }
67
115
  }
68
- skip += batchSize;
69
116
  }
70
- // Use 8-char match if found, otherwise fall back to 6-char match
71
- if (!foundOrder && foundOrderOldFormat) {
72
- foundOrder = foundOrderOldFormat;
117
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
118
+ finally {
119
+ try {
120
+ if (!_d && !_a && (_b = activeBuyOrders_1.return)) yield _b.call(activeBuyOrders_1);
121
+ }
122
+ finally { if (e_1) throw e_1.error; }
73
123
  }
74
- if (!foundOrder) {
75
- throw new Error("Order not found");
124
+ // Old format: the payer typed only the last six characters, and the scan above
125
+ // found an order ending in them. Used only once no order anywhere matched the
126
+ // full token.
127
+ if (scannedOldFormatMatchId) {
128
+ return Order.findById(scannedOldFormatMatchId);
76
129
  }
77
- return foundOrder;
130
+ throw new Error("Order not found");
78
131
  });
79
132
  exports.getOrderFromReference = getOrderFromReference;
@@ -412,6 +412,11 @@ const buildOrder = (mongoose) => {
412
412
  orderSchema.index({ status: 1, createdAt: 1 });
413
413
  orderSchema.index({ clientReferenceId: 1 }, { sparse: true });
414
414
  orderSchema.index({ "paymentsReceived.blockchainTxId": 1 }, { sparse: true });
415
+ // Every inbound bank deposit is matched to its order by the reference the
416
+ // customer typed. Before this, that meant loading every active order and
417
+ // comparing ids in code on each webhook, which under a burst of payments held
418
+ // the fintoc pod long enough for its liveness probe to kill it mid-deposit.
419
+ orderSchema.index({ "paymentInstructions.reference": 1 }, { sparse: true });
415
420
  // At most one order per real quoteId. Imported orders use quoteId "" and are
416
421
  // excluded by the partial filter (which also serves the createOrder lookup).
417
422
  orderSchema.index({ quoteId: 1 }, { unique: true, partialFilterExpression: { quoteId: { $gt: "" } } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2927",
3
+ "version": "1.0.2929",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",