@riocrypto/common-server 1.0.2912 → 1.0.2915

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,29 @@
1
+ import { ProcessorEnablementState } from "@riocrypto/common";
2
+ /**
3
+ * Whether silence about a rail should be read as a refusal, for one customer in
4
+ * one corridor and one direction.
5
+ *
6
+ * The setting alone is not enough to answer it. Accounts are seeded and shown for
7
+ * the country a customer onboarded in, while an order or a quote may name any
8
+ * corridor we support, so a corridor where they have accepted nothing is one
9
+ * nobody has ever put in front of them. Reading silence as a refusal there
10
+ * refuses an order over an account the customer has no page to go and turn on,
11
+ * which is an outage of our own making rather than a decision they made.
12
+ *
13
+ * So the gate turns on with the customer's first yes in that corridor, and from
14
+ * then on only the rails they have accepted can carry an order. A no on its own
15
+ * does not turn it on: it is reachable while the direction is opt-out, where a
16
+ * customer can switch off the rail they were seeded with and leave the rest
17
+ * merely unspoken about, and those customers would otherwise be blocked the
18
+ * moment the direction was switched over. Their no is still honoured either way,
19
+ * since a disabled rail is dropped from routing whatever this returns.
20
+ *
21
+ * Lives on its own so that routing, the pages that show a customer which accounts
22
+ * will take their orders, and the guard that stops them switching off their last
23
+ * usable account all answer it the same way. Those disagreeing is what makes a
24
+ * customer see every account switched off while their orders route perfectly well.
25
+ */
26
+ export declare const isEnablementGateActive: ({ enablement, requireExplicitEnablement, }: {
27
+ enablement?: Pick<ProcessorEnablementState, "enabled"> | undefined;
28
+ requireExplicitEnablement?: boolean | undefined;
29
+ }) => boolean;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isEnablementGateActive = void 0;
4
+ /**
5
+ * Whether silence about a rail should be read as a refusal, for one customer in
6
+ * one corridor and one direction.
7
+ *
8
+ * The setting alone is not enough to answer it. Accounts are seeded and shown for
9
+ * the country a customer onboarded in, while an order or a quote may name any
10
+ * corridor we support, so a corridor where they have accepted nothing is one
11
+ * nobody has ever put in front of them. Reading silence as a refusal there
12
+ * refuses an order over an account the customer has no page to go and turn on,
13
+ * which is an outage of our own making rather than a decision they made.
14
+ *
15
+ * So the gate turns on with the customer's first yes in that corridor, and from
16
+ * then on only the rails they have accepted can carry an order. A no on its own
17
+ * does not turn it on: it is reachable while the direction is opt-out, where a
18
+ * customer can switch off the rail they were seeded with and leave the rest
19
+ * merely unspoken about, and those customers would otherwise be blocked the
20
+ * moment the direction was switched over. Their no is still honoured either way,
21
+ * since a disabled rail is dropped from routing whatever this returns.
22
+ *
23
+ * Lives on its own so that routing, the pages that show a customer which accounts
24
+ * will take their orders, and the guard that stops them switching off their last
25
+ * usable account all answer it the same way. Those disagreeing is what makes a
26
+ * customer see every account switched off while their orders route perfectly well.
27
+ */
28
+ const isEnablementGateActive = ({ enablement, requireExplicitEnablement, }) => Boolean(requireExplicitEnablement && (enablement === null || enablement === void 0 ? void 0 : enablement.enabled.length));
29
+ exports.isEnablementGateActive = isEnablementGateActive;
@@ -17,6 +17,7 @@ const fintoc_deposit_CLABE_1 = require("../models/fintoc-deposit-CLABE");
17
17
  const alfin_virtual_cci_1 = require("../models/alfin-virtual-cci");
18
18
  const processor_readiness_1 = require("../models/processor-readiness");
19
19
  const cluster_client_1 = require("../clients/cluster-client");
20
+ const enablement_gate_1 = require("./enablement-gate");
20
21
  const processor_enablement_1 = require("./processor-enablement");
21
22
  // Rails that give each user their own destination, which therefore has to exist
22
23
  // before we can tell a customer where to pay. Every other rail is a shared house
