@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.
@@ -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 = {}));
@@ -21,6 +21,10 @@ export interface RoutingDecision {
21
21
  percentages?: {
22
22
  [processor in Processor]?: number;
23
23
  };
24
+ window?: {
25
+ startMinute: number;
26
+ endMinute: number;
27
+ };
24
28
  seed?: number;
25
29
  decidedAt: Date;
26
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common",
3
- "version": "1.0.2720",
3
+ "version": "1.0.2721",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",