@riocrypto/common-server 1.0.2880 → 1.0.2881

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.
@@ -13,6 +13,17 @@ export interface PayinBankDetails {
13
13
  NIT?: string;
14
14
  }
15
15
  export declare const PER_USER_PAYIN_PROCESSORS: Processor[];
16
+ export declare const normalizeCci: (cci: string) => string;
17
+ /**
18
+ * Whether a CCI is one of Rio's own Peru accounts.
19
+ *
20
+ * Rio funds Alfin from its Interbank accounts, so those transfers appear on the
21
+ * Alfin statement as ordinary interbank credits carrying Rio's own account as
22
+ * the payer. Anything reading the statement to attribute customer payments has
23
+ * to exclude them, or a treasury top-up would be credited to whichever customer
24
+ * order happened to be awaiting the same amount.
25
+ */
26
+ export declare const isRioOwnPeruAccountCci: (cci?: string) => boolean;
16
27
  export declare const getPayinBankDetails: ({ mongoose, processor, fiat, country, user, }: {
17
28
  mongoose: Mongoose;
18
29
  processor: Processor;
@@ -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.getPayinDestinations = exports.getPayinBankDetails = exports.PER_USER_PAYIN_PROCESSORS = void 0;
12
+ exports.getPayinDestinations = exports.getPayinBankDetails = exports.isRioOwnPeruAccountCci = exports.normalizeCci = exports.PER_USER_PAYIN_PROCESSORS = void 0;
13
13
  const common_1 = require("@riocrypto/common");
14
14
  const rio_bank_account_1 = require("../models/rio-bank-account");
15
15
  const STP_deposit_CLABE_1 = require("../models/STP-deposit-CLABE");
@@ -40,6 +40,34 @@ const ALFIN_ACCOUNT_NUMBER_BY_FIAT = {
40
40
  [common_1.Fiat.PEN]: "01818233900001",
41
41
  [common_1.Fiat.USD]: "01818233900002",
42
42
  };
43
+ const INTERBANK_PERU_CCI_BY_FIAT = {
44
+ [common_1.Fiat.PEN]: "003-200-003004741070-36",
45
+ [common_1.Fiat.USD]: "003-200-003004741088-38",
46
+ };
47
+ // Digits-only form of a Peruvian CCI. Banks present them either grouped with
48
+ // dashes or bare, and Alfin reports the bare form inside a statement line's
49
+ // referencia, so both have to reduce to the same string to be comparable.
50
+ const normalizeCci = (cci) => cci.replace(/\D/g, "");
51
+ exports.normalizeCci = normalizeCci;
52
+ const RIO_PERU_OWN_CCIS = Object.values(INTERBANK_PERU_CCI_BY_FIAT)
53
+ .filter((cci) => Boolean(cci))
54
+ .map(exports.normalizeCci);
55
+ /**
56
+ * Whether a CCI is one of Rio's own Peru accounts.
57
+ *
58
+ * Rio funds Alfin from its Interbank accounts, so those transfers appear on the
59
+ * Alfin statement as ordinary interbank credits carrying Rio's own account as
60
+ * the payer. Anything reading the statement to attribute customer payments has
61
+ * to exclude them, or a treasury top-up would be credited to whichever customer
62
+ * order happened to be awaiting the same amount.
63
+ */
64
+ const isRioOwnPeruAccountCci = (cci) => {
65
+ if (!cci) {
66
+ return false;
67
+ }
68
+ return RIO_PERU_OWN_CCIS.includes((0, exports.normalizeCci)(cci));
69
+ };
70
+ exports.isRioOwnPeruAccountCci = isRioOwnPeruAccountCci;
43
71
  // Shared house accounts. Kept here rather than in the order-payment path so the
44
72
  // customer-facing whitelisting list and the payment instructions on an order can
45
73
  // never drift apart.
@@ -78,7 +106,7 @@ const getStaticPayinBankDetails = (processor, fiat) => {
78
106
  companyName: PERU_COMPANY_NAME,
79
107
  companyAddress: PERU_COMPANY_ADDRESS,
80
108
  accountNumber: "200-3004741070",
81
- CCI: "003-200-003004741070-36",
109
+ CCI: INTERBANK_PERU_CCI_BY_FIAT[common_1.Fiat.PEN],
82
110
  RUC: PERU_RUC,
83
111
  };
84
112
  }
@@ -88,7 +116,7 @@ const getStaticPayinBankDetails = (processor, fiat) => {
88
116
  companyName: PERU_COMPANY_NAME,
89
117
  companyAddress: PERU_COMPANY_ADDRESS,
90
118
  accountNumber: "200-3004741088",
91
- CCI: "003-200-003004741088-38",
119
+ CCI: INTERBANK_PERU_CCI_BY_FIAT[common_1.Fiat.USD],
92
120
  RUC: PERU_RUC,
93
121
  };
94
122
  }
