Skip to content

Instantly share code, notes, and snippets.

@heroic
Created December 7, 2022 12:15
Show Gist options
  • Save heroic/53e92859b490bf9ce20133be536bd64d to your computer and use it in GitHub Desktop.
Save heroic/53e92859b490bf9ce20133be536bd64d to your computer and use it in GitHub Desktop.
const {
request,
rabbitmq: { publish },
sequelize,
GcError,
} = require('@galaxycard/utils');
const { Sequelize } = sequelize;
const Op = Sequelize.Op;
const {
hasUserEnoughContacts,
approvedReferenceContacts,
} = require('./contact');
const PERSONAL_DATA_FIELDS = [
'income_type',
'company_name',
'income_range',
'marital_status',
'kids',
'education',
'work_experience',
'residence',
'fathers_name',
'company_type',
];
// waitlisted is a status that for automatic approval, is same as processed
const PROCESSED_APPLICATION_STATUSES = [
'approved',
'rejected',
'archived',
'amit_user',
'usha_user',
'banned',
'apollo_user',
];
const Application = sequelize.define(
'applications',
{
user_id: {
type: Sequelize.BIGINT,
},
status: {
type: Sequelize.STRING,
},
from_where: {
type: Sequelize.STRING,
},
active: {
type: Sequelize.BOOLEAN,
},
income_type: {
type: Sequelize.STRING,
},
income_range: {
type: Sequelize.BIGINT,
},
marital_status: {
type: Sequelize.STRING,
},
kids: {
type: Sequelize.STRING,
},
experiment_id: {
type: Sequelize.BIGINT,
},
picture_id: {
type: Sequelize.STRING,
},
residence: {
type: Sequelize.STRING,
},
education: {
type: Sequelize.STRING,
},
company_type: {
type: Sequelize.STRING,
},
company_name: {
type: Sequelize.STRING,
},
work_experience: {
type: Sequelize.INTEGER,
},
fathers_name: {
type: Sequelize.STRING,
},
fee_paid: {
type: Sequelize.INTEGER,
},
product: {
type: Sequelize.STRING,
},
dob: {
type: Sequelize.DATE,
},
rejected_at: {
type: Sequelize.DATE,
},
created_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.fn('NOW'),
allowNull: false,
},
admin_user: {
type: Sequelize.VIRTUAL,
},
waitlisted: {
type: Sequelize.BIGINT,
},
},
{
timestamps: false,
underscored: true,
scopes: {
processed: {
where: {
status: {
[Op.in]: PROCESSED_APPLICATION_STATUSES,
},
},
},
unprocessed: {
where: {
status: {
[Op.notIn]: PROCESSED_APPLICATION_STATUSES,
},
},
},
},
//if changes made in status then add new to application_status_logs with app id, status, admin user
hooks: {
afterSave: async function (application) {
if (application.changed('status')) {
await sequelize.models.application_status_logs.create({
application_id: application.id,
status: application.status,
admin_user: application.admin_user,
});
}
},
//if status was changed to rejected add time to rejected_at field
beforeSave: async function (application) {
if (
application.changed('status') &&
application.status === 'rejected'
) {
application.rejected_at = Sequelize.fn('NOW');
}
},
afterCreate: async function (application) {
await application.updateToNextStatus();
},
afterUpdate: async function (application, options) {
let args = {
application_id: application.id,
user_id: application.user_id,
};
if (application.changed('product')) {
args.product = application.product;
}
if (options.transaction) {
return options.transaction.afterCommit(() => {
return publish(
{
name: 'application_updated',
args,
},
`documentation.application.${application.status}`,
{
priority: 10,
},
);
});
} else {
return publish(
{
name: 'application_updated',
args,
},
`documentation.application.${application.status}`,
{
priority: 10,
},
);
}
},
},
},
);
Application.CIC_FEE = 5000000;
Application.PERSONAL_DATA_FIELDS = PERSONAL_DATA_FIELDS;
Application.prototype.getProcessingFeeAndLimit = function () {
const limit = 1000000;
return {
amount: this.getLimitExtensionFee(
limit + (this.id % 2 === 0 ? 100000 : 250000),
),
limit,
};
};
Application.prototype.getLimitExtensionFee = function (amount) {
return Math.ceil((amount * 1.0236) / 10000) * 10000;
};
Application.prototype.findMissingPersonalDataFields = function (fields) {
const personal_data = {};
for (let i = 0, len = fields.length; i < len; i++) {
if (this[fields[i]] === null) {
personal_data[fields[i]] = this[fields[i]];
}
}
return personal_data;
};
Application.prototype.evaluateApplicationCreditEligibility = async function (
pan,
) {
let creditReport = await sequelize.models.credit_scores.findOne({
where: {
application_id: this.id,
},
});
if (!creditReport) {
await this.update({
status: 'credit-score',
});
//if we dont find credit score from table we need to create new using id
return sequelize.models.credit_scores.create({
application_id: this.id,
});
}
if (creditReport.status === 'pending') {
// IF THE STATUS OF CREDIT REPORT IS PENDING WE FETCH THE ID AND NAMES OF USER
await creditReport.fetchReport(this.user_id, {
firstName: pan.extra_data.first_name,
surName: pan.extra_data.last_name,
});
} else if (creditReport.status === 'completed' && creditReport.score_seen) {
if (creditReport.score !== null) {
// check if the pull failed due to no match
const result = await request({
uri: 'http://wanda/predict',
method: 'POST',
body: {
application_id: this.id,
user_id: this.user_id,
experiment: this.experiment_id,
content: creditReport.extra_data,
},
});
if (result.statusCode === 200) {
if (result.body.prediction === 'rejected') {
await this.update({
status: 'cic',
product: 'cic',
});
} else {
await this.update({
product: 'automated',
});
}
} else {
console.error(result.body, this.experiment_id);
throw new GcError('Error, http://wanda/predict api returned non 200');
}
} else {
await this.update({
status: 'cic',
product: 'cic',
});
}
} else if (creditReport.status === 'rejected') {
await this.update({
status: 'cic',
product: 'cic',
});
} else {
await this.update({
status: 'credit-score',
});
}
};
Application.prototype.getApplicationCreditLimit = async function () {
let limit_values = {
min_limit: 0,
max_limit: 0,
};
if (this.product === 'automated') {
limit_values = {
min_limit: 100,
max_limit: 1000,
};
} else {
const result = await Application.fetchOrdersFromHappy(
{
source: 'secured_card',
status: 'completed',
user_id: this.user_id,
},
1,
);
if (result.statusCode === 200 && result.body.code === undefined) {
const securedCardOrder = result.body[0];
if (securedCardOrder) {
//we find a match and processing for secured card
limit_values = {
min_limit: 2000,
max_limit: 2000,
};
} else {
throw new GcError('failed_fetch');
}
}
}
const condition = {
purpose: {
[Op.in]: ['processing-fee-payment', 'cic', 'limit-enhancement'],
},
status: 'completed',
user_id: this.user_id,
};
let orders = [];
let page = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const result = await Application.fetchOrdersFromHappy(
condition,
10,
page++,
);
if (result.statusCode === 200 && result.body.code === undefined) {
orders = orders.concat(result.body);
if (result.body.length < 10) {
break;
}
} else {
throw new GcError('failed_search');
}
}
let creditLimit = 0;
for (let i = 0; i < orders.length; i++) {
creditLimit += orders[i].data.amount;
}
const contacts = await approvedReferenceContacts({
user_id: this.user_id,
application_id: this.id,
});
for (let i = 0; i < contacts.length; i++) {
let contact = contacts[i];
if (contact.knows === 'accepted') {
if (contact.is_important) {
creditLimit += 1000000;
} else {
creditLimit += 100000;
}
}
}
limit_values.min_allowed_limit =
limit_values.min_allowed_limit + creditLimit / 10000;
limit_values.max_allowed_limit =
limit_values.max_allowed_limit + creditLimit / 10000;
return limit_values;
};
Application.prototype.setRemainingApplicationStatus = async function (
limitValue,
) {
console.warn('checking/giving limit', this.id);
if (limitValue) {
const result = await request({
uri: 'http://ironman/v1/settings/activate',
body: {
user_id: this.user_id,
min_allowed_limit: limitValue.min_limit,
max_allowed_limit: limitValue.max_limit,
product: this.product,
},
method: 'POST',
});
if (result.statusCode !== 200) {
throw new GcError(`Error: ${JSON.stringify(result.body)}`);
} else {
console.warn(
'limit now exists',
this.id,
result.statusCode,
result.body.min_allowed_limit,
);
return this.updateAgreementAndContactStatus();
}
}
};
Application.prototype.updateToNextStatus = async function () {
if (PROCESSED_APPLICATION_STATUSES.includes(this.status)) {
return;
}
const personal_data =
this.findMissingPersonalDataFields(PERSONAL_DATA_FIELDS);
if (Object.keys(personal_data).length > 0) {
return this.update({
status: 'personal',
});
}
const pan = await sequelize.models.pan.findOne({
attributes: ['status', 'extra_data', 'value', 'application_id'],
where: {
application_id: this.id,
},
});
if (!pan) {
console.warn('pan not submitted', this.id);
return this.update({
status: 'pan',
});
}
const aadhaar = await sequelize.models.aadhaar.findOne({
where: {
application_id: this.id,
file_id: {
[Op.ne]: null,
},
},
});
if (!aadhaar) {
console.warn('aadhaar not submitted', this.id);
return this.update({
status: 'aadhaar',
});
}
const result = await Application.fetchOrdersFromHappy(
{
source: 'processing-fee-payment',
status: 'completed',
user_id: this.user_id,
},
1,
);
if (result.statusCode === 200 && result.body.code === undefined) {
if (result.body.length === 0) {
return this.update({
status: 'processing-fee-payment',
});
}
} else {
throw new GcError('search_failed');
}
if (pan.status === 'pending') {
return pan.verify();
} else if (pan.status === 'rejected') {
return this.update({
status: 'pan',
});
} else if (pan.status === 'approved') {
if (aadhaar.status === 'pending') {
try {
return await aadhaar.verify();
} catch (e) {
console.error(`Error: aadhaar ${e.message}`);
return this.update({
status: 'aadhaar',
});
}
} else if (aadhaar.status === 'approved') {
if (!this.product) {
await this.evaluateApplicationCreditEligibility(pan);
}
if (this.product) {
const limit = await this.getApplicationCreditLimit();
return this.setRemainingApplicationStatus(limit);
}
} else if (aadhaar.status === 'rejected') {
return this.update({
status: 'aadhaar',
});
}
}
};
Application.prototype.updateContactStatus = async function (
requiredContacts = 1,
) {
if (requiredContacts === 0) {
return false;
}
const contactsShared = await hasUserEnoughContacts(
this.user_id,
requiredContacts,
);
if (!contactsShared) {
console.warn('user has not shared contacts', this.id);
await this.update({
status: 'contacts',
});
return true;
}
return false;
};
Application.prototype.updateAgreementAndContactStatus = async function () {
const result = await request(`http://ironman/v1/settings/${this.user_id}`);
if (result.statusCode === 200) {
const setting = result.body;
// do we have agreement?
const agreement = await sequelize.models.agreements.findOne({
where: {
application_id: this.id,
lender_id: setting.lender_id,
file_id: {
[Op.ne]: null,
},
},
});
return this.update({
status: agreement ? 'approved' : 'agreement',
});
}
};
Application.fetchOrdersFromHappy = async function (
where,
limit = 1,
offset = 0,
) {
return request({
uri: 'http://happy/v1/orders/search',
method: 'POST',
body: {
where,
limit,
offset,
},
});
};
Application.prototype.publishEnhancedLimit = async function () {
let { min_allowed_limit, max_allowed_limit } =
await this.getApplicationCreditLimit();
const body = {
user_id: this.user_id,
min_allowed_limit,
max_allowed_limit,
product: this.product,
};
const result = await request({
uri: 'http://ironman/v1/settings/activate',
body,
method: 'POST',
});
if (result.statusCode !== 200 || result.body.code !== undefined) {
throw new GcError(result.body.code || 'failed_ironman');
}
};
module.exports = Application;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment