@riocrypto/common-server 1.0.2877 → 1.0.2879

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.
Files changed (39) hide show
  1. package/build/clients/cluster-client.d.ts +36 -0
  2. package/build/clients/cluster-client.js +31 -1
  3. package/build/helpers/get-custom-rio-bank-account.d.ts +10 -0
  4. package/build/helpers/get-custom-rio-bank-account.js +37 -0
  5. package/build/helpers/get-processor.d.ts +7 -4
  6. package/build/helpers/get-processor.js +72 -55
  7. package/build/helpers/get-tax-rate.js +1 -0
  8. package/build/helpers/payin-destinations.d.ts +37 -0
  9. package/build/helpers/payin-destinations.js +286 -0
  10. package/build/helpers/payin-verification.d.ts +30 -0
  11. package/build/helpers/payin-verification.js +162 -0
  12. package/build/helpers/processor-enablement.d.ts +19 -0
  13. package/build/helpers/processor-enablement.js +87 -0
  14. package/build/helpers/record-observed-processor-readiness.d.ts +10 -0
  15. package/build/helpers/record-observed-processor-readiness.js +65 -0
  16. package/build/helpers/resolve-processor.d.ts +18 -0
  17. package/build/helpers/resolve-processor.js +122 -0
  18. package/build/index.d.ts +10 -0
  19. package/build/index.js +10 -0
  20. package/build/middlewares/get-order.js +1 -0
  21. package/build/models/bank-account-verification.d.ts +2 -2
  22. package/build/models/bulk-bank-payment.js +3 -0
  23. package/build/models/fintoc-deposit-CLABE.d.ts +28 -0
  24. package/build/models/fintoc-deposit-CLABE.js +56 -0
  25. package/build/models/fintoc-mxn-withdrawal.d.ts +51 -0
  26. package/build/models/fintoc-mxn-withdrawal.js +88 -0
  27. package/build/models/fireblocks-vault.d.ts +8 -0
  28. package/build/models/fireblocks-vault.js +8 -0
  29. package/build/models/internal-balances.d.ts +3 -0
  30. package/build/models/internal-balances.js +3 -0
  31. package/build/models/payment-address-verification.d.ts +49 -0
  32. package/build/models/payment-address-verification.js +77 -0
  33. package/build/models/processor-readiness.d.ts +44 -0
  34. package/build/models/processor-readiness.js +90 -0
  35. package/build/models/rio-bank-account.d.ts +3 -1
  36. package/build/models/rio-bank-account.js +3 -0
  37. package/build/models/rio-settings.d.ts +3 -11
  38. package/build/models/rio-settings.js +1 -2
  39. package/package.json +2 -2
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getPayinDestinations = exports.getPayinBankDetails = exports.PER_USER_PAYIN_PROCESSORS = void 0;
13
+ const common_1 = require("@riocrypto/common");
14
+ const rio_bank_account_1 = require("../models/rio-bank-account");
15
+ const STP_deposit_CLABE_1 = require("../models/STP-deposit-CLABE");
16
+ const fintoc_deposit_CLABE_1 = require("../models/fintoc-deposit-CLABE");
17
+ const alfin_virtual_cci_1 = require("../models/alfin-virtual-cci");
18
+ const processor_readiness_1 = require("../models/processor-readiness");
19
+ const cluster_client_1 = require("../clients/cluster-client");
20
+ // Rails that give each user their own destination, which therefore has to exist
21
+ // before we can tell a customer where to pay. Every other rail is a shared house
22
+ // account that is always available.
23
+ exports.PER_USER_PAYIN_PROCESSORS = [
24
+ common_1.Processor.SPEI_STP,
25
+ common_1.Processor.SPEI_FINTOC,
26
+ common_1.Processor.Alfin,
27
+ ];
28
+ const MEXICO_COMPANY_NAME = "RAMPA BLOCKCHAIN SAPI DE CV";
29
+ const MEXICO_COMPANY_ADDRESS = "Ave. Lázaro Cardenas 2400, Residencial San Agustin 1 Sector, San Pedro Garza Garcia, CP 66260, Mexico";
30
+ const MEXICO_RFC = "RBL2206107N8";
31
+ const PERU_COMPANY_NAME = "Rampa Machu S.A.C.";
32
+ const PERU_COMPANY_ADDRESS = "Calle General de la Fuente, Edificio 393, dep. 102, Lima, Perú, 17076";
33
+ const PERU_RUC = "20610425713";
34
+ // Rio's Alfin Cuenta Vista per currency. A CCIV is reachable only through the
35
+ // interbank clearing house, so a customer who banks at Alfin cannot pay one from
36
+ // inside Alfin and needs the underlying account number. Note that a payment sent
37
+ // here carries no CCIV, so Alfin raises no payin notification for it: it is only
38
+ // picked up by the treasury statement import and has to be matched by hand.
39
+ const ALFIN_ACCOUNT_NUMBER_BY_FIAT = {
40
+ [common_1.Fiat.PEN]: "01818233900001",
41
+ [common_1.Fiat.USD]: "01818233900002",
42
+ };
43
+ // Shared house accounts. Kept here rather than in the order-payment path so the
44
+ // customer-facing whitelisting list and the payment instructions on an order can
45
+ // never drift apart.
46
+ const getStaticPayinBankDetails = (processor, fiat) => {
47
+ if (processor === common_1.Processor.SPEI_NVIO) {
48
+ return {
49
+ bankName: "NVIO",
50
+ companyName: MEXICO_COMPANY_NAME,
51
+ companyAddress: MEXICO_COMPANY_ADDRESS,
52
+ CLABE: "710969000021237966",
53
+ RFC: MEXICO_RFC,
54
+ };
55
+ }
56
+ if (processor === common_1.Processor.SPEI) {
57
+ return {
58
+ bankName: "Fondeadora",
59
+ companyName: MEXICO_COMPANY_NAME,
60
+ companyAddress: MEXICO_COMPANY_ADDRESS,
61
+ CLABE: "699180600005533135",
62
+ RFC: MEXICO_RFC,
63
+ };
64
+ }
65
+ if (processor === common_1.Processor.SPID) {
66
+ return {
67
+ bankName: "Banco Bancrea, S.A.",
68
+ companyName: MEXICO_COMPANY_NAME,
69
+ companyAddress: "Ave. Lázaro Cardenas 2400 INT B51. Residencial San Agustin 1er SEC, San Pedro Garza Garcia, Nuevo Leon, Mexico 66260",
70
+ CLABE: "152580220000044256",
71
+ RFC: MEXICO_RFC,
72
+ };
73
+ }
74
+ if (processor === common_1.Processor.Interbank) {
75
+ if (fiat === common_1.Fiat.PEN) {
76
+ return {
77
+ bankName: "Interbank",
78
+ companyName: PERU_COMPANY_NAME,
79
+ companyAddress: PERU_COMPANY_ADDRESS,
80
+ accountNumber: "200-3004741070",
81
+ CCI: "003-200-003004741070-36",
82
+ RUC: PERU_RUC,
83
+ };
84
+ }
85
+ if (fiat === common_1.Fiat.USD) {
86
+ return {
87
+ bankName: "Interbank",
88
+ companyName: PERU_COMPANY_NAME,
89
+ companyAddress: PERU_COMPANY_ADDRESS,
90
+ accountNumber: "200-3004741088",
91
+ CCI: "003-200-003004741088-38",
92
+ RUC: PERU_RUC,
93
+ };
94
+ }
95
+ return {};
96
+ }
97
+ if (processor === common_1.Processor.BinanceRFQ) {
98
+ return {
99
+ bankName: "Binance RFQ",
100
+ companyName: MEXICO_COMPANY_NAME,
101
+ companyAddress: "Ave. Lázaro Cardenas 2400 INT B51. Residencial San Agustin 1er SEC, San Pedro Garza Garcia, Nuevo Leon, Mexico 66260",
102
+ accountNumber: "Rio Binance account",
103
+ };
104
+ }
105
+ if (processor === common_1.Processor.Coltefinanciera) {
106
+ return {
107
+ bankName: "Coltefinanciera",
108
+ companyName: "RAMPA COLOMBIA S.A.S.",
109
+ companyAddress: "Cl. 82 # 18-36 Oficina 301, Bogotá D.C., Colombia",
110
+ accountNumber: "4060002029",
111
+ NIT: "901700811-9",
112
+ };
113
+ }
114
+ return {};
115
+ };
116
+ // Looks up (and by default creates) the user's own destination on a per-user
117
+ // rail. With `provision` false it reports what exists without calling out to the
118
+ // provider, which is what the whitelisting list needs so that merely viewing the
119
+ // page does not allocate account numbers at Fintoc or Alfin.
120
+ const getPerUserPayinBankDetails = ({ mongoose, processor, fiat, userId, provision, }) => __awaiter(void 0, void 0, void 0, function* () {
121
+ if (processor === common_1.Processor.SPEI_STP) {
122
+ const STPDepositCLABE = (0, STP_deposit_CLABE_1.buildSTPDepositCLABE)(mongoose);
123
+ const existing = yield STPDepositCLABE.findOne({ userId });
124
+ let CLABE = existing === null || existing === void 0 ? void 0 : existing.CLABE;
125
+ if (!CLABE) {
126
+ if (!provision) {
127
+ return undefined;
128
+ }
129
+ const clusterClient = yield (0, cluster_client_1.buildClusterClient)();
130
+ CLABE = (yield clusterClient.generateSTPDepositCLABE(userId)).CLABE;
131
+ }
132
+ return {
133
+ bankName: "Sistema de Transferencias y Pagos, S.A. de C.V.",
134
+ companyName: MEXICO_COMPANY_NAME,
135
+ companyAddress: MEXICO_COMPANY_ADDRESS,
136
+ CLABE,
137
+ RFC: MEXICO_RFC,
138
+ };
139
+ }
140
+ if (processor === common_1.Processor.SPEI_FINTOC) {
141
+ const FintocDepositCLABE = (0, fintoc_deposit_CLABE_1.buildFintocDepositCLABE)(mongoose);
142
+ // A user has exactly one Fintoc CLABE and it never changes. The orderId
143
+ // filter only excludes legacy per-order rows that predate that rule.
144
+ const existing = yield FintocDepositCLABE.findOne({
145
+ userId,
146
+ $or: [{ orderId: { $exists: false } }, { orderId: null }],
147
+ disabled: { $ne: true },
148
+ });
149
+ let CLABE = existing === null || existing === void 0 ? void 0 : existing.CLABE;
150
+ if (!CLABE) {
151
+ if (!provision) {
152
+ return undefined;
153
+ }
154
+ const clusterClient = yield (0, cluster_client_1.buildClusterClient)();
155
+ CLABE = (yield clusterClient.generateFintocDepositCLABE(userId)).CLABE;
156
+ }
157
+ return {
158
+ bankName: "Fintoc",
159
+ companyName: MEXICO_COMPANY_NAME,
160
+ companyAddress: MEXICO_COMPANY_ADDRESS,
161
+ CLABE,
162
+ RFC: MEXICO_RFC,
163
+ };
164
+ }
165
+ if (processor === common_1.Processor.Alfin) {
166
+ const AlfinVirtualCci = (0, alfin_virtual_cci_1.buildAlfinVirtualCci)(mongoose);
167
+ const existing = yield AlfinVirtualCci.findOne({ userId, fiat });
168
+ let CCIV = existing === null || existing === void 0 ? void 0 : existing.CCIV;
169
+ if (!CCIV) {
170
+ if (!provision) {
171
+ return undefined;
172
+ }
173
+ const clusterClient = yield (0, cluster_client_1.buildClusterClient)();
174
+ CCIV = (yield clusterClient.generateAlfinVirtualCci(userId, fiat)).CCIV;
175
+ }
176
+ return {
177
+ bankName: "Alfin Banco",
178
+ companyName: PERU_COMPANY_NAME,
179
+ companyAddress: PERU_COMPANY_ADDRESS,
180
+ accountNumber: ALFIN_ACCOUNT_NUMBER_BY_FIAT[fiat],
181
+ CCI: CCIV,
182
+ RUC: PERU_RUC,
183
+ };
184
+ }
185
+ return undefined;
186
+ });
187
+ // The account a customer must pay into to fund a buy order on a given rail.
188
+ // A custom Rio bank account replaces the rail's own destination outright, which
189
+ // is why routing has to know that account's processor - see resolveProcessor.
190
+ const getPayinBankDetails = ({ mongoose, processor, fiat, country, user, }) => __awaiter(void 0, void 0, void 0, function* () {
191
+ var _a, _b, _c;
192
+ const rioBankAccountId = (_c = (_b = (_a = user.rioBankAccount) === null || _a === void 0 ? void 0 : _a[country]) === null || _b === void 0 ? void 0 : _b[fiat]) === null || _c === void 0 ? void 0 : _c.id;
193
+ if (rioBankAccountId) {
194
+ const RioBankAccount = (0, rio_bank_account_1.buildRioBankAccount)(mongoose);
195
+ const rioBankAccount = yield RioBankAccount.findById(rioBankAccountId);
196
+ if (!rioBankAccount) {
197
+ throw new common_1.NotFoundError("Rio bank account");
198
+ }
199
+ return {
200
+ bankName: rioBankAccount.bankName,
201
+ bankAddress: rioBankAccount.bankAddress,
202
+ companyName: rioBankAccount.companyName,
203
+ companyAddress: rioBankAccount.companyAddress,
204
+ accountNumber: rioBankAccount.accountNumber,
205
+ CLABE: rioBankAccount.CLABE,
206
+ RUC: rioBankAccount.RUC,
207
+ RFC: rioBankAccount.RFC,
208
+ CCI: rioBankAccount.CCI,
209
+ };
210
+ }
211
+ if (exports.PER_USER_PAYIN_PROCESSORS.includes(processor)) {
212
+ const details = yield getPerUserPayinBankDetails({
213
+ mongoose,
214
+ processor,
215
+ fiat,
216
+ userId: user.id,
217
+ provision: true,
218
+ });
219
+ return details || {};
220
+ }
221
+ return getStaticPayinBankDetails(processor, fiat);
222
+ });
223
+ exports.getPayinBankDetails = getPayinBankDetails;
224
+ // Every account a user may be asked to pay into for one corridor, for the page
225
+ // that tells them what to whitelist at their bank. Read-only by default so that
226
+ // viewing the list never allocates anything at a provider; pass `provision` to
227
+ // create the destinations that do not exist yet.
228
+ const getPayinDestinations = ({ mongoose, user, country, fiat, processors, provision = false, }) => __awaiter(void 0, void 0, void 0, function* () {
229
+ var _d, _e;
230
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
231
+ const readiness = yield ProcessorReadiness.find({
232
+ userId: user.id,
233
+ country,
234
+ fiat,
235
+ })
236
+ .select({ processor: 1, status: 1, enablement: 1 })
237
+ .lean();
238
+ const readinessByProcessor = new Map(readiness.map((record) => [record.processor, record]));
239
+ const isEnabled = (processor) => {
240
+ var _a, _b, _c;
241
+ return ((_c = (_b = (_a = readinessByProcessor.get(processor)) === null || _a === void 0 ? void 0 : _a.enablement) === null || _b === void 0 ? void 0 : _b[common_1.Side.Buy]) === null || _c === void 0 ? void 0 : _c.isEnabled) !==
242
+ false;
243
+ };
244
+ const destinations = [];
245
+ for (const processor of processors) {
246
+ let bankDetails;
247
+ if (exports.PER_USER_PAYIN_PROCESSORS.includes(processor)) {
248
+ bankDetails = yield getPerUserPayinBankDetails({
249
+ mongoose,
250
+ processor,
251
+ fiat,
252
+ userId: user.id,
253
+ provision,
254
+ });
255
+ }
256
+ else {
257
+ bankDetails = getStaticPayinBankDetails(processor, fiat);
258
+ }
259
+ // A rail with no destination for this fiat (Interbank outside PEN/USD, say)
260
+ // is not something the customer can whitelist, so it is left off the list
261
+ // rather than shown as an empty row.
262
+ const isProvisioned = Boolean(bankDetails && (bankDetails.CLABE || bankDetails.CCI || bankDetails.accountNumber));
263
+ if (!bankDetails) {
264
+ destinations.push({
265
+ processor,
266
+ bankDetails: {},
267
+ isProvisioned: false,
268
+ isEnabled: isEnabled(processor),
269
+ readinessStatus: (_d = readinessByProcessor.get(processor)) === null || _d === void 0 ? void 0 : _d.status,
270
+ });
271
+ continue;
272
+ }
273
+ if (!isProvisioned) {
274
+ continue;
275
+ }
276
+ destinations.push({
277
+ processor,
278
+ bankDetails,
279
+ isProvisioned: true,
280
+ isEnabled: isEnabled(processor),
281
+ readinessStatus: (_e = readinessByProcessor.get(processor)) === null || _e === void 0 ? void 0 : _e.status,
282
+ });
283
+ }
284
+ return destinations;
285
+ });
286
+ exports.getPayinDestinations = getPayinDestinations;
@@ -0,0 +1,30 @@
1
+ import { Country, Fiat, Processor, ProcessorReadinessStatus } from "@riocrypto/common";
2
+ import { Mongoose } from "mongoose";
3
+ export declare const extractTestReference: (reference: string | undefined | null) => string | undefined;
4
+ export declare const startPayinVerification: ({ mongoose, userId, country, fiat, processor, CLABE, }: {
5
+ mongoose: Mongoose;
6
+ userId: string;
7
+ country: Country;
8
+ fiat: Fiat;
9
+ processor: Processor;
10
+ CLABE?: string | undefined;
11
+ }) => Promise<{
12
+ reference: string;
13
+ amount: number;
14
+ status: ProcessorReadinessStatus;
15
+ }>;
16
+ export interface PayinVerificationMatch {
17
+ userId: string;
18
+ country: Country;
19
+ fiat: Fiat;
20
+ processor: Processor;
21
+ }
22
+ export declare const matchPendingPayinVerification: ({ mongoose, reference, amount, fiat, processor, originCLABE, originAccountHolderName, }: {
23
+ mongoose: Mongoose;
24
+ reference: string | undefined | null;
25
+ amount: number;
26
+ fiat: Fiat;
27
+ processor: Processor;
28
+ originCLABE?: string | undefined;
29
+ originAccountHolderName?: string | undefined;
30
+ }) => Promise<PayinVerificationMatch | undefined>;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.matchPendingPayinVerification = exports.startPayinVerification = exports.extractTestReference = void 0;
16
+ const crypto_1 = __importDefault(require("crypto"));
17
+ const common_1 = require("@riocrypto/common");
18
+ const processor_readiness_1 = require("../models/processor-readiness");
19
+ const logger_1 = __importDefault(require("../services/logger"));
20
+ // Distinctive prefix so a test reference cannot be confused with an order or
21
+ // bulk payment reference (which use "RIO-" and "SPR" prefixes). Matching keys off
22
+ // this pattern, which is what lets the inbound handlers recognise a test deposit
23
+ // before they attempt to match it to an order.
24
+ const TEST_REFERENCE_PREFIX = "RIOV";
25
+ const TEST_REFERENCE_BODY_LENGTH = 10;
26
+ const TEST_REFERENCE_PATTERN = new RegExp(`${TEST_REFERENCE_PREFIX}[0-9A-HJ-NP-Z]{${TEST_REFERENCE_BODY_LENGTH}}`);
27
+ // Crockford-style alphabet: no I, L, O or U, so a customer retyping the reference
28
+ // off a screen cannot turn it into a different valid token.
29
+ const TOKEN_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
30
+ const buildTestReference = () => {
31
+ const bytes = crypto_1.default.randomBytes(TEST_REFERENCE_BODY_LENGTH);
32
+ let body = "";
33
+ for (let i = 0; i < TEST_REFERENCE_BODY_LENGTH; i += 1) {
34
+ body += TOKEN_ALPHABET[bytes[i] % TOKEN_ALPHABET.length];
35
+ }
36
+ return `${TEST_REFERENCE_PREFIX}${body}`;
37
+ };
38
+ // Centavos rather than whole units: it widens the space of possible amounts and
39
+ // makes an accidental collision with an unrelated payment of the same size far
40
+ // less likely, which matters on the shared house rails where the reference is the
41
+ // only thing tying a deposit to a user.
42
+ const buildTestAmount = () => (0, common_1.roundToDecimals)(1 + crypto_1.default.randomInt(0, 899) / 100, 2);
43
+ const extractTestReference = (reference) => {
44
+ var _a;
45
+ if (!reference) {
46
+ return undefined;
47
+ }
48
+ return (_a = TEST_REFERENCE_PATTERN.exec(reference.toUpperCase())) === null || _a === void 0 ? void 0 : _a[0];
49
+ };
50
+ exports.extractTestReference = extractTestReference;
51
+ // Opens a penny test: the customer sends us this exact amount with this exact
52
+ // reference, and the inbound handler for the rail marks them verified when it
53
+ // arrives. Deliberately does not touch an already-verified record - there is
54
+ // nothing left to prove.
55
+ //
56
+ // Proving a rail works does not switch it on, and never switching it on does not
57
+ // stop it being used: enablement is the customer's own decision and lives on the
58
+ // same record under enablement. A test is how they find out a rail works before an
59
+ // order depends on it, not how they get access to it.
60
+ //
61
+ // The token does not expire. Whitelisting an account at a bank can take days of
62
+ // back and forth at the customer's end, and expiring the token in the middle of
63
+ // that would reject a transfer they were told to send - the worst possible
64
+ // outcome, since the deposit has already left their account.
65
+ const startPayinVerification = ({ mongoose, userId, country, fiat, processor, CLABE, }) => __awaiter(void 0, void 0, void 0, function* () {
66
+ if (!(0, common_1.isPayinVerifiable)(processor)) {
67
+ throw new common_1.GenericInputError("This payment rail cannot be verified with a test deposit");
68
+ }
69
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
70
+ const existing = yield ProcessorReadiness.findOne({
71
+ userId,
72
+ country,
73
+ fiat,
74
+ processor,
75
+ });
76
+ if ((existing === null || existing === void 0 ? void 0 : existing.status) === common_1.ProcessorReadinessStatus.Verified) {
77
+ throw new common_1.GenericInputError("This payment rail is already verified for this account");
78
+ }
79
+ // Reuse the outstanding token rather than issuing a new one, so a customer who
80
+ // reloads the page and has already sent the deposit is not left with a
81
+ // reference we no longer recognise.
82
+ if ((existing === null || existing === void 0 ? void 0 : existing.status) === common_1.ProcessorReadinessStatus.AwaitingTestDeposit &&
83
+ existing.testReference &&
84
+ existing.testAmount) {
85
+ return {
86
+ reference: existing.testReference,
87
+ amount: existing.testAmount,
88
+ status: existing.status,
89
+ };
90
+ }
91
+ const reference = buildTestReference();
92
+ const amount = buildTestAmount();
93
+ yield ProcessorReadiness.updateOne({ userId, country, fiat, processor }, {
94
+ $set: Object.assign({ status: common_1.ProcessorReadinessStatus.AwaitingTestDeposit, testReference: reference, testAmount: amount }, (CLABE ? { CLABE } : {})),
95
+ $unset: {
96
+ observedOriginCLABE: "",
97
+ observedOriginAccountHolderName: "",
98
+ },
99
+ $setOnInsert: {
100
+ createdAt: new Date(),
101
+ userId,
102
+ country,
103
+ fiat,
104
+ processor,
105
+ },
106
+ }, { upsert: true });
107
+ return {
108
+ reference,
109
+ amount,
110
+ status: common_1.ProcessorReadinessStatus.AwaitingTestDeposit,
111
+ };
112
+ });
113
+ exports.startPayinVerification = startPayinVerification;
114
+ // Called from each rail's inbound deposit handler BEFORE it tries to match the
115
+ // deposit to an order. A test token is random, so it will not collide with a real
116
+ // order reference - but order matching does substring comparisons, so running it
117
+ // first could still claim a test deposit by accident.
118
+ //
119
+ // Requires both the token and the exact amount. The token alone is unguessable,
120
+ // so the amount is defence in depth rather than the primary control; a token hit
121
+ // with the wrong amount is left unverified on purpose, so it surfaces through the
122
+ // normal unrecognised-payment path where a human looks at it.
123
+ const matchPendingPayinVerification = ({ mongoose, reference, amount, fiat, processor, originCLABE, originAccountHolderName, }) => __awaiter(void 0, void 0, void 0, function* () {
124
+ var _a;
125
+ const testReference = (0, exports.extractTestReference)(reference);
126
+ if (!testReference) {
127
+ return undefined;
128
+ }
129
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
130
+ try {
131
+ // Single atomic transition, so a webhook and a poller delivering the same
132
+ // deposit cannot both claim it.
133
+ const matched = yield ProcessorReadiness.findOneAndUpdate({
134
+ testReference,
135
+ status: common_1.ProcessorReadinessStatus.AwaitingTestDeposit,
136
+ fiat,
137
+ processor,
138
+ testAmount: (0, common_1.roundToDecimals)(amount, 2),
139
+ }, {
140
+ $set: Object.assign(Object.assign({ status: common_1.ProcessorReadinessStatus.Verified, verifiedAt: new Date(), verifiedVia: common_1.ProcessorReadinessVerificationMethod.TestDeposit }, (originCLABE ? { observedOriginCLABE: originCLABE } : {})), (originAccountHolderName
141
+ ? { observedOriginAccountHolderName: originAccountHolderName }
142
+ : {})),
143
+ }, { new: true });
144
+ if (!matched) {
145
+ return undefined;
146
+ }
147
+ return {
148
+ userId: matched.userId,
149
+ country: matched.country,
150
+ fiat: matched.fiat,
151
+ processor: matched.processor,
152
+ };
153
+ }
154
+ catch (error) {
155
+ // Must never break crediting a payment. Falling through leaves the deposit to
156
+ // the normal matching path, which at worst raises it as unrecognised.
157
+ (_a = logger_1.default
158
+ .getLogger()) === null || _a === void 0 ? void 0 : _a.error(`Unable to match pending payin verification ${testReference}: ${error}`);
159
+ return undefined;
160
+ }
161
+ });
162
+ exports.matchPendingPayinVerification = matchPendingPayinVerification;
@@ -0,0 +1,19 @@
1
+ import { Country, Fiat, Processor, Side } from "@riocrypto/common";
2
+ import { Mongoose } from "mongoose";
3
+ export declare const getDisabledProcessors: ({ mongoose, userId, country, fiat, side, }: {
4
+ mongoose: Mongoose;
5
+ userId: string;
6
+ country: Country;
7
+ fiat: Fiat;
8
+ side: Side;
9
+ }) => Promise<Processor[]>;
10
+ export declare const setProcessorEnablement: ({ mongoose, userId, country, fiat, side, processor, isEnabled, adminId, }: {
11
+ mongoose: Mongoose;
12
+ userId: string;
13
+ country: Country;
14
+ fiat: Fiat;
15
+ side: Side;
16
+ processor: Processor;
17
+ isEnabled: boolean;
18
+ adminId?: string | undefined;
19
+ }) => Promise<void>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.setProcessorEnablement = exports.getDisabledProcessors = void 0;
13
+ const common_1 = require("@riocrypto/common");
14
+ const processor_readiness_1 = require("../models/processor-readiness");
15
+ // Enablement is asked per side, so the purpose is fixed by the direction rather
16
+ // than chosen by the caller: verifications are ours to place and are never gated.
17
+ const getPurpose = (side) => side === common_1.Side.Buy
18
+ ? common_1.ProcessorRoutingPurpose.Buy
19
+ : common_1.ProcessorRoutingPurpose.Sell;
20
+ // Rails the user has switched off for one corridor and one direction, which order
21
+ // routing skips. On a buy that means a rail their bank will not let them send
22
+ // money to; on a sell, one it will not accept credits from.
23
+ //
24
+ // Only explicit refusals count. A rail with no record is enabled, which is what
25
+ // keeps a customer who has never opened their account settings trading exactly as
26
+ // they did before.
27
+ const getDisabledProcessors = ({ mongoose, userId, country, fiat, side, }) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
29
+ const readiness = yield ProcessorReadiness.find({
30
+ userId,
31
+ country,
32
+ fiat,
33
+ [`enablement.${side}.isEnabled`]: false,
34
+ })
35
+ .select({ processor: 1 })
36
+ .lean();
37
+ return readiness.map((record) => record.processor);
38
+ });
39
+ exports.getDisabledProcessors = getDisabledProcessors;
40
+ // Turns a rail on or off for one user in one direction, which is what order
41
+ // routing gates on. Deliberately says nothing about verification: a customer can
42
+ // enable a rail they have never tested, and switching one off leaves its test
43
+ // deposit result intact so turning it back on later does not mean testing again.
44
+ //
45
+ // Refuses to switch off the last rail standing. Enabling nothing means we have
46
+ // nowhere to send their money, and finding that out when they try to place an
47
+ // order is worse than being told here.
48
+ const setProcessorEnablement = ({ mongoose, userId, country, fiat, side, processor, isEnabled, adminId, }) => __awaiter(void 0, void 0, void 0, function* () {
49
+ const corridorProcessors = (0, common_1.getProcessorsForCorridor)(country, fiat, getPurpose(side));
50
+ if (!corridorProcessors.includes(processor)) {
51
+ throw new common_1.GenericInputError(side === common_1.Side.Buy
52
+ ? "This payment rail is not available for this country and currency"
53
+ : "This payout rail is not available for this country and currency");
54
+ }
55
+ if (!isEnabled) {
56
+ const disabled = yield (0, exports.getDisabledProcessors)({
57
+ mongoose,
58
+ userId,
59
+ country,
60
+ fiat,
61
+ side,
62
+ });
63
+ const stillEnabled = corridorProcessors.filter((candidate) => candidate !== processor && !disabled.includes(candidate));
64
+ if (!stillEnabled.length) {
65
+ throw new common_1.GenericInputError(side === common_1.Side.Buy
66
+ ? "At least one payment account has to stay enabled for this currency, otherwise there is nowhere to send your orders"
67
+ : "At least one payout account has to stay enabled for this currency, otherwise there is nowhere to pay your orders from");
68
+ }
69
+ }
70
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
71
+ yield ProcessorReadiness.updateOne({ userId, country, fiat, processor }, {
72
+ $set: {
73
+ [`enablement.${side}`]: Object.assign({ isEnabled, updatedAt: new Date() }, (adminId ? { updatedByAdminId: adminId } : {})),
74
+ },
75
+ $setOnInsert: {
76
+ createdAt: new Date(),
77
+ userId,
78
+ country,
79
+ fiat,
80
+ processor,
81
+ // The record may not exist yet: enablement is the first thing a customer
82
+ // touches on a rail they have never tested.
83
+ status: common_1.ProcessorReadinessStatus.Provisioned,
84
+ },
85
+ }, { upsert: true });
86
+ });
87
+ exports.setProcessorEnablement = setProcessorEnablement;
@@ -0,0 +1,10 @@
1
+ import { Country, Fiat, Processor } from "@riocrypto/common";
2
+ import { Mongoose } from "mongoose";
3
+ export declare const recordObservedProcessorReadiness: ({ mongoose, userId, country, fiat, processor, CLABE, }: {
4
+ mongoose: Mongoose;
5
+ userId: string;
6
+ country: Country;
7
+ fiat: Fiat;
8
+ processor: Processor;
9
+ CLABE?: string | undefined;
10
+ }) => Promise<void>;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.recordObservedProcessorReadiness = void 0;
16
+ const common_1 = require("@riocrypto/common");
17
+ const processor_readiness_1 = require("../models/processor-readiness");
18
+ const logger_1 = __importDefault(require("../services/logger"));
19
+ const DUPLICATE_KEY_ERROR_CODE = 11000;
20
+ // Records that money has actually arrived from a user through a rail, which is
21
+ // the strongest evidence there is that they can pay into it - stronger than a
22
+ // penny test, because it is a real payment at real size. Called from the payment
23
+ // matching paths so the record stays current without anyone maintaining it.
24
+ //
25
+ // Only touches verification. A rail the customer has switched off stays off even
26
+ // if a payment turns up on it, because the account they cannot send to next month
27
+ // is not made usable by one that arrived this month.
28
+ //
29
+ // Never downgrades an existing verified record: an admin override stays
30
+ // attributed to the admin who granted it.
31
+ const recordObservedProcessorReadiness = ({ mongoose, userId, country, fiat, processor, CLABE, }) => __awaiter(void 0, void 0, void 0, function* () {
32
+ var _a;
33
+ const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
34
+ try {
35
+ yield ProcessorReadiness.updateOne({
36
+ userId,
37
+ country,
38
+ fiat,
39
+ processor,
40
+ status: { $ne: common_1.ProcessorReadinessStatus.Verified },
41
+ }, {
42
+ $set: Object.assign({ status: common_1.ProcessorReadinessStatus.Verified, verifiedAt: new Date(), verifiedVia: common_1.ProcessorReadinessVerificationMethod.ObservedDeposit }, (CLABE ? { CLABE } : {})),
43
+ $setOnInsert: {
44
+ createdAt: new Date(),
45
+ userId,
46
+ country,
47
+ fiat,
48
+ processor,
49
+ },
50
+ }, { upsert: true });
51
+ }
52
+ catch (error) {
53
+ // The filter excludes already-verified records, so upsert tries to insert
54
+ // and the unique index rejects it. That is the desired outcome - the rail is
55
+ // verified - so it is not an error.
56
+ if ((error === null || error === void 0 ? void 0 : error.code) === DUPLICATE_KEY_ERROR_CODE) {
57
+ return;
58
+ }
59
+ // Readiness is an optimization of the routing gate, never a condition for
60
+ // crediting a payment, so a failure here must not fail the caller.
61
+ (_a = logger_1.default
62
+ .getLogger()) === null || _a === void 0 ? void 0 : _a.error(`Unable to record observed processor readiness for user ${userId} on ${processor}: ${error}`);
63
+ }
64
+ });
65
+ exports.recordObservedProcessorReadiness = recordObservedProcessorReadiness;