package/build/index.d.ts CHANGED
@@ -95,6 +95,8 @@ export * from "./models/alfin-virtual-cci";
95
95
  export * from "./models/alfin-settings";
96
96
  export * from "./models/alfin-withdrawal";
97
97
  export * from "./models/alfin-payin-notification";
98
+ export * from "./models/alfin-payin-origin";
99
+ export * from "./models/alfin-statement-credit";
98
100
  export * from "./models/alfin-webhook-token";
99
101
  export * from "./models/dashboard-push-notification-subscription";
100
102
  export * from "./models/webauthn-credential";
package/build/index.js CHANGED
@@ -111,6 +111,8 @@ __exportStar(require("./models/alfin-virtual-cci"), exports);
111
111
  __exportStar(require("./models/alfin-settings"), exports);
112
112
  __exportStar(require("./models/alfin-withdrawal"), exports);
113
113
  __exportStar(require("./models/alfin-payin-notification"), exports);
114
+ __exportStar(require("./models/alfin-payin-origin"), exports);
115
+ __exportStar(require("./models/alfin-statement-credit"), exports);
114
116
  __exportStar(require("./models/alfin-webhook-token"), exports);
115
117
  __exportStar(require("./models/dashboard-push-notification-subscription"), exports);
116
118
  __exportStar(require("./models/webauthn-credential"), exports);
@@ -19,6 +19,8 @@ interface AlfinPayinNotificationDoc extends Document {
19
19
  amount: number;
20
20
  processedAt: Date;
21
21
  payload: Record<string, any>;
22
+ processingCompletedAt?: Date | null;
23
+ reconciledStatementCreditKey?: string;
22
24
  }
