@riocrypto/common 1.0.2720 → 1.0.2721
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/helpers/resolve-processor-routing-percentages.d.ts +84 -0
- package/build/helpers/resolve-processor-routing-percentages.js +163 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/types/processor-routing.d.ts +47 -0
- package/build/types/processor-routing.js +8 -1
- package/build/types/routing-decision.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Country } from "../types/country";
|
|
2
|
+
import { Fiat } from "../types/fiat";
|
|
3
|
+
import { Processor } from "../types/processor";
|
|
4
|
+
import { ProcessorRoutingPurpose, ProcessorRoutingRule, ProcessorRoutingWindow } from "../types/processor-routing";
|
|
5
|
+
export interface ResolvedRoutingPercentages {
|
|
6
|
+
percentages: {
|
|
7
|
+
[key in Processor]?: number;
|
|
8
|
+
};
|
|
9
|
+
/** The window the percentages came from, absent when the base split was used. */
|
|
10
|
+
matchedWindow?: ProcessorRoutingWindow;
|
|
11
|
+
}
|
|
12
|
+
export declare const ROUTING_TOTAL_TOLERANCE = 0.01;
|
|
13
|
+
/**
|
|
14
|
+
* Whether one window is usable on its own terms, ignoring its neighbours.
|
|
15
|
+
*
|
|
16
|
+
* Shares are checked here rather than only in the settings validator because
|
|
17
|
+
* this is also what the resolver trusts at routing time. A window whose shares
|
|
18
|
+
* do not add up would quietly renormalize into a split nobody chose, which is
|
|
19
|
+
* worse than falling back to the base split.
|
|
20
|
+
*/
|
|
21
|
+
export declare const isWellFormedProcessorRoutingWindow: (window: ProcessorRoutingWindow, country: Country, fiat: Fiat, purpose: ProcessorRoutingPurpose) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The split a corridor runs on right now, according to configuration alone.
|
|
24
|
+
*
|
|
25
|
+
* Kept here rather than inside `resolveProcessor` so the settings page can show
|
|
26
|
+
* an operator which window is live without a round trip, and so the two cannot
|
|
27
|
+
* disagree about which minute belongs to which window. Knows nothing about
|
|
28
|
+
* eligibility, enablement or the roll between rails, all of which stay in
|
|
29
|
+
* routing where the customer's records are.
|
|
30
|
+
*
|
|
31
|
+
* Malformed configuration falls back to the rule's own percentages rather than
|
|
32
|
+
* throwing. This sits on the path that prices every order, so a bad hand-edit in
|
|
33
|
+
* Mongo should cost the schedule rather than the corridor.
|
|
34
|
+
*/
|
|
35
|
+
export declare const resolveProcessorRoutingPercentages: (rule: ProcessorRoutingRule | undefined, country: Country, fiat: Fiat, purpose: ProcessorRoutingPurpose, now?: Date) => ResolvedRoutingPercentages;
|
|
36
|
+
/**
|
|
37
|
+
* The single statement of the window coverage rule, shared by the throwing
|
|
38
|
+
* validator on the `rio-settings` PATCH and by the dashboard editor's inline
|
|
39
|
+
* check, so the two cannot disagree about what a valid schedule is.
|
|
40
|
+
*
|
|
41
|
+
* Returns a human-readable description of the first problem, or null when the
|
|
42
|
+
* list is acceptable. An empty list is acceptable and means no schedule.
|
|
43
|
+
*
|
|
44
|
+
* Order is significant and the list is deliberately not sorted first. If an
|
|
45
|
+
* operator's rows are out of order, saying so is more useful than silently
|
|
46
|
+
* reinterpreting what they wrote as something that happens to be valid.
|
|
47
|
+
*/
|
|
48
|
+
export declare const findProcessorRoutingWindowCoverageIssue: (windows: ProcessorRoutingWindow[], country: Country, fiat: Fiat, purpose: ProcessorRoutingPurpose) => string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the contiguous day that covers the common case of "run a different
|
|
51
|
+
* split for one stretch of the afternoon", so neither the operator nor the
|
|
52
|
+
* dashboard has to assemble the filler rows by hand. Returns a single full-day
|
|
53
|
+
* window when the stretch covers everything.
|
|
54
|
+
*
|
|
55
|
+
* Requiring full coverage is what makes a stored schedule unambiguous. It should
|
|
56
|
+
* not also make the routine edit tedious.
|
|
57
|
+
*/
|
|
58
|
+
export declare const buildContiguousProcessorRoutingWindows: (basePercentages: {
|
|
59
|
+
interbank?: number | undefined;
|
|
60
|
+
swift?: number | undefined;
|
|
61
|
+
spei?: number | undefined;
|
|
62
|
+
spid?: number | undefined;
|
|
63
|
+
ksr?: number | undefined;
|
|
64
|
+
bridge?: number | undefined;
|
|
65
|
+
"spei-stp"?: number | undefined;
|
|
66
|
+
"spei-nvio"?: number | undefined;
|
|
67
|
+
"spei-fintoc"?: number | undefined;
|
|
68
|
+
"binance-rfq"?: number | undefined;
|
|
69
|
+
coltefinanciera?: number | undefined;
|
|
70
|
+
alfin?: number | undefined;
|
|
71
|
+
}, windowPercentages: {
|
|
72
|
+
interbank?: number | undefined;
|
|
73
|
+
swift?: number | undefined;
|
|
74
|
+
spei?: number | undefined;
|
|
75
|
+
spid?: number | undefined;
|
|
76
|
+
ksr?: number | undefined;
|
|
77
|
+
bridge?: number | undefined;
|
|
78
|
+
"spei-stp"?: number | undefined;
|
|
79
|
+
"spei-nvio"?: number | undefined;
|
|
80
|
+
"spei-fintoc"?: number | undefined;
|
|
81
|
+
"binance-rfq"?: number | undefined;
|
|
82
|
+
coltefinanciera?: number | undefined;
|
|
83
|
+
alfin?: number | undefined;
|
|
84
|
+
}, startMinute: number, endMinute: number) => ProcessorRoutingWindow[];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildContiguousProcessorRoutingWindows = exports.findProcessorRoutingWindowCoverageIssue = exports.resolveProcessorRoutingPercentages = exports.isWellFormedProcessorRoutingWindow = exports.ROUTING_TOTAL_TOLERANCE = void 0;
|
|
4
|
+
const fx_price_source_policy_1 = require("../types/fx-price-source-policy");
|
|
5
|
+
const processor_routing_1 = require("../types/processor-routing");
|
|
6
|
+
const get_country_timezone_1 = require("./get-country-timezone");
|
|
7
|
+
const get_minute_of_day_in_timezone_1 = require("./get-minute-of-day-in-timezone");
|
|
8
|
+
const get_processors_for_corridor_1 = require("./get-processors-for-corridor");
|
|
9
|
+
// Entered by hand and allowed a decimal, so totals are compared with a tolerance
|
|
10
|
+
// rather than exactly. Shared with the rule's own total so a window is held to
|
|
11
|
+
// the same standard as the split it is standing in for.
|
|
12
|
+
exports.ROUTING_TOTAL_TOLERANCE = 0.01;
|
|
13
|
+
const isWholeMinute = (value) => typeof value === "number" && Number.isInteger(value);
|
|
14
|
+
const sumPercentages = (percentages) => Object.values(percentages || {}).reduce((total, percentage) => total + (Number(percentage) || 0), 0);
|
|
15
|
+
/**
|
|
16
|
+
* Whether one window is usable on its own terms, ignoring its neighbours.
|
|
17
|
+
*
|
|
18
|
+
* Shares are checked here rather than only in the settings validator because
|
|
19
|
+
* this is also what the resolver trusts at routing time. A window whose shares
|
|
20
|
+
* do not add up would quietly renormalize into a split nobody chose, which is
|
|
21
|
+
* worse than falling back to the base split.
|
|
22
|
+
*/
|
|
23
|
+
const isWellFormedProcessorRoutingWindow = (window, country, fiat, purpose) => {
|
|
24
|
+
if (!window || typeof window !== "object") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const { startMinute, endMinute, percentages } = window;
|
|
28
|
+
if (!isWholeMinute(startMinute) || !isWholeMinute(endMinute)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (startMinute < 0 || endMinute > fx_price_source_policy_1.MINUTES_IN_DAY) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (startMinute >= endMinute) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (typeof percentages !== "object" || percentages === null) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const allowed = (0, get_processors_for_corridor_1.getProcessorsForCorridor)(country, fiat, purpose);
|
|
41
|
+
for (const [processor, percentage] of Object.entries(percentages)) {
|
|
42
|
+
if (!allowed.includes(processor)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (typeof percentage !== "number" ||
|
|
46
|
+
!Number.isFinite(percentage) ||
|
|
47
|
+
percentage < 0 ||
|
|
48
|
+
percentage > 100) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return (Math.abs(sumPercentages(percentages) - 100) <= exports.ROUTING_TOTAL_TOLERANCE);
|
|
53
|
+
};
|
|
54
|
+
exports.isWellFormedProcessorRoutingWindow = isWellFormedProcessorRoutingWindow;
|
|
55
|
+
const windowMatchesMinute = (window, minuteOfDay) => minuteOfDay >= window.startMinute && minuteOfDay < window.endMinute;
|
|
56
|
+
/**
|
|
57
|
+
* The split a corridor runs on right now, according to configuration alone.
|
|
58
|
+
*
|
|
59
|
+
* Kept here rather than inside `resolveProcessor` so the settings page can show
|
|
60
|
+
* an operator which window is live without a round trip, and so the two cannot
|
|
61
|
+
* disagree about which minute belongs to which window. Knows nothing about
|
|
62
|
+
* eligibility, enablement or the roll between rails, all of which stay in
|
|
63
|
+
* routing where the customer's records are.
|
|
64
|
+
*
|
|
65
|
+
* Malformed configuration falls back to the rule's own percentages rather than
|
|
66
|
+
* throwing. This sits on the path that prices every order, so a bad hand-edit in
|
|
67
|
+
* Mongo should cost the schedule rather than the corridor.
|
|
68
|
+
*/
|
|
69
|
+
const resolveProcessorRoutingPercentages = (rule, country, fiat, purpose, now = new Date()) => {
|
|
70
|
+
var _a;
|
|
71
|
+
const percentages = (rule === null || rule === void 0 ? void 0 : rule.percentages) || {};
|
|
72
|
+
if ((rule === null || rule === void 0 ? void 0 : rule.mode) !== processor_routing_1.ProcessorRoutingMode.Scheduled) {
|
|
73
|
+
return { percentages };
|
|
74
|
+
}
|
|
75
|
+
const windows = (_a = rule.windows) !== null && _a !== void 0 ? _a : [];
|
|
76
|
+
if (!Array.isArray(windows) || windows.length === 0) {
|
|
77
|
+
return { percentages };
|
|
78
|
+
}
|
|
79
|
+
const minuteOfDay = (0, get_minute_of_day_in_timezone_1.getMinuteOfDayInTimezone)(now, (0, get_country_timezone_1.getCountryTimezone)(country));
|
|
80
|
+
const matchedWindow = windows.find((window) => (0, exports.isWellFormedProcessorRoutingWindow)(window, country, fiat, purpose) &&
|
|
81
|
+
windowMatchesMinute(window, minuteOfDay));
|
|
82
|
+
// Validated windows cover the whole day, so a miss means the stored schedule
|
|
83
|
+
// is malformed or was written for a corridor that has since lost a rail.
|
|
84
|
+
if (!matchedWindow) {
|
|
85
|
+
return { percentages };
|
|
86
|
+
}
|
|
87
|
+
return { percentages: matchedWindow.percentages, matchedWindow };
|
|
88
|
+
};
|
|
89
|
+
exports.resolveProcessorRoutingPercentages = resolveProcessorRoutingPercentages;
|
|
90
|
+
/**
|
|
91
|
+
* The single statement of the window coverage rule, shared by the throwing
|
|
92
|
+
* validator on the `rio-settings` PATCH and by the dashboard editor's inline
|
|
93
|
+
* check, so the two cannot disagree about what a valid schedule is.
|
|
94
|
+
*
|
|
95
|
+
* Returns a human-readable description of the first problem, or null when the
|
|
96
|
+
* list is acceptable. An empty list is acceptable and means no schedule.
|
|
97
|
+
*
|
|
98
|
+
* Order is significant and the list is deliberately not sorted first. If an
|
|
99
|
+
* operator's rows are out of order, saying so is more useful than silently
|
|
100
|
+
* reinterpreting what they wrote as something that happens to be valid.
|
|
101
|
+
*/
|
|
102
|
+
const findProcessorRoutingWindowCoverageIssue = (windows, country, fiat, purpose) => {
|
|
103
|
+
var _a;
|
|
104
|
+
if (!Array.isArray(windows)) {
|
|
105
|
+
return "windows must be an array";
|
|
106
|
+
}
|
|
107
|
+
if (windows.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
for (let i = 0; i < windows.length; i += 1) {
|
|
111
|
+
if (!(0, exports.isWellFormedProcessorRoutingWindow)(windows[i], country, fiat, purpose)) {
|
|
112
|
+
const total = sumPercentages(((_a = windows[i]) === null || _a === void 0 ? void 0 : _a.percentages) || {});
|
|
113
|
+
return `window[${i}] needs whole-minute startMinute < endMinute within 0..${fx_price_source_policy_1.MINUTES_IN_DAY} and percentages adding up to 100 over ${(0, get_processors_for_corridor_1.getProcessorsForCorridor)(country, fiat, purpose).join(", ")}, got ${total}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (windows[0].startMinute !== 0) {
|
|
117
|
+
return `window[0].startMinute must be 0`;
|
|
118
|
+
}
|
|
119
|
+
for (let i = 0; i < windows.length - 1; i += 1) {
|
|
120
|
+
if (windows[i + 1].startMinute !== windows[i].endMinute) {
|
|
121
|
+
return `windows must be contiguous: window[${i}].endMinute (${windows[i].endMinute}) must equal window[${i + 1}].startMinute (${windows[i + 1].startMinute})`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const last = windows[windows.length - 1];
|
|
125
|
+
if (last.endMinute !== fx_price_source_policy_1.MINUTES_IN_DAY) {
|
|
126
|
+
return `window[${windows.length - 1}].endMinute must be ${fx_price_source_policy_1.MINUTES_IN_DAY} so the windows cover the full day`;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
exports.findProcessorRoutingWindowCoverageIssue = findProcessorRoutingWindowCoverageIssue;
|
|
131
|
+
/**
|
|
132
|
+
* Builds the contiguous day that covers the common case of "run a different
|
|
133
|
+
* split for one stretch of the afternoon", so neither the operator nor the
|
|
134
|
+
* dashboard has to assemble the filler rows by hand. Returns a single full-day
|
|
135
|
+
* window when the stretch covers everything.
|
|
136
|
+
*
|
|
137
|
+
* Requiring full coverage is what makes a stored schedule unambiguous. It should
|
|
138
|
+
* not also make the routine edit tedious.
|
|
139
|
+
*/
|
|
140
|
+
const buildContiguousProcessorRoutingWindows = (basePercentages, windowPercentages, startMinute, endMinute) => {
|
|
141
|
+
const windows = [];
|
|
142
|
+
if (startMinute > 0) {
|
|
143
|
+
windows.push({
|
|
144
|
+
startMinute: 0,
|
|
145
|
+
endMinute: startMinute,
|
|
146
|
+
percentages: Object.assign({}, basePercentages),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
windows.push({
|
|
150
|
+
startMinute,
|
|
151
|
+
endMinute,
|
|
152
|
+
percentages: Object.assign({}, windowPercentages),
|
|
153
|
+
});
|
|
154
|
+
if (endMinute < fx_price_source_policy_1.MINUTES_IN_DAY) {
|
|
155
|
+
windows.push({
|
|
156
|
+
startMinute: endMinute,
|
|
157
|
+
endMinute: fx_price_source_policy_1.MINUTES_IN_DAY,
|
|
158
|
+
percentages: Object.assign({}, basePercentages),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return windows;
|
|
162
|
+
};
|
|
163
|
+
exports.buildContiguousProcessorRoutingWindows = buildContiguousProcessorRoutingWindows;
|
package/build/index.d.ts
CHANGED
|
@@ -591,6 +591,7 @@ export * from "./helpers/get-country-for-fiat";
|
|
|
591
591
|
export * from "./helpers/get-minute-of-day-in-timezone";
|
|
592
592
|
export * from "./helpers/resolve-fx-price-source";
|
|
593
593
|
export * from "./helpers/get-processors-for-corridor";
|
|
594
|
+
export * from "./helpers/resolve-processor-routing-percentages";
|
|
594
595
|
export * from "./helpers/get-default-processor";
|
|
595
596
|
export * from "./helpers/get-routing-candidates";
|
|
596
597
|
export * from "./helpers/verifiable-payin-processors";
|
package/build/index.js
CHANGED
|
@@ -607,6 +607,7 @@ __exportStar(require("./helpers/get-country-for-fiat"), exports);
|
|
|
607
607
|
__exportStar(require("./helpers/get-minute-of-day-in-timezone"), exports);
|
|
608
608
|
__exportStar(require("./helpers/resolve-fx-price-source"), exports);
|
|
609
609
|
__exportStar(require("./helpers/get-processors-for-corridor"), exports);
|
|
610
|
+
__exportStar(require("./helpers/resolve-processor-routing-percentages"), exports);
|
|
610
611
|
__exportStar(require("./helpers/get-default-processor"), exports);
|
|
611
612
|
__exportStar(require("./helpers/get-routing-candidates"), exports);
|
|
612
613
|
__exportStar(require("./helpers/verifiable-payin-processors"), exports);
|
|
@@ -6,7 +6,54 @@ export declare enum ProcessorRoutingPurpose {
|
|
|
6
6
|
Sell = "sell",
|
|
7
7
|
BankAccountVerification = "bankAccountVerification"
|
|
8
8
|
}
|
|
9
|
+
export declare enum ProcessorRoutingMode {
|
|
10
|
+
/** Always use `percentages`. Windows are ignored. */
|
|
11
|
+
Manual = "manual",
|
|
12
|
+
/** Use the window covering the current local minute. */
|
|
13
|
+
Scheduled = "scheduled"
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One time-of-day window naming the split a corridor runs on while it is
|
|
17
|
+
* active.
|
|
18
|
+
*
|
|
19
|
+
* Minutes are from midnight in the corridor country's local timezone and the
|
|
20
|
+
* window is `[startMinute, endMinute)`. Local time because the reason to schedule
|
|
21
|
+
* a split at all is somebody else's working day: a provider's support desk, a
|
|
22
|
+
* bank's clearing window, a treasury team arriving at their desks.
|
|
23
|
+
*
|
|
24
|
+
* Windows follow the same coverage rule as `FXPriceSourceWindow`, being ordered,
|
|
25
|
+
* strictly contiguous, first starting at 0 and last ending at 1440, so every
|
|
26
|
+
* minute of the day maps to exactly one window. Each window's percentages carry
|
|
27
|
+
* the same obligation as the rule's own, summing to 100 over rails the corridor
|
|
28
|
+
* serves, so there is never a stretch of the day whose split is partly stated.
|
|
29
|
+
*
|
|
30
|
+
* The cost is that moving one afternoon onto Fintoc takes three rows rather than
|
|
31
|
+
* one, each restating every rail. The benefit is that the configuration says what
|
|
32
|
+
* it means at every minute, an operator reading it back at 3am does not have to
|
|
33
|
+
* hold "and the rest of the day is the base split" in their head, and the editor
|
|
34
|
+
* can draw one unbroken day they can check at a glance. A window spanning
|
|
35
|
+
* midnight is written as two rows rather than wrapping.
|
|
36
|
+
*/
|
|
37
|
+
export interface ProcessorRoutingWindow {
|
|
38
|
+
startMinute: number;
|
|
39
|
+
endMinute: number;
|
|
40
|
+
percentages: {
|
|
41
|
+
[key in Processor]?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
9
44
|
export interface ProcessorRoutingRule {
|
|
45
|
+
/**
|
|
46
|
+
* Whether this corridor runs one split all day or a scheduled one.
|
|
47
|
+
*
|
|
48
|
+
* Absent means `Manual`, so every rule saved before scheduling existed keeps
|
|
49
|
+
* behaving exactly as it did.
|
|
50
|
+
*/
|
|
51
|
+
mode?: ProcessorRoutingMode;
|
|
52
|
+
/**
|
|
53
|
+
* Ordered, contiguous windows covering `[0, 1440)`. Only consulted in
|
|
54
|
+
* `Scheduled` mode. Empty means no schedule.
|
|
55
|
+
*/
|
|
56
|
+
windows?: ProcessorRoutingWindow[];
|
|
10
57
|
percentages: {
|
|
11
58
|
[key in Processor]?: number;
|
|
12
59
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProcessorRoutingPurpose = void 0;
|
|
3
|
+
exports.ProcessorRoutingMode = exports.ProcessorRoutingPurpose = void 0;
|
|
4
4
|
// What a rule routes. Buy and Sell deliberately carry the same values as the
|
|
5
5
|
// Side enum, so a config written when rules were keyed by side needs no
|
|
6
6
|
// migration. Bank account verifications route through the same machinery rather
|
|
@@ -12,3 +12,10 @@ var ProcessorRoutingPurpose;
|
|
|
12
12
|
ProcessorRoutingPurpose["Sell"] = "sell";
|
|
13
13
|
ProcessorRoutingPurpose["BankAccountVerification"] = "bankAccountVerification";
|
|
14
14
|
})(ProcessorRoutingPurpose = exports.ProcessorRoutingPurpose || (exports.ProcessorRoutingPurpose = {}));
|
|
15
|
+
var ProcessorRoutingMode;
|
|
16
|
+
(function (ProcessorRoutingMode) {
|
|
17
|
+
/** Always use `percentages`. Windows are ignored. */
|
|
18
|
+
ProcessorRoutingMode["Manual"] = "manual";
|
|
19
|
+
/** Use the window covering the current local minute. */
|
|
20
|
+
ProcessorRoutingMode["Scheduled"] = "scheduled";
|
|
21
|
+
})(ProcessorRoutingMode = exports.ProcessorRoutingMode || (exports.ProcessorRoutingMode = {}));
|