From 277da49880e9146b2e61d4628892d7ba3d9d73de Mon Sep 17 00:00:00 2001 From: Bernardo Vieira Date: Mon, 30 Oct 2023 12:26:02 +0000 Subject: [PATCH 1/3] repayment rate --- .../src/controllers/v2/microcredit/create.ts | 42 ++++++++++++------- .../api/src/routes/v2/microcredit/create.ts | 5 ++- .../api/src/routes/v2/microcredit/list.ts | 2 +- packages/api/src/validators/microcredit.ts | 8 ++-- ...703120856-create-micro-credit-borrowers.js | 12 +++++- ...1027214157-update-microcredit-borrowers.js | 30 +++++++++++++ .../database/models/microCredit/borrowers.ts | 30 +++++++++++-- .../src/interfaces/microCredit/borrowers.ts | 8 +++- .../core/src/services/microcredit/create.ts | 25 +++++++++++ .../core/src/services/microcredit/list.ts | 42 ++++++++++++++----- 10 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 packages/core/src/database/migrations/z20231027214157-update-microcredit-borrowers.js diff --git a/packages/api/src/controllers/v2/microcredit/create.ts b/packages/api/src/controllers/v2/microcredit/create.ts index 38f982739..eddbe97f9 100644 --- a/packages/api/src/controllers/v2/microcredit/create.ts +++ b/packages/api/src/controllers/v2/microcredit/create.ts @@ -76,21 +76,33 @@ class MicroCreditController { return; } - this.microCreditService - .updateApplication( - req.body.map(a => a.applicationId), - req.body.map(a => a.status) - ) - .then(r => { - // we will do the cleaning cache here since this only needs to clean cache - // for applications, but if accepted it also needs to clean cache for borrowers list - // and the chain subscriber will do that - if (req.user?.userId) { - utils.cache.cleanMicroCreditApplicationsCache(req.user?.userId); - } - standardResponse(res, 201, true, r); - }) - .catch(e => standardResponse(res, 400, false, '', { error: e })); + const repaymentsDefined = req.body.filter(a => a.repaymentRate !== undefined); + + if (repaymentsDefined.length > 0) { + this.microCreditService + .updateRepaymentRate( + req.body.map(a => a.applicationId), + req.body.map(a => a.repaymentRate) + ) + .then(r => standardResponse(res, 201, true, r)) + .catch(e => standardResponse(res, 400, false, '', { error: e })); + } else { + this.microCreditService + .updateApplication( + req.body.map(a => a.applicationId), + req.body.map(a => a.status) + ) + .then(r => { + // we will do the cleaning cache here since this only needs to clean cache + // for applications, but if accepted it also needs to clean cache for borrowers list + // and the chain subscriber will do that + if (req.user?.userId) { + utils.cache.cleanMicroCreditApplicationsCache(req.user?.userId); + } + standardResponse(res, 201, true, r); + }) + .catch(e => standardResponse(res, 400, false, '', { error: e })); + } }; saveForm = async (req: RequestWithUser, res: Response): Promise => { diff --git a/packages/api/src/routes/v2/microcredit/create.ts b/packages/api/src/routes/v2/microcredit/create.ts index f427b4937..8caa64607 100644 --- a/packages/api/src/routes/v2/microcredit/create.ts +++ b/packages/api/src/routes/v2/microcredit/create.ts @@ -97,7 +97,7 @@ export default (route: Router): void => { * tags: * - "microcredit" * summary: "Update microcredit applications" - * description: "Status can be 0: draft, 1: pending, 2: in-review, 3: requested-changes, 4: interview, 5: approved, 6: rejected" + * description: "repaymentRate is in seconds. Status can be 0: draft, 1: pending, 2: in-review, 3: requested-changes, 4: interview, 5: approved, 6: rejected" * requestBody: * required: true * content: @@ -113,6 +113,9 @@ export default (route: Router): void => { * status: * type: number * example: 1 + * repaymentRate: + * type: number + * example: 604800 * responses: * "200": * description: "Success" diff --git a/packages/api/src/routes/v2/microcredit/list.ts b/packages/api/src/routes/v2/microcredit/list.ts index 3618c6f66..0ede69c45 100644 --- a/packages/api/src/routes/v2/microcredit/list.ts +++ b/packages/api/src/routes/v2/microcredit/list.ts @@ -39,7 +39,7 @@ export default (route: Router): void => { * name: filter * schema: * type: string - * enum: [not-claimed, ontrack, need-help, repaid, urgent] + * enum: [not-claimed, ontrack, need-help, repaid, urgent, failed-repayment] * required: false * description: optional filter (leave it undefined to get all) * - in: query diff --git a/packages/api/src/validators/microcredit.ts b/packages/api/src/validators/microcredit.ts index 06bbedaf8..0ddeb22c9 100644 --- a/packages/api/src/validators/microcredit.ts +++ b/packages/api/src/validators/microcredit.ts @@ -8,7 +8,7 @@ const validator = createValidator(); type ListBorrowersType = { offset?: number; limit?: number; - filter?: 'not-claimed' | 'ontrack' | 'need-help' | 'repaid' | 'urgent'; + filter?: 'not-claimed' | 'ontrack' | 'need-help' | 'repaid' | 'urgent' | 'failed-repayment'; orderBy?: | 'amount' | 'amount:asc' @@ -39,7 +39,7 @@ type ListApplicationsType = { const queryListBorrowersSchema = defaultSchema.object({ offset: Joi.number().optional().default(0), limit: Joi.number().optional().max(20).default(10), - filter: Joi.string().optional().valid('not-claimed', 'ontrack', 'need-help', 'repaid', 'urgent'), + filter: Joi.string().optional().valid('not-claimed', 'ontrack', 'need-help', 'repaid', 'urgent', 'failed-repayment'), orderBy: Joi.string() .optional() .valid( @@ -134,6 +134,7 @@ type PostDocsRequestType = { type PutApplicationsRequestType = [ { applicationId: number; + repaymentRate: number; status: number; } ]; @@ -164,7 +165,8 @@ const putApplicationsValidator = celebrate({ .items( Joi.object({ applicationId: Joi.number().required(), - status: Joi.number().required() + repaymentRate: Joi.number().optional(), + status: Joi.number().optional() }).required() ) .required() diff --git a/packages/core/src/database/migrations/z20230703120856-create-micro-credit-borrowers.js b/packages/core/src/database/migrations/z20230703120856-create-micro-credit-borrowers.js index b4a742921..1c261fab2 100644 --- a/packages/core/src/database/migrations/z20230703120856-create-micro-credit-borrowers.js +++ b/packages/core/src/database/migrations/z20230703120856-create-micro-credit-borrowers.js @@ -13,15 +13,25 @@ module.exports = { allowNull: false, type: Sequelize.INTEGER }, + applicationId: { + allowNull: true, + type: Sequelize.INTEGER + }, performance: { allowNull: false, + type: Sequelize.INTEGER, + defaultValue: 100 + }, + repaymentRate: { + allowNull: true, type: Sequelize.INTEGER }, lastNotificationRepayment: { + allowNull: true, type: Sequelize.DATE }, manager: { - allowNull: false, + allowNull: true, type: Sequelize.STRING(48) } }); diff --git a/packages/core/src/database/migrations/z20231027214157-update-microcredit-borrowers.js b/packages/core/src/database/migrations/z20231027214157-update-microcredit-borrowers.js new file mode 100644 index 000000000..6f7d6f84b --- /dev/null +++ b/packages/core/src/database/migrations/z20231027214157-update-microcredit-borrowers.js @@ -0,0 +1,30 @@ +'use strict'; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + if (process.env.NODE_ENV === 'test') { + return; + } + + await queryInterface.addColumn('microcredit_borrowers', 'repaymentRate', { + allowNull: true, + type: Sequelize.INTEGER + }); + await queryInterface.addColumn('microcredit_borrowers', 'applicationId', { + allowNull: true, + type: Sequelize.INTEGER + }); + await queryInterface.changeColumn('microcredit_borrowers', 'manager', { + allowNull: true, + type: Sequelize.STRING(48) + }); + await queryInterface.changeColumn('microcredit_borrowers', 'performance', { + allowNull: false, + type: Sequelize.INTEGER, + defaultValue: 100 + }); + }, + async down(queryInterface, Sequelize) { + // + } +}; diff --git a/packages/core/src/database/models/microCredit/borrowers.ts b/packages/core/src/database/models/microCredit/borrowers.ts index 6dd936a21..4a19ba8e4 100644 --- a/packages/core/src/database/models/microCredit/borrowers.ts +++ b/packages/core/src/database/models/microCredit/borrowers.ts @@ -8,7 +8,9 @@ import { SubgraphMicroCreditBorrowersModel } from './subgraphBorrowers'; export class MicroCreditBorrowersModel extends Model { public id!: number; public userId!: number; + public applicationId!: number; public performance!: number; + public repaymentRate!: number; public lastNotificationRepayment!: Date; public manager!: string; @@ -17,7 +19,7 @@ export class MicroCreditBorrowersModel extends Model { + for (let x = 0; x < applicationId_.length; x++) { + const applicationId = applicationId_[x]; + const repaymentRate = repaymentRate_[x]; + + const application = await models.microCreditApplications.findOne({ + where: { + id: applicationId + } + }); + const borrowerUserId = application!.userId; + + await models.microCreditBorrowers.upsert( + { + userId: borrowerUserId, + applicationId, + repaymentRate + }, + { + conflictFields: ['userId', 'applicationId'] + } + ); + } + } + public saveForm = async ( userId: number, form: object, diff --git a/packages/core/src/services/microcredit/list.ts b/packages/core/src/services/microcredit/list.ts index 7866a1b31..cb86da0e7 100644 --- a/packages/core/src/services/microcredit/list.ts +++ b/packages/core/src/services/microcredit/list.ts @@ -1,6 +1,7 @@ import { MicroCreditApplication, MicroCreditApplicationStatus } from '../../interfaces/microCredit/applications'; import { MicroCreditBorrowers } from '../../interfaces/microCredit/borrowers'; import { Op, Order, WhereOptions, col, fn, literal } from 'sequelize'; +import { SubgraphMicroCreditBorrowers } from '../../interfaces/microCredit/subgraphBorrowers'; import { config } from '../../..'; import { getAddress } from '@ethersproject/address'; import { @@ -70,7 +71,7 @@ export type GetBorrowersQuery = { offset?: number; limit?: number; addedBy?: string; - filter?: 'all' | 'not-claimed' | 'ontrack' | 'need-help' | 'repaid' | 'urgent'; + filter?: 'all' | 'not-claimed' | 'ontrack' | 'need-help' | 'repaid' | 'urgent' | 'failed-repayment'; orderBy?: | 'amount' | 'amount:asc' @@ -104,12 +105,15 @@ export default class MicroCreditList { maturity: number; amount: number; period: number; - dailyInterest: number; - claimed: number; - repaid: number; - lastRepayment: number; - lastRepaymentAmount: number; - lastDebt: number; + dailyInterest?: number; + claimed?: number; + repaid?: number; + lastRepayment?: number; + lastRepaymentAmount?: number; + lastDebt?: number; + // + performance: number; + repaymentRate: number | null; }; }[]; }> => { @@ -179,13 +183,26 @@ export default class MicroCreditList { literal(`(claimed + period) <= ${Math.trunc(limitDate.getTime() / 1000)}`) ] }; + case 'failed-repayment': + where = { + ...where, + repaymentRate: { [Op.ne]: null } as any + }; + return { + [Op.and]: [ + { status: 1 }, + { lastDebt: { [Op.gt]: 0 } }, + { lastRepayment: { [Op.ne]: null } }, + literal(`(loan."lastRepayment" + "repaymentRate") < ${Math.trunc(now.getTime() / 1000)}`) + ] + }; default: return {}; } }; const rBorrowers = await models.microCreditBorrowers.findAndCountAll({ - attributes: ['performance'], + attributes: ['performance', 'repaymentRate'], where: { ...where, manager: query.addedBy @@ -196,7 +213,7 @@ export default class MicroCreditList { include: [ { model: models.appUser, - attributes: ['address', 'firstName', 'lastName', 'avatarMediaPath'], + attributes: ['id', 'address', 'firstName', 'lastName', 'avatarMediaPath'], as: 'user', required: true }, @@ -226,8 +243,11 @@ export default class MicroCreditList { count: rBorrowers.count, rows: rBorrowers.rows.map(r => ({ ...r.user!.toJSON(), - loan: r.loan!, - performance: r.performance + loan: { + ...(r.loan!.toJSON() as SubgraphMicroCreditBorrowers & { maturity: number }), + performance: r.performance, + repaymentRate: r.repaymentRate + } })) }; }; From 53f3de466d1578dfe245448c6eed3fbdaa6be4e4 Mon Sep 17 00:00:00 2001 From: Bernardo Vieira Date: Mon, 30 Oct 2023 16:17:41 +0000 Subject: [PATCH 2/3] fix borrower update from chain subscriber --- packages/api/src/validators/microcredit.ts | 4 +- .../core/src/services/microcredit/create.ts | 5 +- packages/worker/src/chainSubscribers.ts | 77 ++++++++++--------- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/packages/api/src/validators/microcredit.ts b/packages/api/src/validators/microcredit.ts index 0ddeb22c9..e733daddc 100644 --- a/packages/api/src/validators/microcredit.ts +++ b/packages/api/src/validators/microcredit.ts @@ -39,7 +39,9 @@ type ListApplicationsType = { const queryListBorrowersSchema = defaultSchema.object({ offset: Joi.number().optional().default(0), limit: Joi.number().optional().max(20).default(10), - filter: Joi.string().optional().valid('not-claimed', 'ontrack', 'need-help', 'repaid', 'urgent', 'failed-repayment'), + filter: Joi.string() + .optional() + .valid('not-claimed', 'ontrack', 'need-help', 'repaid', 'urgent', 'failed-repayment'), orderBy: Joi.string() .optional() .valid( diff --git a/packages/core/src/services/microcredit/create.ts b/packages/core/src/services/microcredit/create.ts index de300991b..7e65fc2e4 100644 --- a/packages/core/src/services/microcredit/create.ts +++ b/packages/core/src/services/microcredit/create.ts @@ -138,9 +138,12 @@ export default class MicroCreditCreate { }, include: [ { + // get last application only model: models.microCreditApplications, as: 'microCreditApplications', - attributes: ['id'] + attributes: ['id'], + order: [['id', 'DESC']], + limit: 1 } ] }); diff --git a/packages/worker/src/chainSubscribers.ts b/packages/worker/src/chainSubscribers.ts index 3e9af2ed1..0fab99af6 100644 --- a/packages/worker/src/chainSubscribers.ts +++ b/packages/worker/src/chainSubscribers.ts @@ -389,7 +389,7 @@ class ChainSubscribers { await database.models.subgraphUBIBeneficiary.create({ userAddress: getAddress(userAddress), communityAddress: getAddress(communityAddress), - since: new Date().getTime() / 1000 | 0, + since: (new Date().getTime() / 1000) | 0, claimed: 0, state: 0 }); @@ -428,14 +428,17 @@ class ChainSubscribers { if (community) { // update subgraph beneficiary - await database.models.subgraphUBIBeneficiary.update({ - state: 1 - }, { - where: { - userAddress: getAddress(userAddress), - communityAddress: getAddress(communityAddress), + await database.models.subgraphUBIBeneficiary.update( + { + state: 1 + }, + { + where: { + userAddress: getAddress(userAddress), + communityAddress: getAddress(communityAddress) + } } - }); + ); this._waitForSubgraphToIndex(log).then(() => { utils.cache.cleanBeneficiaryCache(community); @@ -456,56 +459,54 @@ class ChainSubscribers { if (parsedLog.name === 'LoanAdded') { utils.Logger.info('Add Loan event'); - const [user, transactionsReceipt] = await Promise.all([ + const [user, application, transactionsReceipt] = await Promise.all([ database.models.appUser.findOne({ attributes: ['id', 'language', 'walletPNT', 'appPNT'], where: { address: getAddress(userAddress) } }), + database.models.microCreditApplications.findOne({ + where: { + userId: userAddress + }, + order: [['id', 'DESC']] + }), this.provider.getTransaction(log.transactionHash) ]); if (user) { - const [[borrower, created], loanManagerUser] = await Promise.all([ - database.models.microCreditBorrowers.findOrCreate({ - where: { - userId: user.id - }, - defaults: { - userId: user.id, - manager: transactionsReceipt.from, - performance: 100 - }, - transaction - }), + const [loanManagerUser] = await Promise.all([ database.models.appUser.findOne({ attributes: ['id'], where: { address: getAddress(transactionsReceipt.from) } }), + database.models.microCreditBorrowers.upsert( + { + userId: user.id, + applicationId: application?.id || 1, + manager: transactionsReceipt.from, + performance: 100 + }, + { + conflictFields: ['userId', 'applicationId'], + transaction + } + ), this.microCreditService.updateApplication( - [userAddress], + application !== null ? [application.id] : [userAddress], [interfaces.microcredit.microCreditApplications.MicroCreditApplicationStatus.APPROVED], transaction ) ]); - if (!created) { - this._waitForSubgraphToIndex(log).then(() => { - utils.cache.cleanUserRolesCache(userAddress); - if (loanManagerUser) { - utils.cache.cleanMicroCreditBorrowersCache(loanManagerUser.id); - utils.cache.cleanMicroCreditApplicationsCache(loanManagerUser.id); - } - }); - await borrower.update( - { - manager: transactionsReceipt.from, - performance: 100 - }, - { transaction } - ); - } + this._waitForSubgraphToIndex(log).then(() => { + utils.cache.cleanUserRolesCache(userAddress); + if (loanManagerUser) { + utils.cache.cleanMicroCreditBorrowersCache(loanManagerUser.id); + utils.cache.cleanMicroCreditApplicationsCache(loanManagerUser.id); + } + }); } } else if (parsedLog.name === 'LoanClaimed') { utils.Logger.info('Claim Loan event'); From f34331abf2372e2458a8bbbfa05e9025b781a044 Mon Sep 17 00:00:00 2001 From: Bernardo Vieira Date: Mon, 30 Oct 2023 16:47:49 +0000 Subject: [PATCH 3/3] fix tests --- services/microcredit/tests/notification.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/microcredit/tests/notification.test.ts b/services/microcredit/tests/notification.test.ts index 0f7bec59e..057f4ac83 100644 --- a/services/microcredit/tests/notification.test.ts +++ b/services/microcredit/tests/notification.test.ts @@ -62,6 +62,7 @@ describe.skip('notification lambda', () => { await database.models.microCreditBorrowers.create({ userId: user.id, + applicationId: 1, performance: idx === 1 ? 100 : 80, manager: 'abc123' });