From 5833a64d398d8e3864fc8cbb585b7457883efb44 Mon Sep 17 00:00:00 2001 From: Joao Pedro da Silva Date: Thu, 29 Jun 2023 09:16:51 -0300 Subject: [PATCH] microcredit demographics route (#687) --- .../src/controllers/v2/microcredit/list.ts | 7 + .../api/src/routes/v2/microcredit/list.ts | 23 ++ .../core/src/services/microcredit/list.ts | 221 +++++++++++++++++- .../core/src/subgraph/queries/microcredit.ts | 12 +- 4 files changed, 257 insertions(+), 6 deletions(-) diff --git a/packages/api/src/controllers/v2/microcredit/list.ts b/packages/api/src/controllers/v2/microcredit/list.ts index 717d973cd..4ba91e0bc 100644 --- a/packages/api/src/controllers/v2/microcredit/list.ts +++ b/packages/api/src/controllers/v2/microcredit/list.ts @@ -83,6 +83,13 @@ class MicroCreditController { .then(r => standardResponse(res, 200, true, r)) .catch(e => standardResponse(res, 400, false, '', { error: e })); }; + + demographics = (req: RequestWithUser, res: Response) => { + this.microCreditService + .demographics() + .then(r => standardResponse(res, 200, true, r)) + .catch(e => standardResponse(res, 400, false, '', { error: e })); + }; } export { MicroCreditController }; diff --git a/packages/api/src/routes/v2/microcredit/list.ts b/packages/api/src/routes/v2/microcredit/list.ts index 1dac7cd2e..64ed9ae70 100644 --- a/packages/api/src/routes/v2/microcredit/list.ts +++ b/packages/api/src/routes/v2/microcredit/list.ts @@ -213,4 +213,27 @@ export default (route: Router): void => { cache(cacheIntervals.fiveMinutes), controller.getRepaymentsHistory ); + + /** + * @swagger + * + * /microcredit/demographics: + * get: + * tags: + * - "microcredit" + * summary: "Get Microcredit demographics" + * description: "Get Microcredit demographics" + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/demographics' + */ + route.get( + '/demographics', + cache(cacheIntervals.oneHour), + controller.demographics + ) }; diff --git a/packages/core/src/services/microcredit/list.ts b/packages/core/src/services/microcredit/list.ts index fc83eb592..dd895517f 100644 --- a/packages/core/src/services/microcredit/list.ts +++ b/packages/core/src/services/microcredit/list.ts @@ -5,7 +5,7 @@ import { JsonRpcProvider } from '@ethersproject/providers'; import { MicrocreditABI as MicroCreditABI } from '../../contracts'; import { BigNumber, Contract } from 'ethers'; import { config } from '../../..'; -import { WhereOptions } from 'sequelize'; +import { WhereOptions, literal, Op } from 'sequelize'; import { MicroCreditApplications } from '../../interfaces/microCredit/applications'; import { utils } from '@impactmarket/core'; @@ -239,4 +239,223 @@ export default class MicroCreditList { docs: docs.map(d => d.toJSON()) }; }; + + /** + * @swagger + * components: + * schemas: + * ageRange: + * type: object + * properties: + * ageRange1: + * type: number + * description: age range 18 - 24 + * ageRange2: + * type: number + * description: age range 25 - 34 + * ageRange3: + * type: number + * description: age range 35 - 44 + * ageRange4: + * type: number + * description: age range 45 - 54 + * ageRange5: + * type: number + * description: age range 55 - 64 + * ageRange6: + * type: number + * description: age range 65+ + */ + + /** + * @swagger + * components: + * schemas: + * demographics: + * type: object + * properties: + * gender: + * type: array + * items: + * type: object + * properties: + * country: + * type: string + * description: country + * example: BR + * male: + * type: number + * description: total user males + * female: + * type: number + * description: total user females + * undisclosed: + * type: number + * description: users with no information about gender + * totalGender: + * type: number + * description: total users + * ageRange: + * type: object + * properties: + * paid: + * $ref: '#/components/schemas/ageRange' + * pending: + * $ref: '#/components/schemas/ageRange' + * overdue: + * $ref: '#/components/schemas/ageRange' + * + */ + public demographics = async () => { + try { + // get all borrower addresses + const limit = 100; + const addresses: { + paid: string[], + pending: string[], + overdue: string[], + } = { + paid: [], + pending: [], + overdue: [], + }; + + for (let i = 0; ; i += limit) { + const rawBorrowers = await getBorrowers({ limit, offset: i, claimed: true, filter: 'all' }); + if (rawBorrowers.borrowers.length === 0) + break; + + rawBorrowers.borrowers.forEach((b) => { + // create payment status + if (b.lastDebt === '0') { + addresses.paid.push(getAddress(b.borrower!.id)); + } else { + const limitDate = new Date(); + const claimed = new Date(b.claimed*1000); + limitDate.setSeconds(claimed.getSeconds() + b.period); + + if (limitDate > new Date()) { + addresses.overdue.push(getAddress(b.borrower!.id)); + } else { + addresses.pending.push(getAddress(b.borrower!.id)); + } + } + }); + } + + const year = new Date().getUTCFullYear(); + const ageAttributes: any[] = [ + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 18 AND 24)` + ), + 'ageRange1', + ], + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 25 AND 34)` + ), + 'ageRange2', + ], + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 35 AND 44)` + ), + 'ageRange3', + ], + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 45 AND 54)` + ), + 'ageRange4', + ], + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 55 AND 64)` + ), + 'ageRange5', + ], + [ + literal( + `count(*) FILTER (WHERE ${year}-year BETWEEN 65 AND 120)` + ), + 'ageRange6', + ], + ]; + const genderAttributes: any[] = [ + 'country', + [ + literal( + 'count(*) FILTER (WHERE gender = \'m\')' + ), + 'male', + ], + [ + literal( + 'count(*) FILTER (WHERE gender = \'f\')' + ), + 'female', + ], + [ + literal( + 'count(*) FILTER (WHERE gender = \'u\' OR gender is null)' + ), + 'undisclosed', + ], + [literal('count(*)'), 'totalGender'], + ] + + // get age range and gender by payment status + const [overdue, pending, paid, gender] = await Promise.all([ + models.appUser.findAll({ + attributes: ageAttributes, + where: { + address: { + [Op.in]: addresses.overdue + } + }, + raw: true, + }), + models.appUser.findAll({ + attributes: ageAttributes, + where: { + address: { + [Op.in]: addresses.pending + } + }, + raw: true, + }), + models.appUser.findAll({ + attributes: ageAttributes, + where: { + address: { + [Op.in]: addresses.paid + } + }, + raw: true, + }), + models.appUser.findAll({ + attributes: genderAttributes, + where: { + address: { + [Op.in]: [...addresses.paid, ...addresses.overdue, ...addresses.pending] + } + }, + group: ['country'], + raw: true, + }), + ]); + + return { + gender, + ageRange: { + paid: paid[0], + pending: pending[0], + overdue: overdue[0], + }, + }; + } catch (error) { + throw new utils.BaseError('DEMOGRAPHICS_FAILED', error.message || 'failed to get microcredit demographics') + } + } } diff --git a/packages/core/src/subgraph/queries/microcredit.ts b/packages/core/src/subgraph/queries/microcredit.ts index 07f60c6d3..87fb6076d 100644 --- a/packages/core/src/subgraph/queries/microcredit.ts +++ b/packages/core/src/subgraph/queries/microcredit.ts @@ -222,9 +222,9 @@ export const getMicroCreditStatsLastDays = async ( export type SubgraphGetBorrowersQuery = { offset?: number; limit?: number; - addedBy: string; + addedBy?: string; claimed?: boolean; - filter?: 'repaid' | 'needHelp'; + filter?: 'repaid' | 'needHelp' | 'all'; orderBy?: | 'amount' | 'amount:asc' @@ -240,7 +240,7 @@ export type SubgraphGetBorrowersQuery = { | 'lastDebt:desc'; }; -const filtersToBorrowersQuery = (filter: 'repaid' | 'needHelp' | undefined): string => { +const filtersToBorrowersQuery = (filter: 'repaid' | 'needHelp' | 'all' | undefined): string => { switch (filter) { case 'repaid': return 'lastDebt: 0'; @@ -249,6 +249,8 @@ const filtersToBorrowersQuery = (filter: 'repaid' | 'needHelp' | undefined): str const date = new Date(); date.setMonth(date.getMonth() - 3); return `lastRepayment_lte: ${Math.floor(date.getTime() / 1000)}`; + case 'all': + return ''; default: return 'lastDebt_not: 0'; } @@ -265,7 +267,7 @@ const countGetBorrowers = async (query: SubgraphGetBorrowersQuery): Promise