@@ -300,12 +301,24 @@ const getPayinDestinations = ({ mongoose, user, country, fiat, processors, provi
300
301
  .select({ processor: 1, status: 1, enablement: 1 })
301
302
  .lean();
302
303
  const readinessByProcessor = new Map(readiness.map((record) => [record.processor, record]));
304
+ // Read through the same gate routing uses, so a customer who has accepted nothing
305
+ // in this corridor is shown what will really happen - every account able to take
306
+ // an order - rather than every account switched off while their orders route fine.
307
+ const gateActive = (0, enablement_gate_1.isEnablementGateActive)({
308
+ enablement: {
309
+ enabled: processors.filter((processor) => {
310
+ var _a, _b, _c;
311
+ 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) === true;
312
+ }),
313
+ },
314
+ requireExplicitEnablement,
315
+ });
303
316
  const isEnabled = (processor) => {
304
317
  var _a;
305
318
  return (0, processor_enablement_1.isProcessorEnabled)({
306
319
  enablement: (_a = readinessByProcessor.get(processor)) === null || _a === void 0 ? void 0 : _a.enablement,
307
320
  side: common_1.Side.Buy,
308
- requireExplicitEnablement,
321
+ requireExplicitEnablement: gateActive,
309
322
  });
310
323
  };
311
324
  const destinations = [];
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
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
+ const enablement_gate_1 = require("./enablement-gate");
15
16
  const get_processor_1 = require("./get-processor");
16
17
  // Enablement is asked per side, so the purpose is fixed by the direction rather
17
18
  // than chosen by the caller: verifications are ours to place and are never gated.
@@ -95,9 +96,18 @@ const setProcessorEnablement = ({ mongoose, userId, country, fiat, side, process
95
96
  fiat,
96
97
  side,
97
98
  });
99
+ // Counts an unspoken rail as usable on exactly the terms routing does, which
100
+ // includes a customer who has accepted nothing here yet: their unspoken rails
101
+ // can still carry an order, so refusing to let them switch one off would be
102
+ // guarding a last usable account they do not have.
98
103
  const stillUsable = [
99
104
  ...state.enabled,
100
- ...(requireExplicitEnablement ? [] : state.unset),
105
+ ...((0, enablement_gate_1.isEnablementGateActive)({
106
+ enablement: state,
107
+ requireExplicitEnablement,
108
+ })
109
+ ? []
110
+ : state.unset),
101
111
  ].filter((candidate) => candidate !== processor);
102
112
  if (!stillUsable.length) {
103
113
  throw new common_1.GenericInputError(side === common_1.Side.Buy
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveProcessor = void 0;
4
4
  const common_1 = require("@riocrypto/common");
5
+ const enablement_gate_1 = require("./enablement-gate");
5
6
  const getRule = (config, country, fiat, purpose) => { var _a, _b; return (_b = (_a = config === null || config === void 0 ? void 0 : config[country]) === null || _a === void 0 ? void 0 : _a[fiat]) === null || _b === void 0 ? void 0 : _b[purpose]; };
6
7
  // Renormalizes over the candidates it is given rather than assuming they still
7
8
  // add up to 100, which is what lets a rail drop out - excluded by environment, or
@@ -29,9 +30,9 @@ const pickByShare = (candidates, position) => {
29
30
  // corridor rather than from the rule. What keeps an order away from a rail is the
30
31
  // customer, by switching it off or, while that direction is opt-in, by never
31
32
  // turning it on. Two exceptions: a split set to apply to all users divides the
32
- // corridor by percentage alone and asks nobody, and a corridor the customer has
33
- // no answer on record for at all is one they were never asked about, so silence
34
- // there is not read as a refusal.
33
+ // corridor by percentage alone and asks nobody, and a corridor where the customer
34
+ // has accepted nothing yet is one they were never really asked about, so silence
35
+ // there is not read as a refusal either.
35
36
  //
36
37
  // Does no IO of its own - the routing config and the user's enablement are passed
37
38
  // in - and the only nondeterminism is the per-order roll, which a caller can
@@ -120,21 +121,21 @@ const resolveProcessor = ({ country, fiat, purpose, routingConfig, excludedProce
120
121
  const isGated = purpose !== common_1.ProcessorRoutingPurpose.BankAccountVerification;
121
122
  const disabled = isGated ? (enablement === null || enablement === void 0 ? void 0 : enablement.disabled) || [] : [];
122
123
  const unset = isGated && requireExplicitEnablement ? (enablement === null || enablement === void 0 ? void 0 : enablement.unset) || [] : [];
123
- // Silence only means no in a corridor the customer has actually been asked
124
- // about. Accounts are seeded and shown for the country they onboarded in, while
125
- // a quote may name any corridor, so a corridor with nothing at all on record is
126
- // one nobody has ever offered them rather than one they turned down. Refusing
127
- // there would refuse a price over an account the customer has no page to go and
128
- // turn on, so the gate stands down and the corridor routes as it did before
129
- // opt-in. Anything they have answered, in either direction, puts the gate back.
130
- const wasAsked = Boolean(enablement && (enablement.enabled.length || enablement.disabled.length));
131
124
  // Recorded either way, and only acted on when the split leaves the choice with
132
125
  // the customer. An order routed over their objection is the one that will be
133
126
  // asked about later, so what they had said needs to survive on the decision.
134
127
  const enablementOverridden = isGated && Boolean(rule === null || rule === void 0 ? void 0 : rule.appliesToAllUsers);
135
- // Both sets are still reported below whether or not they were acted on, so a
136
- // decision that routed past unspoken rails can be told from one that had none.
137
- const gatedUnset = wasAsked ? unset : [];
128
+ // Silence is only a refusal once the customer has told us what does work in this
129
+ // corridor - see isEnablementGateActive. Until then the rails they have not
130
+ // refused are all still in play, as they were before the direction went opt-in.
131
+ // The unspoken set is reported below either way, so a decision that routed past
132
+ // one can still be told from a decision that had none to route past.
133
+ const gatedUnset = (0, enablement_gate_1.isEnablementGateActive)({
134
+ enablement,
135
+ requireExplicitEnablement,
136
+ })
137
+ ? unset
138
+ : [];
138
139
  const candidates = enablementOverridden
139
140
  ? eligible
140
141
  : eligible.filter((processor) => !disabled.includes(processor) && !gatedUnset.includes(processor));
package/build/index.d.ts CHANGED
@@ -187,6 +187,7 @@ export * from "./helpers/resolve-processor";
187
187
  export * from "./helpers/get-custom-rio-bank-account";
188
188
  export * from "./helpers/payin-destinations";
189
189
  export * from "./helpers/payin-verification";
190
+ export * from "./helpers/enablement-gate";
190
191
  export * from "./helpers/processor-enablement";
191
192
  export * from "./helpers/record-observed-processor-readiness";
192
193
  export * from "./helpers/get-bulk-payment-from-reference";
package/build/index.js CHANGED
@@ -203,6 +203,7 @@ __exportStar(require("./helpers/resolve-processor"), exports);
203
203
  __exportStar(require("./helpers/get-custom-rio-bank-account"), exports);
204
204
  __exportStar(require("./helpers/payin-destinations"), exports);
205
205
  __exportStar(require("./helpers/payin-verification"), exports);
206
+ __exportStar(require("./helpers/enablement-gate"), exports);
206
207
  __exportStar(require("./helpers/processor-enablement"), exports);
207
208
  __exportStar(require("./helpers/record-observed-processor-readiness"), exports);
208
209
  __exportStar(require("./helpers/get-bulk-payment-from-reference"), exports);
@@ -84,6 +84,14 @@ const buildBankAccount = (mongoose) => {
84
84
  },
85
85
  },
86
86
  });
87
+ // Duplicate check on create. The CLABE suffix also covers the CCI and
88
+ // accountNumber variants, which fall back to the userId/country/fiat prefix.
89
+ BankAccountSchema.index({ userId: 1, country: 1, fiat: 1, CLABE: 1 });
90
+ // Listing a single user's accounts, newest first.
91
+ BankAccountSchema.index({ userId: 1, createdAt: -1, _id: -1 });
92
+ // Admin and cluster listing, which filters on nothing and sorts the whole
93
+ // collection before paginating.
94
+ BankAccountSchema.index({ createdAt: -1, _id: -1 });
87
95
  BankAccountSchema.statics.build = (attrs) => {
88
96
  return new BankAccount(attrs);
89
97
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2912",
3
+ "version": "1.0.2915",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",