@riocrypto/common-server 1.0.2896 → 1.0.2898

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.
@@ -3,7 +3,7 @@ export declare const getEnvironmentExcludedProcessors: (purpose: ProcessorRoutin
3
3
  export type ProcessorSelection = RoutingDecision & {
4
4
  processor: Processor;
5
5
  };
6
- export declare const getProcessor: ({ country, fiat, side, isBinanceRFQ, routingConfig, enablement, requireExplicitPayinEnablement, customRioBankAccount, }: {
6
+ export declare const getProcessor: ({ country, fiat, side, isBinanceRFQ, routingConfig, enablement, requireExplicitPayinEnablement, requireExplicitPayoutEnablement, customRioBankAccount, }: {
7
7
  country: Country;
8
8
  fiat: Fiat;
9
9
  side: Side;
@@ -11,6 +11,7 @@ export declare const getProcessor: ({ country, fiat, side, isBinanceRFQ, routing
11
11
  routingConfig?: ProcessorRoutingConfig | undefined;
12
12
  enablement?: ProcessorEnablementState | undefined;
13
13
  requireExplicitPayinEnablement?: boolean | undefined;
14
+ requireExplicitPayoutEnablement?: boolean | undefined;
14
15
  customRioBankAccount?: {
15
16
  processor?: Processor | undefined;
16
17
  } | undefined;
@@ -27,7 +27,7 @@ const getEnvironmentExcludedProcessors = (purpose) => [
27
27
  ...(isSTPAvailable(purpose) ? [] : [common_1.Processor.SPEI_STP]),
28
28
  ];
29
29
  exports.getEnvironmentExcludedProcessors = getEnvironmentExcludedProcessors;
30
- const getProcessor = ({ country, fiat, side, isBinanceRFQ, routingConfig, enablement, requireExplicitPayinEnablement, customRioBankAccount, }) => {
30
+ const getProcessor = ({ country, fiat, side, isBinanceRFQ, routingConfig, enablement, requireExplicitPayinEnablement, requireExplicitPayoutEnablement, customRioBankAccount, }) => {
31
31
  if (isBinanceRFQ === true) {
32
32
  return {
33
33
  processor: common_1.Processor.BinanceRFQ,
@@ -56,17 +56,21 @@ const getProcessor = ({ country, fiat, side, isBinanceRFQ, routingConfig, enable
56
56
  routingConfig,
57
57
  excludedProcessors,
58
58
  enablement,
59
- requireExplicitEnablement: requireExplicitPayinEnablement,
59
+ requireExplicitEnablement: side === common_1.Side.Buy
60
+ ? requireExplicitPayinEnablement
61
+ : requireExplicitPayoutEnablement,
60
62
  customRioBankAccount,
61
63
  });
62
64
  if (routed.processor) {
63
65
  return Object.assign(Object.assign({}, routed), { processor: routed.processor });
64
66
  }
65
- // The customer has never told us which of these accounts their bank will let
66
- // them pay, so there is nothing to route to. Says so in their terms, since
67
- // the fix is theirs to make rather than something support has to escalate.
67
+ // The customer has never told us which of these accounts works for them, so
68
+ // there is nothing to route to. Says so in their terms, since the fix is
69
+ // theirs to make rather than something support has to escalate.
68
70
  if (routed.reason === common_1.RoutingDecisionReason.NoAcceptedCandidate) {
69
- throw new common_1.GenericInputError("You need to turn on a payment account for this currency before you can buy. You can do that from the payment accounts page, once your bank allows transfers to it.");
71
+ throw new common_1.GenericInputError(side === common_1.Side.Buy
72
+ ? "You need to turn on a payment account for this currency before you can buy. You can do that from the payment accounts page, once your bank allows transfers to it."
73
+ : "You need to turn on a payout account for this currency before you can sell. You can do that from the payout accounts page.");
70
74
  }
71
75
  // Every rail in this corridor is switched off for this direction, so there is
72
76
  // nowhere to send them - a rail at zero percent would still have taken the
@@ -30,9 +30,10 @@ export declare const setProcessorEnablement: ({ mongoose, userId, country, fiat,
30
30
  adminId?: string | undefined;
31
31
  requireExplicitEnablement?: boolean | undefined;
32
32
  }) => Promise<void>;
33
- export declare const ensureDefaultPayinEnablement: ({ mongoose, userId, country, fiat, }: {
33
+ export declare const ensureDefaultEnablement: ({ mongoose, userId, country, fiat, side, }: {
34
34
  mongoose: Mongoose;
35
35
  userId: string;
36
36
  country: Country;
37
37
  fiat: Fiat;
38
+ side: Side;
38
39
  }) => Promise<Processor | undefined>;
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ensureDefaultPayinEnablement = exports.setProcessorEnablement = exports.getProcessorEnablementState = exports.isProcessorEnabled = void 0;
12
+ exports.ensureDefaultEnablement = exports.setProcessorEnablement = exports.getProcessorEnablementState = exports.isProcessorEnabled = void 0;
13
13
  const common_1 = require("@riocrypto/common");
14
14
  const processor_readiness_1 = require("../models/processor-readiness");
15
15
  const get_processor_1 = require("./get-processor");
@@ -20,22 +20,21 @@ const getPurpose = (side) => side === common_1.Side.Buy
20
20
  : common_1.ProcessorRoutingPurpose.Sell;
21
21
  // Whether one rail may carry one customer's orders in one direction.
22
22
  //
23
- // The two directions disagree about silence, which is the whole point. A payout
24
- // needs nothing arranged at the customer's bank, so a rail nobody has spoken
25
- // about can pay them. A payin asks them to send money to an account their bank
26
- // has to allow first, so silence means no while payin accounts are opt-in.
27
- const isProcessorEnabled = ({ enablement, side, requireExplicitEnablement, }) => {
28
- var _a, _b;
29
- return (_b = (_a = enablement === null || enablement === void 0 ? void 0 : enablement[side]) === null || _a === void 0 ? void 0 : _a.isEnabled) !== null && _b !== void 0 ? _b : (side === common_1.Side.Sell || !requireExplicitEnablement);
30
- };
23
+ // What silence means is the caller's to decide, by passing the setting for this
24
+ // direction: `requireExplicitPayinEnablement` on a buy and
25
+ // `requireExplicitPayoutEnablement` on a sell. On, a rail nobody has spoken about
26
+ // cannot carry the order; off, it can, which is how both directions behaved before
27
+ // enablement became opt-in.
28
+ const isProcessorEnabled = ({ enablement, side, requireExplicitEnablement, }) => { var _a, _b; return (_b = (_a = enablement === null || enablement === void 0 ? void 0 : enablement[side]) === null || _a === void 0 ? void 0 : _a.isEnabled) !== null && _b !== void 0 ? _b : !requireExplicitEnablement; };
31
29
  exports.isProcessorEnabled = isProcessorEnabled;
32
30
  // What a customer has said about each rail in one corridor and one direction.
33
31
  //
34
32
  // Reports the three sets rather than a verdict so that the policy lives in one
35
33
  // place, in routing, instead of being decided here and again wherever a page
36
- // needs to show the same thing. Only rails the corridor currently serves are
37
- // reported, so a record left behind by a rail we no longer route to is ignored
38
- // rather than resurrected.
34
+ // needs to show the same thing. Whether an unset rail counts as usable is that
35
+ // policy, and it belongs to the caller with the settings in hand. Only rails the
36
+ // corridor currently serves are reported, so a record left behind by a rail we no
37
+ // longer route to is ignored rather than resurrected.
39
38
  const getProcessorEnablementState = ({ mongoose, userId, country, fiat, side, }) => __awaiter(void 0, void 0, void 0, function* () {
40
39
  const ProcessorReadiness = (0, processor_readiness_1.buildProcessorReadiness)(mongoose);
41
40
  const records = yield ProcessorReadiness.find({
@@ -98,7 +97,7 @@ const setProcessorEnablement = ({ mongoose, userId, country, fiat, side, process
98
97
  });
99
98
  const stillUsable = [
100
99
  ...state.enabled,
101
- ...(side === common_1.Side.Sell || !requireExplicitEnablement ? state.unset : []),
100
+ ...(requireExplicitEnablement ? [] : state.unset),
102
101
  ].filter((candidate) => candidate !== processor);
103
102
  if (!stillUsable.length) {
104
103
  throw new common_1.GenericInputError(side === common_1.Side.Buy
@@ -124,9 +123,9 @@ const setProcessorEnablement = ({ mongoose, userId, country, fiat, side, process
124
123
  }, { upsert: true });
125
124
  });
126
125
  exports.setProcessorEnablement = setProcessorEnablement;
127
- // Gives a customer with no payin account turned on the one their corridor would
128
- // have routed them to anyway, so opt-in never leaves somebody unable to be
129
- // quoted. Called when an onboarding is approved, and by the backfill for accounts
126
+ // Gives a customer with nothing turned on in one direction the rail their corridor
127
+ // would have routed them to anyway, so opt-in never leaves somebody unable to
128
+ // trade. Called when an onboarding is approved, and by the backfill for accounts
130
129
  // that predate all of this.
131
130
  //
132
131
  // Skips anyone who has already turned a rail on, because their answer is better
@@ -135,13 +134,14 @@ exports.setProcessorEnablement = setProcessorEnablement;
135
134
  // the corridor's own order, which is the rail routing would have picked for them
136
135
  // before opt-in existed. Returns the rail it turned on, or nothing if it left
137
136
  // them alone.
138
- const ensureDefaultPayinEnablement = ({ mongoose, userId, country, fiat, }) => __awaiter(void 0, void 0, void 0, function* () {
137
+ const ensureDefaultEnablement = ({ mongoose, userId, country, fiat, side, }) => __awaiter(void 0, void 0, void 0, function* () {
138
+ const purpose = getPurpose(side);
139
139
  const state = yield (0, exports.getProcessorEnablementState)({
140
140
  mongoose,
141
141
  userId,
142
142
  country,
143
143
  fiat,
144
- side: common_1.Side.Buy,
144
+ side,
145
145
  });
146
146
  if (state.enabled.length) {
147
147
  return undefined;
@@ -150,10 +150,10 @@ const ensureDefaultPayinEnablement = ({ mongoose, userId, country, fiat, }) => _
150
150
  // seeding a rail this deployment cannot reach would hand the customer an
151
151
  // account nothing can route to and read as opt-in having blocked them.
152
152
  const unreachable = [
153
- ...(0, get_processor_1.getEnvironmentExcludedProcessors)(common_1.ProcessorRoutingPurpose.Buy),
153
+ ...(0, get_processor_1.getEnvironmentExcludedProcessors)(purpose),
154
154
  ...state.disabled,
155
155
  ];
156
- const seed = (0, common_1.getDefaultProcessor)(country, fiat, common_1.ProcessorRoutingPurpose.Buy, unreachable) ||
156
+ const seed = (0, common_1.getDefaultProcessor)(country, fiat, purpose, unreachable) ||
157
157
  state.unset.find((candidate) => !unreachable.includes(candidate));
158
158
  if (!seed || !state.unset.includes(seed)) {
159
159
  return undefined;
@@ -163,10 +163,10 @@ const ensureDefaultPayinEnablement = ({ mongoose, userId, country, fiat, }) => _
163
163
  userId,
164
164
  country,
165
165
  fiat,
166
- side: common_1.Side.Buy,
166
+ side,
167
167
  processor: seed,
168
168
  isEnabled: true,
169
169
  });
170
170
  return seed;
171
171
  });
172
- exports.ensureDefaultPayinEnablement = ensureDefaultPayinEnablement;
172
+ exports.ensureDefaultEnablement = ensureDefaultEnablement;
@@ -27,7 +27,9 @@ const pickByShare = (candidates, position) => {
27
27
  // belonging to a corridor with no rule at all still takes their orders when it is
28
28
  // the only one available to them, which is why the candidate set comes from the
29
29
  // corridor rather than from the rule. What keeps an order away from a rail is the
30
- // customer, by switching it off or, on buys, by never turning it on.
30
+ // customer, by switching it off or, while that direction is opt-in, by never
31
+ // turning it on. Unless the rule says otherwise: a split set to apply to all
32
+ // users divides the corridor by percentage alone and asks nobody.
31
33
  //
32
34
  // Does no IO of its own - the routing config and the user's enablement are passed
33
35
  // in - and the only nondeterminism is the per-order roll, which a caller can
@@ -110,28 +112,33 @@ const resolveProcessor = ({ country, fiat, purpose, routingConfig, excludedProce
110
112
  // can refuse a wire from Bancrea as easily as a SPEI transfer from Fintoc.
111
113
  //
112
114
  // Verifications are exempt because they are ours to place against an account the
113
- // customer gave us, not a rail they have to accept.
115
+ // customer gave us, not a rail they have to accept. So is a corridor whose split
116
+ // has been set to apply to everyone, which is the deliberate exception: see
117
+ // `appliesToAllUsers`.
114
118
  const isGated = purpose !== common_1.ProcessorRoutingPurpose.BankAccountVerification;
115
119
  const disabled = isGated ? (enablement === null || enablement === void 0 ? void 0 : enablement.disabled) || [] : [];
116
- const unset = isGated && requireExplicitEnablement && purpose === common_1.ProcessorRoutingPurpose.Buy
117
- ? (enablement === null || enablement === void 0 ? void 0 : enablement.unset) || []
118
- : [];
119
- const candidates = eligible.filter((processor) => !disabled.includes(processor) && !unset.includes(processor));
120
+ const unset = isGated && requireExplicitEnablement ? (enablement === null || enablement === void 0 ? void 0 : enablement.unset) || [] : [];
121
+ // Recorded either way, and only acted on when the split leaves the choice with
122
+ // the customer. An order routed over their objection is the one that will be
123
+ // asked about later, so what they had said needs to survive on the decision.
124
+ const enablementOverridden = isGated && Boolean(rule === null || rule === void 0 ? void 0 : rule.appliesToAllUsers);
125
+ const candidates = enablementOverridden
126
+ ? eligible
127
+ : eligible.filter((processor) => !disabled.includes(processor) && !unset.includes(processor));
128
+ // What was filtered, carried by every decision below. Kept in one place so a
129
+ // branch cannot quietly stop reporting one of them, since these sets are the
130
+ // whole reason a decision can still be explained weeks later.
131
+ const filtered = Object.assign({ corridor, excluded: excludedProcessors, disabled,
132
+ unset }, (enablementOverridden ? { enablementOverridden: true } : {}));
120
133
  if (!candidates.length) {
121
- return {
134
+ return Object.assign(Object.assign({
122
135
  // Told apart because they are different conversations. One customer turned
123
136
  // their rails off and can turn one back on; the other has never told us
124
137
  // which account their bank will let them pay, and needs to whitelist one
125
138
  // before anything can be routed.
126
139
  reason: unset.length
127
140
  ? common_1.RoutingDecisionReason.NoAcceptedCandidate
128
- : common_1.RoutingDecisionReason.NoEnabledCandidate,
129
- corridor,
130
- excluded: excludedProcessors,
131
- disabled,
132
- unset,
133
- decidedAt,
134
- };
141
+ : common_1.RoutingDecisionReason.NoEnabledCandidate }, filtered), { decidedAt });
135
142
  }
136
143
  const shares = candidates
137
144
  .map((processor) => {
@@ -151,42 +158,18 @@ const resolveProcessor = ({ country, fiat, purpose, routingConfig, excludedProce
151
158
  // position gets is predictable rather than rolled.
152
159
  if (!shares.length) {
153
160
  const fallback = (0, common_1.getDefaultProcessor)(country, fiat, purpose, excludedProcessors);
154
- return {
155
- processor: fallback && candidates.includes(fallback) ? fallback : candidates[0],
156
- reason: common_1.RoutingDecisionReason.EnabledRailWithoutShare,
157
- corridor,
158
- excluded: excludedProcessors,
159
- disabled,
160
- unset,
161
- decidedAt,
162
- };
161
+ return Object.assign(Object.assign({ processor: fallback && candidates.includes(fallback) ? fallback : candidates[0], reason: common_1.RoutingDecisionReason.EnabledRailWithoutShare }, filtered), { decidedAt });
163
162
  }
164
163
  if (shares.length === 1) {
165
- return {
166
- processor: shares[0].processor,
167
- reason: common_1.RoutingDecisionReason.OnlyCandidate,
168
- corridor,
169
- excluded: excludedProcessors,
170
- disabled,
171
- unset,
172
- percentages,
173
- decidedAt,
174
- };
164
+ return Object.assign(Object.assign({ processor: shares[0].processor, reason: common_1.RoutingDecisionReason.OnlyCandidate }, filtered), { percentages,
165
+ decidedAt });
175
166
  }
176
167
  // Rolled per order, so the realized share converges on the configured
177
168
  // percentages by order count. Nothing is persisted here: a caller that needs
178
169
  // the same answer twice passes back the seed it used.
179
170
  const seed = randomSeed !== null && randomSeed !== void 0 ? randomSeed : Math.random();
180
- return {
181
- processor: pickByShare(shares, seed),
182
- reason: common_1.RoutingDecisionReason.Split,
183
- corridor,
184
- excluded: excludedProcessors,
185
- disabled,
186
- unset,
187
- percentages,
171
+ return Object.assign(Object.assign({ processor: pickByShare(shares, seed), reason: common_1.RoutingDecisionReason.Split }, filtered), { percentages,
188
172
  seed,
189
- decidedAt,
190
- };
173
+ decidedAt });
191
174
  };
192
175
  exports.resolveProcessor = resolveProcessor;
@@ -56,6 +56,7 @@ interface RioSettingsAttrs {
56
56
  fxTradingPolicies?: FXTradingPolicies;
57
57
  processorRouting?: ProcessorRoutingConfig;
58
58
  requireExplicitPayinEnablement?: boolean;
59
+ requireExplicitPayoutEnablement?: boolean;
59
60
  TVFXDataProvider: {
60
61
  [key in Fiat]: TVFXDataProvider;
61
62
  };
@@ -151,6 +152,7 @@ interface RioSettingsDoc extends mongoose.Document {
151
152
  fxTradingPolicies?: FXTradingPolicies;
152
153
  processorRouting?: ProcessorRoutingConfig;
153
154
  requireExplicitPayinEnablement?: boolean;
155
+ requireExplicitPayoutEnablement?: boolean;
154
156
  TVFXDataProvider: {
155
157
  [key in Fiat]: TVFXDataProvider;
156
158
  };
@@ -62,6 +62,9 @@ const buildRioSettings = (mongoose) => {
62
62
  requireExplicitPayinEnablement: {
63
63
  type: Boolean,
64
64
  },
65
+ requireExplicitPayoutEnablement: {
66
+ type: Boolean,
67
+ },
65
68
  TVFXDataProvider: {
66
69
  type: Object,
67
70
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2896",
3
+ "version": "1.0.2898",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  "@google-cloud/secret-manager": "^5.6.0",
29
29
  "@google-cloud/storage": "^7.19.0",
30
30
  "@hyperdx/node-opentelemetry": "^0.10.3",
31
- "@riocrypto/common": "1.0.2708",
31
+ "@riocrypto/common": "1.0.2711",
32
32
  "@slack/web-api": "^7.15.0",
33
33
  "@types/express": "^4.17.25",
34
34
  "axios": "1.18.1",