@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.
- package/build/clients/cluster-client.d.ts +36 -0
- package/build/clients/cluster-client.js +31 -1
- package/build/helpers/get-custom-rio-bank-account.d.ts +10 -0
- package/build/helpers/get-custom-rio-bank-account.js +37 -0
- package/build/helpers/get-processor.d.ts +7 -4
- package/build/helpers/get-processor.js +72 -55
- package/build/helpers/get-tax-rate.js +1 -0
- package/build/helpers/payin-destinations.d.ts +37 -0
- package/build/helpers/payin-destinations.js +286 -0
- package/build/helpers/payin-verification.d.ts +30 -0
- package/build/helpers/payin-verification.js +162 -0
- package/build/helpers/processor-enablement.d.ts +19 -0
- package/build/helpers/processor-enablement.js +87 -0
- package/build/helpers/record-observed-processor-readiness.d.ts +10 -0
- package/build/helpers/record-observed-processor-readiness.js +65 -0
- package/build/helpers/resolve-processor.d.ts +18 -0
- package/build/helpers/resolve-processor.js +122 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +10 -0
- package/build/middlewares/get-order.js +1 -0
- package/build/models/bank-account-verification.d.ts +2 -2
- package/build/models/bulk-bank-payment.js +3 -0
- package/build/models/fintoc-deposit-CLABE.d.ts +28 -0
- package/build/models/fintoc-deposit-CLABE.js +56 -0
- package/build/models/fintoc-mxn-withdrawal.d.ts +51 -0
- package/build/models/fintoc-mxn-withdrawal.js +88 -0
- package/build/models/fireblocks-vault.d.ts +8 -0
- package/build/models/fireblocks-vault.js +8 -0
- package/build/models/internal-balances.d.ts +3 -0
- package/build/models/internal-balances.js +3 -0
- package/build/models/payment-address-verification.d.ts +49 -0
- package/build/models/payment-address-verification.js +77 -0
- package/build/models/processor-readiness.d.ts +44 -0
- package/build/models/processor-readiness.js +90 -0
- package/build/models/rio-bank-account.d.ts +3 -1
- package/build/models/rio-bank-account.js +3 -0
- package/build/models/rio-settings.d.ts +3 -11
- package/build/models/rio-settings.js +1 -2
- 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;
|