23
25
  interface AlfinPayinNotificationModel extends Model<AlfinPayinNotificationDoc> {
24
26
  build(attrs: AlfinPayinNotificationAttrs): HydratedDocument<AlfinPayinNotificationDoc>;
@@ -37,6 +37,17 @@ const buildAlfinPayinNotification = (mongoose) => {
37
37
  payload: {
38
38
  type: mongoose.Schema.Types.Mixed,
39
39
  },
40
+ // Defaulted so every new notification carries the field explicitly, which
41
+ // is what separates one that has not finished from one written before this
42
+ // existed. A query that cannot tell those apart either credits a payin
43
+ // twice or not at all.
44
+ processingCompletedAt: {
45
+ type: mongoose.Schema.Types.Date,
46
+ default: null,
47
+ },
48
+ reconciledStatementCreditKey: {
49
+ type: String,
50
+ },
40
51
  }, {
41
52
  toJSON: {
42
53
  transform(doc, ret) {
@@ -46,6 +57,12 @@ const buildAlfinPayinNotification = (mongoose) => {
46
57
  },
47
58
  },
48
59
  });
60
+ // Supports the claim lookup, which searches unclaimed notifications by
61
+ // currency and amount within a few days of a statement line.
62
+ AlfinPayinNotificationSchema.index({ fiat: 1, amount: 1, processedAt: 1 });
63
+ // One statement line can only ever be the same money as one notification.
64
+ // Sparse so the unclaimed majority are not indexed as duplicate nulls.
65
+ AlfinPayinNotificationSchema.index({ reconciledStatementCreditKey: 1 }, { unique: true, sparse: true });
49
66
  AlfinPayinNotificationSchema.statics.build = (attrs) => {
50
67
  return new AlfinPayinNotification(attrs);
51
68
  };
@@ -0,0 +1,39 @@
1
+ import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
2
+ type AlfinPayinOriginSource = "payinWebhook" | "payinVerification";
3
+ interface AlfinPayinOriginAttrs {
4
+ CCI: string;
5
+ userId: string;
6
+ source: AlfinPayinOriginSource;
7
+ accountHolderName?: string;
8
+ firstSeenAt: Date;
9
+ lastSeenAt: Date;
10
+ }
11
+ interface AlfinPayinOriginDoc extends Document {
12
+ CCI: string;
13
+ userId: string;
14
+ source: AlfinPayinOriginSource;
15
+ accountHolderName?: string;
16
+ firstSeenAt: Date;
17
+ lastSeenAt: Date;
18
+ }
19
+ interface AlfinPayinOriginModel extends Model<AlfinPayinOriginDoc> {
20
+ build(attrs: AlfinPayinOriginAttrs): HydratedDocument<AlfinPayinOriginDoc>;
21
+ }
22
+ /**
23
+ * Accounts customers have paid us from at Alfin, learned as they pay.
24
+ *
25
+ * A payment that lands on a user's virtual CCI identifies its owner outright, so
26
+ * nothing here is needed to process it. It exists for payments that cannot use a
27
+ * virtual CCI at all - anything over the CCE immediate-transfer ceiling, which
28
+ * the CCIV rejects - because those land on the principal account, raise no
29
+ * notification, and are only visible as an account-statement credit. The
30
+ * statement carries the payer's CCI, so a customer's first small payment through
31
+ * their CCIV is what teaches us where their large ones will come from.
32
+ *
33
+ * One document per account/user pair rather than one per account: a shared
34
+ * corporate account can legitimately belong to more than one user, and a
35
+ * statement credit from an account claimed by two users cannot be attributed to
36
+ * either, which the matcher detects by finding more than one document.
37
+ */
38
+ declare const buildAlfinPayinOrigin: (mongoose: Mongoose) => AlfinPayinOriginModel;
39
+ export { buildAlfinPayinOrigin, AlfinPayinOriginDoc, AlfinPayinOriginAttrs, AlfinPayinOriginSource, };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAlfinPayinOrigin = void 0;
4
+ /**
5
+ * Accounts customers have paid us from at Alfin, learned as they pay.
6
+ *
7
+ * A payment that lands on a user's virtual CCI identifies its owner outright, so
8
+ * nothing here is needed to process it. It exists for payments that cannot use a
9
+ * virtual CCI at all - anything over the CCE immediate-transfer ceiling, which
10
+ * the CCIV rejects - because those land on the principal account, raise no
11
+ * notification, and are only visible as an account-statement credit. The
12
+ * statement carries the payer's CCI, so a customer's first small payment through
13
+ * their CCIV is what teaches us where their large ones will come from.
14
+ *
15
+ * One document per account/user pair rather than one per account: a shared
16
+ * corporate account can legitimately belong to more than one user, and a
17
+ * statement credit from an account claimed by two users cannot be attributed to
18
+ * either, which the matcher detects by finding more than one document.
19
+ */
20
+ const buildAlfinPayinOrigin = (mongoose) => {
21
+ if (mongoose.models.AlfinPayinOrigin) {
22
+ return mongoose.model("AlfinPayinOrigin");
23
+ }
24
+ const AlfinPayinOriginSchema = new mongoose.Schema({
25
+ CCI: {
26
+ type: String,
27
+ required: true,
28
+ },
29
+ userId: {
30
+ type: String,
31
+ required: true,
32
+ index: true,
33
+ },
34
+ source: {
35
+ type: String,
36
+ required: true,
37
+ },
38
+ accountHolderName: {
39
+ type: String,
40
+ },
41
+ firstSeenAt: {
42
+ type: mongoose.Schema.Types.Date,
43
+ required: true,
44
+ },
45
+ lastSeenAt: {
46
+ type: mongoose.Schema.Types.Date,
47
+ required: true,
48
+ },
49
+ }, {
50
+ toJSON: {
51
+ transform(doc, ret) {
52
+ ret.id = ret._id.valueOf();
53
+ delete ret._id;
54
+ delete ret.__v;
55
+ },
56
+ },
57
+ });
58
+ AlfinPayinOriginSchema.index({ CCI: 1, userId: 1 }, { unique: true });
59
+ AlfinPayinOriginSchema.statics.build = (attrs) => {
60
+ return new AlfinPayinOrigin(attrs);
61
+ };
62
+ const AlfinPayinOrigin = mongoose.model("AlfinPayinOrigin", AlfinPayinOriginSchema);
63
+ return AlfinPayinOrigin;
64
+ };
65
+ exports.buildAlfinPayinOrigin = buildAlfinPayinOrigin;
@@ -0,0 +1,55 @@
1
+ import { Fiat } from "@riocrypto/common";
2
+ import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
3
+ type AlfinStatementCreditStatus = "matched" | "attributedToWebhook" | "ignored" | "awaitingApproval" | "unmatched";
4
+ interface AlfinStatementCreditAttrs {
5
+ uniqueCompositeKey: string;
6
+ movimientoUId: string;
7
+ ordinal?: number;
8
+ fiat: Fiat;
9
+ amount: number;
10
+ postedAt: Date;
11
+ concepto?: string;
12
+ referencia?: string;
13
+ originCCI?: string;
14
+ originName?: string;
15
+ status: AlfinStatementCreditStatus;
16
+ reason?: string;
17
+ userId?: string;
18
+ orderId?: string;
19
+ bulkBankPaymentId?: string;
20
+ evaluatedAt: Date;
21
+ }
22
+ interface AlfinStatementCreditDoc extends Document {
23
+ uniqueCompositeKey: string;
24
+ movimientoUId: string;
25
+ ordinal?: number;
26
+ fiat: Fiat;
27
+ amount: number;
28
+ postedAt: Date;
29
+ concepto?: string;
30
+ referencia?: string;
31
+ originCCI?: string;
32
+ originName?: string;
33
+ status: AlfinStatementCreditStatus;
34
+ reason?: string;
35
+ userId?: string;
36
+ orderId?: string;
37
+ bulkBankPaymentId?: string;
38
+ evaluatedAt: Date;
39
+ }
40
+ interface AlfinStatementCreditModel extends Model<AlfinStatementCreditDoc> {
41
+ build(attrs: AlfinStatementCreditAttrs): HydratedDocument<AlfinStatementCreditDoc>;
42
+ }
43
+ /**
44
+ * One record per account-statement credit we have evaluated for payment
45
+ * matching, and the decision we reached.
46
+ *
47
+ * The statement is re-read on every poll over a 30-day window, so this is what
48
+ * stops a credit from being attributed twice: the key is inserted before any
49
+ * matching happens, and a duplicate-key error means another run already owns
50
+ * this line. It doubles as the audit trail for why a credit was or was not
51
+ * attributed, which is the only record of that reasoning - the credit itself
52
+ * arrives with no notification behind it.
53
+ */
54
+ declare const buildAlfinStatementCredit: (mongoose: Mongoose) => AlfinStatementCreditModel;
55
+ export { buildAlfinStatementCredit, AlfinStatementCreditDoc, AlfinStatementCreditAttrs, AlfinStatementCreditStatus, };
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAlfinStatementCredit = void 0;
4
+ /**
5
+ * One record per account-statement credit we have evaluated for payment
6
+ * matching, and the decision we reached.
7
+ *
8
+ * The statement is re-read on every poll over a 30-day window, so this is what
9
+ * stops a credit from being attributed twice: the key is inserted before any
10
+ * matching happens, and a duplicate-key error means another run already owns
11
+ * this line. It doubles as the audit trail for why a credit was or was not
12
+ * attributed, which is the only record of that reasoning - the credit itself
13
+ * arrives with no notification behind it.
14
+ */
15
+ const buildAlfinStatementCredit = (mongoose) => {
16
+ if (mongoose.models.AlfinStatementCredit) {
17
+ return mongoose.model("AlfinStatementCredit");
18
+ }
19
+ const AlfinStatementCreditSchema = new mongoose.Schema({
20
+ uniqueCompositeKey: {
21
+ type: String,
22
+ required: true,
23
+ unique: true,
24
+ },
25
+ movimientoUId: {
26
+ type: String,
27
+ required: true,
28
+ },
29
+ ordinal: {
30
+ type: Number,
31
+ },
32
+ fiat: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ amount: {
37
+ type: Number,
38
+ required: true,
39
+ },
40
+ postedAt: {
41
+ type: mongoose.Schema.Types.Date,
42
+ required: true,
43
+ },
44
+ concepto: {
45
+ type: String,
46
+ },
47
+ referencia: {
48
+ type: String,
49
+ },
50
+ originCCI: {
51
+ type: String,
52
+ },
53
+ originName: {
54
+ type: String,
55
+ },
56
+ status: {
57
+ type: String,
58
+ required: true,
59
+ },
60
+ reason: {
61
+ type: String,
62
+ },
63
+ userId: {
64
+ type: String,
65
+ },
66
+ orderId: {
67
+ type: String,
68
+ },
69
+ bulkBankPaymentId: {
70
+ type: String,
71
+ },
72
+ evaluatedAt: {
73
+ type: mongoose.Schema.Types.Date,
74
+ required: true,
75
+ },
76
+ }, {
77
+ toJSON: {
78
+ transform(doc, ret) {
79
+ ret.id = ret._id.valueOf();
80
+ delete ret._id;
81
+ delete ret.__v;
82
+ },
83
+ },
84
+ });
85
+ AlfinStatementCreditSchema.statics.build = (attrs) => {
86
+ return new AlfinStatementCredit(attrs);
87
+ };
88
+ const AlfinStatementCredit = mongoose.model("AlfinStatementCredit", AlfinStatementCreditSchema);
89
+ return AlfinStatementCredit;
90
+ };
91
+ exports.buildAlfinStatementCredit = buildAlfinStatementCredit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2880",
3
+ "version": "1.0.2881",
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.2699",
31
+ "@riocrypto/common": "1.0.2702",
32
32
  "@slack/web-api": "^7.15.0",
33
33
  "@types/express": "^4.17.25",
34
34
  "axios": "1.18.1",