-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverificationService.js
More file actions
273 lines (233 loc) · 8.67 KB
/
verificationService.js
File metadata and controls
273 lines (233 loc) · 8.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import {
getLoanApplicationRow,
getLoanApplicationByApplicationId,
updateLoanApplicationById,
updateLoanFinalStatus,
creditBankAccountBalance
} from "./loanService.js";
const VALID_REGIONS = ["APAC", "EMEA", "AMERICAS", "MEA", "NA", "SA", "EU", "ASIA"];
const nowIso = () => new Date().toISOString();
class VerificationService {
static async performKyc(application) {
const remarks = [];
let approved = true;
if (!application.email.includes("@") || !application.email.includes(".")) {
remarks.push("Invalid email format");
approved = false;
} else {
remarks.push("Email format valid");
}
if (application.phone && application.phone.replace(/\D/g, "").length < 10) {
remarks.push("Phone number too short");
approved = false;
} else {
remarks.push("Phone number valid");
}
if (!application.documents_uploaded) {
remarks.push("Required documents not uploaded");
approved = false;
} else {
remarks.push("Documents uploaded and verified");
}
if (!application.name || application.name.trim().length < 3) {
remarks.push("Invalid name");
approved = false;
} else {
remarks.push("Name verified");
}
if (!VALID_REGIONS.includes((application.region || "").toUpperCase())) {
remarks.push(`Invalid region: ${application.region}`);
approved = false;
} else {
remarks.push(`Region verified: ${application.region}`);
}
await updateLoanFinalStatus(application.id, approved ? "APPROVED" : "REJECTED");
await updateLoanApplicationById(application.id, {
kyc_status: approved ? "APPROVED" : "REJECTED",
kyc_verified_at: nowIso(),
kyc_remarks: remarks.join("; ")
});
return approved;
}
static async performComplianceCheck(application) {
const remarks = [];
let approved = true;
const politicalConnectionDetected = Math.random() < 0.1;
const seniorRelativeIndicators = ["jr", "sr", "ceo", "cfo", "director"];
const seniorRelativeDetected = seniorRelativeIndicators.some((indicator) =>
(application.name || "").toLowerCase().includes(indicator)
);
const sanctionedDomains = ["sanctioned.com", "blocked.net", "restricted.org"];
const emailDomain = application.email?.split("@")[1] || "";
const highRiskCountries = ["Country-X", "Country-Y"];
if (politicalConnectionDetected) {
remarks.push("Political connection detected - requires manual review");
approved = false;
} else {
remarks.push("No political connections found");
}
if (seniorRelativeDetected) {
remarks.push("Potential senior employee relative - requires verification");
approved = false;
} else {
remarks.push("No senior employee relation detected");
}
if (sanctionedDomains.includes(emailDomain)) {
remarks.push(`Email domain on sanctions list: ${emailDomain}`);
approved = false;
} else {
remarks.push("Email domain cleared");
}
if (highRiskCountries.includes(application.country)) {
remarks.push(`High-risk country: ${application.country}`);
approved = false;
} else {
remarks.push("Country risk assessment: CLEAR");
}
if (application.loan_amount > 500000) {
remarks.push(
`High-value transaction ($${Number(application.loan_amount).toLocaleString()}) - enhanced due diligence required`
);
}
await updateLoanApplicationById(application.id, {
compliance_status: approved ? "APPROVED" : "REJECTED",
compliance_verified_at: nowIso(),
compliance_remarks: remarks.join("; "),
political_connection: politicalConnectionDetected,
senior_relative: seniorRelativeDetected
});
return approved;
}
static async performEligibilityCheck(application) {
const remarks = [];
let approved = true;
if (application.income > 0) {
const dtiRatio = Number(application.debt) / Number(application.income);
if (dtiRatio >= 0.4) {
remarks.push(`High DTI ratio: ${(dtiRatio * 100).toFixed(1)}% (threshold: 40%)`);
approved = false;
} else {
remarks.push(`DTI ratio acceptable: ${(dtiRatio * 100).toFixed(1)}%`);
}
await updateLoanApplicationById(application.id, { dti_ratio: dtiRatio });
} else {
remarks.push("Invalid income value");
approved = false;
}
if (application.credit_score < 650) {
remarks.push(
`Credit score below minimum: ${application.credit_score} (minimum: 650)`
);
approved = false;
} else if (application.credit_score < 700) {
remarks.push(`Credit score marginal: ${application.credit_score}`);
} else {
remarks.push(`Credit score good: ${application.credit_score}`);
}
if (Number(application.income) * 3 < Number(application.loan_amount)) {
remarks.push(
`Insufficient income for loan amount (Income: $${Number(application.income).toLocaleString()}, Loan: $${Number(
application.loan_amount
).toLocaleString()})`
);
approved = false;
} else {
remarks.push("Income sufficient for requested loan amount");
}
if (application.loan_amount > 1_000_000) {
remarks.push(
`Loan amount exceeds maximum: $${Number(application.loan_amount).toLocaleString()} (max: $1,000,000)`
);
approved = false;
}
if (application.income < 30000) {
remarks.push(
`Income below minimum requirement: $${Number(application.income).toLocaleString()} (min: $30,000)`
);
approved = false;
}
await updateLoanApplicationById(application.id, {
eligibility_status: approved ? "APPROVED" : "REJECTED",
eligibility_verified_at: nowIso(),
eligibility_remarks: remarks.join("; ")
});
return approved;
}
static async finalizeApplication(application) {
const approved =
application.kyc_status === "APPROVED" &&
application.compliance_status === "APPROVED" &&
application.eligibility_status === "APPROVED";
const failedChecks = [];
if (application.kyc_status !== "APPROVED") failedChecks.push("KYC");
if (application.compliance_status !== "APPROVED") failedChecks.push("Compliance");
if (application.eligibility_status !== "APPROVED") failedChecks.push("Eligibility");
await updateLoanFinalStatus(
application.id,
approved ? "APPROVED" : "REJECTED"
);
await updateLoanApplicationById(application.id, {
final_decision_at: nowIso(),
final_remarks: approved
? "All verification checks passed. Loan application approved."
: `Loan application rejected. Failed checks: ${failedChecks.join(", ")}`
});
if (
approved &&
application.bank_account_id &&
Number(application.loan_amount) > 0
) {
await creditBankAccountBalance(
application.bank_account_id,
Number(application.loan_amount)
);
}
return approved;
}
static async sendNotification(application) {
console.log(
`[NOTIFY] Application ${application.application_id} ${application.final_status} for ${application.name}`
);
await updateLoanApplicationById(application.id, {
alert_sent: true,
email_sent: true
});
}
static async markFailure(application, message) {
await updateLoanApplicationById(application.id, {
final_status: "REJECTED",
final_remarks: message,
final_decision_at: nowIso(),
review_status: "REJECTED"
});
const latest = await getLoanApplicationByApplicationId(application.application_id);
await this.sendNotification(latest);
}
static async processApplication(applicationId) {
let application = await getLoanApplicationRow(applicationId);
if (!application) {
console.warn(`[Workflow] Application ${applicationId} not found`);
return false;
}
if (!(await this.performKyc(application))) {
await this.markFailure(application, "Application rejected at KYC stage");
return false;
}
application = await getLoanApplicationRow(applicationId);
if (!(await this.performComplianceCheck(application))) {
await this.markFailure(application, "Application rejected at compliance stage");
return false;
}
application = await getLoanApplicationRow(applicationId);
if (!(await this.performEligibilityCheck(application))) {
await this.markFailure(application, "Application rejected at eligibility stage");
return false;
}
application = await getLoanApplicationRow(applicationId);
const approved = await this.finalizeApplication(application);
application = await getLoanApplicationByApplicationId(applicationId);
await this.sendNotification(application);
return approved;
}
}
export default VerificationService;