diff --git a/functions/src/backoffice/business/index.ts b/functions/src/backoffice/business/index.ts index b7aa1b32..85cfa313 100644 --- a/functions/src/backoffice/business/index.ts +++ b/functions/src/backoffice/business/index.ts @@ -1,357 +1,353 @@ -import { backofficeDb } from '../database'; -import { getSecret } from '../../settings'; -import { env } from '../../constants'; -import { google } from 'googleapis' -import { date } from '../helper' - - -const SERVICE_ACCOUNT = 'SERVICE_ACCOUNT' -const SERVICE_ACCOUNT_PRIVATE_KEY = 'SERVICE_ACCOUNT_PRIVATE_KEY' - -export async function fillPayOutSheet():Promise { - const data = await backofficeDb.getPayout(); - - const sheets = google.sheets('v4') - const jwtClient = new google.auth.JWT({ - email: await getSecret(SERVICE_ACCOUNT), - key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), - scopes: [ 'https://www.googleapis.com/auth/spreadsheets' ], // read and write sheets - }) - - - const values = [[ - 'Proposal Id', - 'Amount', - 'Approval created at', - 'Approval updated at', - 'User UID', - 'User email', - 'First name', - 'Last name', - 'Common id', - 'Common name', - 'Init Link', - 'Payment id', - 'Payment status', - 'Payment amount', - 'Fees', - 'Payment creation date', - 'Payment updated', - 'IBAN**', - 'Routing number**', - 'Bank account**', - 'Full Name* - Billing address', - 'City* - Billing address', - 'Country* - Billing address', - 'Line 1* - Billing address', - 'Line 2 - Billing address', - 'District - Billing address', - 'Postal code* - Billing address', - 'Name* - Bank', - 'City* - Bank', - 'Country* - Bank', - 'Line 1 - Bank', - 'Line 2 - Bank', - 'District - Bank', - 'Postal code - Bank', - 'Type* - Payout' - ]]; - - let row = 2; - for (const key in data) { - // eslint-disable-next-line no-prototype-builtins - if (data.hasOwnProperty(key)) { - const cells = [] - cells.push(data[key].proposal.id) - cells.push(data[key].proposal.fundingRequest.amount/100) - cells.push(date(new Date(data[key].proposal.createdAt.toDate()))) - cells.push(date(new Date(data[key].proposal.updatedAt.toDate()))) - cells.push(data[key].proposal.proposerId) - cells.push(data[key].user.email) - cells.push(data[key].user.firstName) - cells.push(data[key].user.lastName) - cells.push(data[key].common.id) - cells.push(data[key].common.name) - - cells.push(`=createInitLink(R${row}:AG${row}, A${row})`); - - - if(data[key].payout){ - //this is init link, must be empty - - cells.push(data[key].payout.id); - - let status = ''; - if(!data[key].payout.executed){ - status = 'initiated'; - } else { - if(data[key].payout.status === 'pending'){ - status = 'pending'; - - } - if(data[key].payout.status === 'complete'){ - status = 'complete'; - - } - if(data[key].payout.status === 'failed'){ - status = 'failed'; - - } - } - cells.push(status); - cells.push(data[key].payout.amount); - //this is fees - cells.push(""); - cells.push(date(new Date(data[key].payout.createdAt.toDate()))) - cells.push(date(new Date(data[key].payout.updatedAt.toDate()))) - - } - - values.push(cells); +import {backofficeDb} from '../database'; +import {getSecret} from '../../settings'; +import {env} from '../../constants'; +import {google} from 'googleapis'; +import {date} from '../helper'; + +const SERVICE_ACCOUNT = 'SERVICE_ACCOUNT'; +const SERVICE_ACCOUNT_PRIVATE_KEY = 'SERVICE_ACCOUNT_PRIVATE_KEY'; + +export async function fillPayOutSheet(): Promise { + const data = await backofficeDb.getPayout(); + + const sheets = google.sheets('v4'); + const jwtClient = new google.auth.JWT({ + email: await getSecret(SERVICE_ACCOUNT), + key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), + scopes: ['https://www.googleapis.com/auth/spreadsheets'], // read and write sheets + }); + + const values = [ + [ + 'Proposal Id', + 'Amount', + 'Approval created at', + 'Approval updated at', + 'User UID', + 'User email', + 'First name', + 'Last name', + 'Common id', + 'Common name', + 'Init Link', + 'Payment id', + 'Payment status', + 'Payment amount', + 'Fees', + 'Payment creation date', + 'Payment updated', + 'IBAN**', + 'Routing number**', + 'Bank account**', + 'Full Name* - Billing address', + 'City* - Billing address', + 'Country* - Billing address', + 'Line 1* - Billing address', + 'Line 2 - Billing address', + 'District - Billing address', + 'Postal code* - Billing address', + 'Name* - Bank', + 'City* - Bank', + 'Country* - Bank', + 'Line 1 - Bank', + 'Line 2 - Bank', + 'District - Bank', + 'Postal code - Bank', + 'Type* - Payout', + ], + ]; + + let row = 2; + for (const key in data) { + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty(key)) { + const cells = []; + cells.push(data[key].proposal.id); + cells.push(data[key].proposal.fundingRequest.amount / 100); + cells.push(date(new Date(data[key].proposal.createdAt.toDate()))); + cells.push(date(new Date(data[key].proposal.updatedAt.toDate()))); + cells.push(data[key].proposal.proposerId); + cells.push(data[key].user.email); + cells.push(data[key].user.firstName); + cells.push(data[key].user.lastName); + cells.push(data[key].common.id); + cells.push(data[key].common.name); + + cells.push(`=createInitLink(R${row}:AG${row}, A${row})`); + + if (data[key].payout) { + //this is init link, must be empty + + cells.push(data[key].payout.id); + + let status = ''; + if (!data[key].payout.executed) { + status = 'initiated'; + } else { + if (data[key].payout.status === 'pending') { + status = 'pending'; + } + if (data[key].payout.status === 'complete') { + status = 'complete'; } - row++; + if (data[key].payout.status === 'failed') { + status = 'failed'; + } + } + cells.push(status); + cells.push(data[key].payout.amount); + //this is fees + cells.push(''); + cells.push(date(new Date(data[key].payout.createdAt.toDate()))); + cells.push(date(new Date(data[key].payout.updatedAt.toDate()))); } - const resource = { - values, - }; - - const jwtAuthPromise = jwtClient.authorize() - await jwtAuthPromise - await sheets.spreadsheets.values.update({ - auth: jwtClient, - spreadsheetId: env.backoffice.sheetUrl, - range: 'PAY_OUT!A1', // update this range of cells - valueInputOption: 'USER_ENTERED', - requestBody: resource - }, {}) - - return data; + values.push(cells); + } + row++; + } + const resource = { + values, + }; + + const jwtAuthPromise = jwtClient.authorize(); + await jwtAuthPromise; + await sheets.spreadsheets.values.update( + { + auth: jwtClient, + spreadsheetId: env.backoffice.sheetUrl, + range: 'PAY_OUT!A1', // update this range of cells + valueInputOption: 'USER_ENTERED', + requestBody: resource, + }, + {}, + ); + + return data; } -export async function fillPayInSheet():Promise { +export async function fillPayInSheet(): Promise { const data = await backofficeDb.getPayin(); - - const sheets = google.sheets('v4') + + const sheets = google.sheets('v4'); const jwtClient = new google.auth.JWT({ - email: await getSecret(SERVICE_ACCOUNT), - key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), - scopes: [ 'https://www.googleapis.com/auth/spreadsheets' ], // read and write sheets - }) - - const values = [[ - "Payment id", - "Circle Payment id", - "Payment status", - "Payment amount", - "Fees", - "Payment creation date", - "Payment updated", - "User UID", - "User email", - "First name", - "Last name", - "Common id", - "Common name", - "Common information", - "Proposal Id", - "Funding", - "Proposal title", - "Proposal created at", - "Proposal updated at", - "Subscription Id", - "Subscription amount", - "Subscription created at", - "Subscription updated at" - ]]; + email: await getSecret(SERVICE_ACCOUNT), + key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), + scopes: ['https://www.googleapis.com/auth/spreadsheets'], // read and write sheets + }); + + const values = [ + [ + 'Payment id', + 'Circle Payment id', + 'Payment status', + 'Payment amount', + 'Fees', + 'Payment creation date', + 'Payment updated', + 'User UID', + 'User email', + 'First name', + 'Last name', + 'Common id', + 'Common name', + 'Common information', + 'Proposal Id', + 'Funding', + 'Proposal title', + 'Proposal created at', + 'Proposal updated at', + 'Subscription Id', + 'Subscription amount', + 'Subscription created at', + 'Subscription updated at', + ], + ]; for (const key in data) { - // eslint-disable-next-line no-prototype-builtins - if (data.hasOwnProperty(key)) { - const cells = [] - - if(data[key].payment){ - cells.push(data[key].payment.id) - cells.push(data[key].payment.circlePaymentId) - cells.push(data[key].payment.status) - cells.push(data[key].payment.amount.amount/100) - if(data[key].payment.fees){ - cells.push(data[key].payment.fees.amount/100) - } else{ - cells.push("") - } - cells.push(`${date(new Date(data[key].payment.createdAt.toDate()))}`) - cells.push(`${date(new Date(data[key].payment.updatedAt.toDate()))}`) - } - else{ - cells.push("") - cells.push("") - cells.push("") - cells.push("") - cells.push("") - cells.push("") - } + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty(key)) { + const cells = []; + + if (data[key].payment) { + cells.push(data[key].payment.id); + cells.push(data[key].payment.circlePaymentId); + cells.push(data[key].payment.status); + cells.push(data[key].payment.amount.amount / 100); + if (data[key].payment.fees) { + cells.push(data[key].payment.fees.amount / 100); + } else { + cells.push(''); + } + cells.push(`${date(new Date(data[key].payment.createdAt.toDate()))}`); + cells.push(`${date(new Date(data[key].payment.updatedAt.toDate()))}`); + } else { + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + } - if(data[key].proposal){ - - cells.push(data[key].proposal.proposerId) - } else { - cells.push("") - } + if (data[key].proposal) { + cells.push(data[key].proposal.proposerId); + } else { + cells.push(''); + } - if(data[key].user){ - cells.push(data[key].user.email) - cells.push(data[key].user.firstName) - cells.push(data[key].user.lastName) - } else { - cells.push("") - cells.push("") - cells.push("") - } + if (data[key].user) { + cells.push(data[key].user.email); + cells.push(data[key].user.firstName); + cells.push(data[key].user.lastName); + } else { + cells.push(''); + cells.push(''); + cells.push(''); + } - if(data[key].common){ - cells.push(data[key].common.id) - cells.push(data[key].common.name) - cells.push(data[key].common.metadata.contributionType) - } else { - cells.push("") - cells.push("") - cells.push("") - } + if (data[key].common) { + cells.push(data[key].common.id); + cells.push(data[key].common.name); + cells.push(data[key].common.metadata.contributionType); + } else { + cells.push(''); + cells.push(''); + cells.push(''); + } - if(data[key].proposal){ - cells.push(data[key].proposal.id) - cells.push(data[key].proposal.join.funding/100) - cells.push(data[key].proposal.description.title) - cells.push(`${date(new Date(data[key].proposal.createdAt.toDate()))}`) - cells.push(`${date(new Date(data[key].proposal.updatedAt.toDate()))}`) - } else { - cells.push("") - cells.push("") - cells.push("") - cells.push("") - cells.push("") - cells.push("") - } + if (data[key].proposal) { + cells.push(data[key].proposal.id); + cells.push(data[key].proposal.join.funding / 100); + cells.push(data[key].proposal.description.title); + cells.push(`${date(new Date(data[key].proposal.createdAt.toDate()))}`); + cells.push(`${date(new Date(data[key].proposal.updatedAt.toDate()))}`); + } else { + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + } - if(data[key].subscription){ - cells.push(data[key].subscription.id) - cells.push(data[key].subscription.amount/100) - cells.push(`${date(new Date(data[key].subscription.createdAt.toDate()))}`) - cells.push(`${date(new Date(data[key].subscription.updatedAt.toDate()))}`) - } else { - cells.push("") - cells.push("") - cells.push("") - cells.push("") - cells.push("") - } - - - - - - - values.push(cells) + if (data[key].subscription) { + cells.push(data[key].subscription.id); + cells.push(data[key].subscription.amount / 100); + cells.push( + `${date(new Date(data[key].subscription.createdAt.toDate()))}`, + ); + cells.push( + `${date(new Date(data[key].subscription.updatedAt.toDate()))}`, + ); + } else { + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); + cells.push(''); } + + values.push(cells); + } } const resource = { values, }; - const jwtAuthPromise = jwtClient.authorize() - await jwtAuthPromise - await sheets.spreadsheets.values.update({ + const jwtAuthPromise = jwtClient.authorize(); + await jwtAuthPromise; + await sheets.spreadsheets.values.update( + { auth: jwtClient, spreadsheetId: env.backoffice.sheetUrl, - range: 'PAY_IN!A1', // update this range of cells + range: 'PAY_IN!A1', // update this range of cells valueInputOption: 'USER_ENTERED', - requestBody: resource - }, {}) + requestBody: resource, + }, + {}, + ); return data; } -export async function filCircleBalanceSheet():Promise { - const sheets = google.sheets('v4') +export async function filCircleBalanceSheet(): Promise { + const sheets = google.sheets('v4'); const jwtClient = new google.auth.JWT({ - email: await getSecret(SERVICE_ACCOUNT), - key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), - scopes: [ 'https://www.googleapis.com/auth/spreadsheets' ], // read and write sheets - }) + email: await getSecret(SERVICE_ACCOUNT), + key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), + scopes: ['https://www.googleapis.com/auth/spreadsheets'], // read and write sheets + }); const data = (await backofficeDb.getCircleBalance()).data.data; - const values = [[ - 'Account', - 'Available', - 'Unsettled', - 'Date', - ]]; - for (let i = 0; i { - const sheets = google.sheets('v4') - const jwtClient = new google.auth.JWT({ - email: await getSecret(SERVICE_ACCOUNT), - key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), - scopes: [ 'https://www.googleapis.com/auth/spreadsheets' ], // read and write sheets - }) - - const data = await backofficeDb.getCommonBalance(); - const values = [[ - 'Common id', - 'Common name', - 'Balance', - 'Date' - ]]; - for (const key in data) { - // eslint-disable-next-line no-prototype-builtins - if (data.hasOwnProperty(key)) { - const cells = [] - cells.push(data[key].id) - cells.push(data[key].name) - cells.push(data[key].balance/100) - cells.push(date()) - values.push(cells) - } +export async function fillCommonBalanceSheet(): Promise { + const sheets = google.sheets('v4'); + const jwtClient = new google.auth.JWT({ + email: await getSecret(SERVICE_ACCOUNT), + key: await getSecret(SERVICE_ACCOUNT_PRIVATE_KEY), + scopes: ['https://www.googleapis.com/auth/spreadsheets'], // read and write sheets + }); - } + const data = await backofficeDb.getCommonBalance(); + const values = [['Common id', 'Common name', 'Balance', 'Date']]; + for (const key in data) { + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty(key)) { + const cells = []; + cells.push(data[key].id); + cells.push(data[key].name); + cells.push(data[key].balance / 100); + cells.push(date()); + values.push(cells); + } + } + + const resource = { + values, + }; - const resource = { - values, - }; - - const jwtAuthPromise = jwtClient.authorize() - await jwtAuthPromise - await sheets.spreadsheets.values.update({ - auth: jwtClient, - spreadsheetId: env.backoffice.sheetUrl, - range: 'COMMON_BALANCES!A1', // update this range of cells - valueInputOption: 'USER_ENTERED', - requestBody: resource - }, {}) - - return data; -} \ No newline at end of file + const jwtAuthPromise = jwtClient.authorize(); + await jwtAuthPromise; + await sheets.spreadsheets.values.update( + { + auth: jwtClient, + spreadsheetId: env.backoffice.sheetUrl, + range: 'COMMON_BALANCES!A1', // update this range of cells + valueInputOption: 'USER_ENTERED', + requestBody: resource, + }, + {}, + ); + + return data; +} diff --git a/functions/src/backoffice/database/getCircleBalance.ts b/functions/src/backoffice/database/getCircleBalance.ts index 6eaa0864..eec91c6d 100644 --- a/functions/src/backoffice/database/getCircleBalance.ts +++ b/functions/src/backoffice/database/getCircleBalance.ts @@ -1,20 +1,25 @@ import axios from 'axios'; -import { ICircleBalance } from '../types'; -import { circlePayApi } from '../../settings'; -import { externalRequestExecutor } from '../../util'; -import { ErrorCodes } from '../../constants'; -import { getCircleHeaders } from '../../circlepay/index'; +import {ICircleBalance} from '../types'; +import {circlePayApi} from '../../settings'; +import {externalRequestExecutor} from '../../util'; +import {ErrorCodes} from '../../constants'; +import {getCircleHeaders} from '../../circlepay/index'; - -export const getCircleBalance = async() : Promise => { +export const getCircleBalance = async (): Promise => { const headers = await getCircleHeaders(); - return await externalRequestExecutor(async () => { - return await axios.get(`${circlePayApi}/balances`, headers) - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Call to CirclePay failed. Please try again later and if the issue persist contact us.' - }); -} - \ No newline at end of file + return await externalRequestExecutor( + async () => { + return await axios.get( + `${circlePayApi}/balances`, + headers, + ); + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Call to CirclePay failed. Please try again later and if the issue persist contact us.', + }, + ); +}; diff --git a/functions/src/backoffice/database/getCommonBalance.ts b/functions/src/backoffice/database/getCommonBalance.ts index fa15b7f6..94c57dd2 100644 --- a/functions/src/backoffice/database/getCommonBalance.ts +++ b/functions/src/backoffice/database/getCommonBalance.ts @@ -1,8 +1,9 @@ -import { CommonCollection } from './index'; +import {CommonCollection} from './index'; -export async function getCommonBalance():Promise { - const commonCollectionQuery: any = CommonCollection; +export async function getCommonBalance(): Promise { + const commonCollectionQuery: any = CommonCollection; - return (await commonCollectionQuery.get()).docs - .map(common => common.data()); -} \ No newline at end of file + return (await commonCollectionQuery.get()).docs.map((common) => + common.data(), + ); +} diff --git a/functions/src/backoffice/database/getPayin.ts b/functions/src/backoffice/database/getPayin.ts index 50da9098..642e9538 100644 --- a/functions/src/backoffice/database/getPayin.ts +++ b/functions/src/backoffice/database/getPayin.ts @@ -1,79 +1,78 @@ -import { PaymentsCollection, ProposalsCollection, CommonCollection, UsersCollection, SubscriptionsCollection } from './index'; - -export async function getPayin():Promise { - - - - const payments = (await PaymentsCollection - .orderBy("createdAt", "asc") - .where("status", "in", ['paid', 'confirmed']) - .get() - ).docs.map(p => p.data()); - - - let key = 0; - const payInCollection = {} - - for (const property in payments) { - - const payment = payments[property]; - - if(!payment.proposalId && !payment.subscriptionId) continue; - - - const proposalsQuery: any = ProposalsCollection; - proposalsQuery.orderBy("createdAt", "asc") - - const subscriptionsQuery: any = SubscriptionsCollection; - subscriptionsQuery.orderBy("createdAt", "asc") - - let proposal, subscription; - if(payment.proposalId){ - // eslint-disable-next-line no-await-in-loop - proposal = (await proposalsQuery.doc(payment.proposalId).get()) - } - - - const proposalData = proposal.data() - - if(proposalData){ - // eslint-disable-next-line no-await-in-loop - payInCollection[key] = { payment: payment, proposal: proposalData} - - if(payment.subscriptionId){ - // eslint-disable-next-line no-await-in-loop - subscription = (await subscriptionsQuery.doc(payment.subscriptionId).get()) - const subscriptionData = subscription.data() - payInCollection[key] = { ...payInCollection[key], subscription: subscriptionData} - - } - - - const commonQuery: any = CommonCollection; - - //eslint-disable-next-line no-await-in-loop - const dao = await commonQuery.doc(proposalData.commonId).get() - const daoData = dao.data() - if(daoData){ - payInCollection[key] = {...payInCollection[key], common: daoData} - } - - - const usersQuery: any = UsersCollection; - usersQuery.orderBy("createdAt", "asc") - - //eslint-disable-next-line no-await-in-loop - const user = await (usersQuery.doc(proposalData.proposerId).get()) - - const userData = user.data() - if(userData){ - payInCollection[key] = {...payInCollection[key], user: userData} - } - - - key++; - } +import { + PaymentsCollection, + ProposalsCollection, + CommonCollection, + UsersCollection, + SubscriptionsCollection, +} from './index'; + +export async function getPayin(): Promise { + const payments = ( + await PaymentsCollection.orderBy('createdAt', 'asc') + .where('status', 'in', ['paid', 'confirmed']) + .get() + ).docs.map((p) => p.data()); + + let key = 0; + const payInCollection = {}; + + for (const property in payments) { + const payment = payments[property]; + + if (!payment.proposalId && !payment.subscriptionId) continue; + + const proposalsQuery: any = ProposalsCollection; + proposalsQuery.orderBy('createdAt', 'asc'); + + const subscriptionsQuery: any = SubscriptionsCollection; + subscriptionsQuery.orderBy('createdAt', 'asc'); + + let proposal, subscription; + if (payment.proposalId) { + // eslint-disable-next-line no-await-in-loop + proposal = await proposalsQuery.doc(payment.proposalId).get(); + } + const proposalData = proposal.data(); + + if (proposalData) { + // eslint-disable-next-line no-await-in-loop + payInCollection[key] = {payment: payment, proposal: proposalData}; + + if (payment.subscriptionId) { + // eslint-disable-next-line no-await-in-loop + subscription = await subscriptionsQuery + .doc(payment.subscriptionId) + .get(); + const subscriptionData = subscription.data(); + payInCollection[key] = { + ...payInCollection[key], + subscription: subscriptionData, + }; + } + + const commonQuery: any = CommonCollection; + + //eslint-disable-next-line no-await-in-loop + const dao = await commonQuery.doc(proposalData.commonId).get(); + const daoData = dao.data(); + if (daoData) { + payInCollection[key] = {...payInCollection[key], common: daoData}; + } + + const usersQuery: any = UsersCollection; + usersQuery.orderBy('createdAt', 'asc'); + + //eslint-disable-next-line no-await-in-loop + const user = await usersQuery.doc(proposalData.proposerId).get(); + + const userData = user.data(); + if (userData) { + payInCollection[key] = {...payInCollection[key], user: userData}; + } + + key++; } - return payInCollection -} \ No newline at end of file + } + return payInCollection; +} diff --git a/functions/src/backoffice/database/getPayout.ts b/functions/src/backoffice/database/getPayout.ts index acecc140..42b9ed47 100644 --- a/functions/src/backoffice/database/getPayout.ts +++ b/functions/src/backoffice/database/getPayout.ts @@ -1,63 +1,60 @@ -import { ProposalsCollection, UsersCollection, CommonCollection, PayoutsCollection } from './index'; - -export async function getPayout():Promise { - - const proposals: any = (await ProposalsCollection - .orderBy("createdAt", "asc") - .get() - ).docs.map(p => p.data()); - - - - const payOutCollection = {}; - let key = 0; - for (const property in proposals) { - - const proposal = proposals[property]; - - if(!proposal.fundingRequest) continue; - if(proposal.fundingRequest.amount === 0) continue; - if(proposal.state !== "passed") continue; - if(proposal.type !== "fundingRequest") continue; - if(!proposal.proposerId) continue; - - payOutCollection[key] = { proposal: proposal} - - const usersQuery: any = UsersCollection; - - // eslint-disable-next-line no-await-in-loop - const user = await usersQuery.doc(proposal.proposerId).get() - const userData = user.data() - if(userData){ - payOutCollection[key] = {...payOutCollection[key], user: userData} - } - - const commonsQuery: any = CommonCollection; - //eslint-disable-next-line no-await-in-loop - const dao = await commonsQuery.doc(proposal.commonId).get() - const daoData = dao.data() - if(daoData){ - payOutCollection[key] = {...payOutCollection[key], common: daoData} - } - - const payoutsQuery: any = PayoutsCollection; - //eslint-disable-next-line no-await-in-loop - const payout = (await payoutsQuery - .where("proposalId", "==", proposal.id) - .get()).docs.map(p => p.data()); - - if(payout){ - const payoutData = payout[0] - if(payoutData){ - payOutCollection[key] = {...payOutCollection[key], payout: payoutData} - } - } - - - - key++; +import { + ProposalsCollection, + UsersCollection, + CommonCollection, + PayoutsCollection, +} from './index'; + +export async function getPayout(): Promise { + const proposals: any = ( + await ProposalsCollection.orderBy('createdAt', 'asc').get() + ).docs.map((p) => p.data()); + + const payOutCollection = {}; + let key = 0; + for (const property in proposals) { + const proposal = proposals[property]; + + if (!proposal.fundingRequest) continue; + if (proposal.fundingRequest.amount === 0) continue; + if (proposal.state !== 'passed') continue; + if (proposal.type !== 'fundingRequest') continue; + if (!proposal.proposerId) continue; + + payOutCollection[key] = {proposal: proposal}; + + const usersQuery: any = UsersCollection; + + // eslint-disable-next-line no-await-in-loop + const user = await usersQuery.doc(proposal.proposerId).get(); + const userData = user.data(); + if (userData) { + payOutCollection[key] = {...payOutCollection[key], user: userData}; } - - return payOutCollection -} \ No newline at end of file + const commonsQuery: any = CommonCollection; + //eslint-disable-next-line no-await-in-loop + const dao = await commonsQuery.doc(proposal.commonId).get(); + const daoData = dao.data(); + if (daoData) { + payOutCollection[key] = {...payOutCollection[key], common: daoData}; + } + + const payoutsQuery: any = PayoutsCollection; + //eslint-disable-next-line no-await-in-loop + const payout = ( + await payoutsQuery.where('proposalId', '==', proposal.id).get() + ).docs.map((p) => p.data()); + + if (payout) { + const payoutData = payout[0]; + if (payoutData) { + payOutCollection[key] = {...payOutCollection[key], payout: payoutData}; + } + } + + key++; + } + + return payOutCollection; +} diff --git a/functions/src/backoffice/database/index.ts b/functions/src/backoffice/database/index.ts index e0313918..63e3a5f8 100644 --- a/functions/src/backoffice/database/index.ts +++ b/functions/src/backoffice/database/index.ts @@ -1,84 +1,110 @@ -import { IPaymentEntity } from '../../circlepay/payments/types'; -import { ICommonEntity } from '../../common/types'; -import { Collections } from '../../constants'; -import { IProposalEntity } from '../../proposals/proposalTypes'; -import { db } from '../../settings'; -import { IUserEntity } from '../../util/types'; -import { getCircleBalance } from './getCircleBalance'; -import { getCommonBalance } from './getCommonBalance'; -import { getPayout } from './getPayout'; -import { getPayin } from './getPayin'; -import { IPayoutEntity } from '../../circlepay/payouts/types'; -import { ISubscriptionEntity } from '../../subscriptions/types'; +import {IPaymentEntity} from '../../circlepay/payments/types'; +import {ICommonEntity} from '../../common/types'; +import {Collections} from '../../constants'; +import {IProposalEntity} from '../../proposals/proposalTypes'; +import {db} from '../../settings'; +import {IUserEntity} from '../../util/types'; +import {getCircleBalance} from './getCircleBalance'; +import {getCommonBalance} from './getCommonBalance'; +import {getPayout} from './getPayout'; +import {getPayin} from './getPayin'; +import {IPayoutEntity} from '../../circlepay/payouts/types'; +import {ISubscriptionEntity} from '../../subscriptions/types'; - -export const SubscriptionsCollection = db.collection(Collections.Subscriptions) -.withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): ISubscriptionEntity { - return snapshot.data() as ISubscriptionEntity; +export const SubscriptionsCollection = db + .collection(Collections.Subscriptions) + .withConverter({ + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): ISubscriptionEntity { + return snapshot.data() as ISubscriptionEntity; }, - toFirestore(object: ISubscriptionEntity | Partial): FirebaseFirestore.DocumentData { - return object; - } -}); - -export const PayoutsCollection = db.collection(Collections.Payouts) -.withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IPayoutEntity { - return snapshot.data() as IPayoutEntity; + toFirestore( + object: ISubscriptionEntity | Partial, + ): FirebaseFirestore.DocumentData { + return object; }, - toFirestore(object: IPayoutEntity | Partial): FirebaseFirestore.DocumentData { - return object; - } -}); + }); -export const CommonCollection = db.collection(Collections.Commons) -.withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): ICommonEntity { - return snapshot.data() as ICommonEntity; +export const PayoutsCollection = db + .collection(Collections.Payouts) + .withConverter({ + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IPayoutEntity { + return snapshot.data() as IPayoutEntity; + }, + toFirestore( + object: IPayoutEntity | Partial, + ): FirebaseFirestore.DocumentData { + return object; }, - toFirestore(object: ICommonEntity | Partial): FirebaseFirestore.DocumentData { - return object; - } -}); + }); +export const CommonCollection = db + .collection(Collections.Commons) + .withConverter({ + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): ICommonEntity { + return snapshot.data() as ICommonEntity; + }, + toFirestore( + object: ICommonEntity | Partial, + ): FirebaseFirestore.DocumentData { + return object; + }, + }); -export const PaymentsCollection = db.collection(Collections.Payments) +export const PaymentsCollection = db + .collection(Collections.Payments) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IPaymentEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IPaymentEntity { return snapshot.data() as IPaymentEntity; }, - toFirestore(object: IPaymentEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: IPaymentEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); - -export const ProposalsCollection = db.collection(Collections.Proposals) -.withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IProposalEntity { - return snapshot.data() as IProposalEntity; +export const ProposalsCollection = db + .collection(Collections.Proposals) + .withConverter({ + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IProposalEntity { + return snapshot.data() as IProposalEntity; }, - toFirestore(object: IProposalEntity | Partial): FirebaseFirestore.DocumentData { - return object; - } -}); - -export const UsersCollection = db.collection(Collections.Users) -.withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IUserEntity { - return snapshot.data() as IUserEntity; + toFirestore( + object: IProposalEntity | Partial, + ): FirebaseFirestore.DocumentData { + return object; }, - toFirestore(object: IUserEntity | Partial): FirebaseFirestore.DocumentData { - return object; - } -}); + }); +export const UsersCollection = db + .collection(Collections.Users) + .withConverter({ + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IUserEntity { + return snapshot.data() as IUserEntity; + }, + toFirestore( + object: IUserEntity | Partial, + ): FirebaseFirestore.DocumentData { + return object; + }, + }); export const backofficeDb = { - getCircleBalance, - getCommonBalance, - getPayout, - getPayin - }; \ No newline at end of file + getCircleBalance, + getCommonBalance, + getPayout, + getPayin, +}; diff --git a/functions/src/backoffice/helper/index.ts b/functions/src/backoffice/helper/index.ts index 88177b5e..1530c95d 100644 --- a/functions/src/backoffice/helper/index.ts +++ b/functions/src/backoffice/helper/index.ts @@ -1,24 +1,22 @@ -export function date(pastDate: Date = new Date()):string { - - - const addZero = function(x: string | number, n: number) { - while (x.toString().length < n) { - x = "0" + x; - } - return x; +export function date(pastDate: Date = new Date()): string { + const addZero = function (x: string | number, n: number) { + while (x.toString().length < n) { + x = '0' + x; } + return x; + }; - let d - if(pastDate){ - d = pastDate; - } else { - d = new Date(); - } - const date = d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate(); - const h = addZero(d.getHours(), 2); - const m = addZero(d.getMinutes(), 2); - const s = addZero(d.getSeconds(), 2); - const fullDate = date + ' ' + h + ":" + m + ":" + s; + let d; + if (pastDate) { + d = pastDate; + } else { + d = new Date(); + } + const date = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(); + const h = addZero(d.getHours(), 2); + const m = addZero(d.getMinutes(), 2); + const s = addZero(d.getSeconds(), 2); + const fullDate = date + ' ' + h + ':' + m + ':' + s; - return fullDate; -} \ No newline at end of file + return fullDate; +} diff --git a/functions/src/backoffice/index.ts b/functions/src/backoffice/index.ts index a9cba450..ab5ab7ad 100644 --- a/functions/src/backoffice/index.ts +++ b/functions/src/backoffice/index.ts @@ -1,12 +1,16 @@ import * as functions from 'firebase-functions'; -import { commonApp, commonRouter } from '../util'; -import { responseExecutor } from '../util/responseExecutor'; -import {fillPayInSheet, fillPayOutSheet, filCircleBalanceSheet, fillCommonBalanceSheet} from './business' - +import {commonApp, commonRouter} from '../util'; +import {responseExecutor} from '../util/responseExecutor'; +import { + fillPayInSheet, + fillPayOutSheet, + filCircleBalanceSheet, + fillCommonBalanceSheet, +} from './business'; const runtimeOptions = { - timeoutSeconds: 540 // Maximum time 9 mins + timeoutSeconds: 540, // Maximum time 9 mins }; const router = commonRouter(); @@ -14,62 +18,66 @@ const router = commonRouter(); router.get('/payout', async (req, res, next) => { await responseExecutor( async () => { - return fillPayOutSheet() - }, { + return fillPayOutSheet(); + }, + { req, res, next, - successMessage: `Fetch PAYOUT succesfully!` - } + successMessage: `Fetch PAYOUT succesfully!`, + }, ); }); router.get('/payin', async (req, res, next) => { await responseExecutor( async () => { - return fillPayInSheet() - }, { + return fillPayInSheet(); + }, + { req, res, next, - successMessage: `Fetch PAYIN succesfully!` - } + successMessage: `Fetch PAYIN succesfully!`, + }, ); }); router.get('/commonbalance', async (req, res, next) => { await responseExecutor( async () => { - return fillCommonBalanceSheet() - }, { + return fillCommonBalanceSheet(); + }, + { req, res, next, - successMessage: `Fetch BALANCE succesfully!` - } + successMessage: `Fetch BALANCE succesfully!`, + }, ); }); router.get('/circlebalance', async (req, res, next) => { await responseExecutor( async () => { - return filCircleBalanceSheet() - }, { + return filCircleBalanceSheet(); + }, + { req, res, next, - successMessage: `Fetch BALANCE succesfully!` - } + successMessage: `Fetch BALANCE succesfully!`, + }, ); }); -export const backofficeApp = functions - .runWith(runtimeOptions) - .https.onRequest(commonApp(router, { - unauthenticatedRoutes:[ - '/payin', - '/payout', - '/circlebalance', - '/commonbalance' - ] - })); \ No newline at end of file +export const backofficeApp = functions.runWith(runtimeOptions).https.onRequest( + commonApp(router, { + unauthenticatedRoutes: [ + '/payin', + '/payout', + '/circlebalance', + '/commonbalance', + ], + }), +); diff --git a/functions/src/backoffice/types.ts b/functions/src/backoffice/types.ts index 93d05ec0..18f4f588 100644 --- a/functions/src/backoffice/types.ts +++ b/functions/src/backoffice/types.ts @@ -1,9 +1,8 @@ - export interface ICircleBalanceBase { - data: { - available: any; - unsettled: any; - } + data: { + available: any; + unsettled: any; + }; } -export type ICircleBalance = ICircleBalanceBase //ISubscriptionPayment | IProposalPayment; \ No newline at end of file +export type ICircleBalance = ICircleBalanceBase; //ISubscriptionPayment | IProposalPayment; diff --git a/functions/src/circlepay/backAccounts/bussiness/createBankAccount.ts b/functions/src/circlepay/backAccounts/bussiness/createBankAccount.ts index d6774d73..1a115df1 100644 --- a/functions/src/circlepay/backAccounts/bussiness/createBankAccount.ts +++ b/functions/src/circlepay/backAccounts/bussiness/createBankAccount.ts @@ -2,19 +2,22 @@ import axios from 'axios'; import * as yup from 'yup'; import * as iban from 'ibantools'; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import { billingDetailsValidationSchema, - bankAccountValidationSchema as bankAccountValidationExternalSchema + bankAccountValidationSchema as bankAccountValidationExternalSchema, } from '../../../util/schemas'; -import { validate } from '../../../util/validate'; -import { IBankAccountEntity } from '../types'; -import { getCircleHeaders } from '../../index'; -import { ICircleCreateBankAccountPayload, ICircleCreateBankAccountResponse } from '../../cards/circleTypes'; -import { externalRequestExecutor, isNullOrUndefined } from '../../../util'; -import { circlePayApi } from '../../../settings'; -import { ErrorCodes } from '../../../constants'; -import { bankAccountDb } from '../database'; +import {validate} from '../../../util/validate'; +import {IBankAccountEntity} from '../types'; +import {getCircleHeaders} from '../../index'; +import { + ICircleCreateBankAccountPayload, + ICircleCreateBankAccountResponse, +} from '../../cards/circleTypes'; +import {externalRequestExecutor, isNullOrUndefined} from '../../../util'; +import {circlePayApi} from '../../../settings'; +import {ErrorCodes} from '../../../constants'; +import {bankAccountDb} from '../database'; const bankAccountValidationSchema = yup.object({ iban: yup @@ -27,54 +30,59 @@ const bankAccountValidationSchema = yup.object({ is: (accountNumber) => isNullOrUndefined(accountNumber), then: yup .string() - .required('The IBAN is required field when there are not account number and routing number provided'), - otherwise: yup - .string() - .test({ - test: (value) => isNullOrUndefined(value), - message: 'Cannot have both account number and IBAN' - }) + .required( + 'The IBAN is required field when there are not account number and routing number provided', + ), + otherwise: yup.string().test({ + test: (value) => isNullOrUndefined(value), + message: 'Cannot have both account number and IBAN', + }), }) .test({ name: 'Validate IBAN', message: 'Please provide valid IBAN', test: (value): boolean => { - return isNullOrUndefined(value) || - iban.isValidIBAN(value); - } + return isNullOrUndefined(value) || iban.isValidIBAN(value); + }, }), - accountNumber: yup.string() - .when('billingDetails.country', { - is: (country: string) => country?.toLowerCase() === 'us', - then: yup - .string() - .required('The account number is required for US transfers.') - }), + accountNumber: yup.string().when('billingDetails.country', { + is: (country: string) => country?.toLowerCase() === 'us', + then: yup + .string() + .required('The account number is required for US transfers.'), + }), - routingNumber: yup.string() + routingNumber: yup + .string() .when('iban', { is: (iban: string) => isNullOrUndefined(iban), then: yup .string() - .required('The routing number is required when the IBAN is not provided') + .required( + 'The routing number is required when the IBAN is not provided', + ), }) .when('billingDetails.country', { is: (country: string) => country?.toLowerCase() === 'us', then: yup .string() - .required('The routing number is required for US transfers.') + .required('The routing number is required for US transfers.'), }), billingDetails: billingDetailsValidationSchema, - bankAddress: bankAccountValidationExternalSchema + bankAddress: bankAccountValidationExternalSchema, }); -type CreateBankAccountPayload = yup.InferType; +type CreateBankAccountPayload = yup.InferType< + typeof bankAccountValidationSchema +>; const normalizeIban = (iban: string): string => iban.toUpperCase().trim(); -export const createBankAccount = async (payload: CreateBankAccountPayload): Promise => { +export const createBankAccount = async ( + payload: CreateBankAccountPayload, +): Promise => { // Validate the provided data await validate(payload, bankAccountValidationSchema); @@ -83,39 +91,50 @@ export const createBankAccount = async (payload: CreateBankAccountPayload): Prom const data: ICircleCreateBankAccountPayload = { idempotencyKey: v4(), - ...(payload.iban ? { - iban: normalizeIban(payload.iban), - } : { - routingNumber: payload.routingNumber, - accountNumber: payload.accountNumber - }), + ...(payload.iban + ? { + iban: normalizeIban(payload.iban), + } + : { + routingNumber: payload.routingNumber, + accountNumber: payload.accountNumber, + }), billingDetails: payload.billingDetails as any, bankAddress: { - ...payload.bankAddress as any, - bankName: payload.bankAddress.name - } + ...(payload.bankAddress as any), + bankName: payload.bankAddress.name, + }, }; // Create the account on Circle - const { data: response } = await externalRequestExecutor(async () => { - logger.debug('Trying to create new bank account with circle', { - data - }); - - return (await axios.post(`${circlePayApi}/banks/wires`, - data, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Cannot create the bank account, because it was rejected by Circle' - }); + const { + data: response, + } = await externalRequestExecutor( + async () => { + logger.debug('Trying to create new bank account with circle', { + data, + }); + + return ( + await axios.post( + `${circlePayApi}/banks/wires`, + data, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Cannot create the bank account, because it was rejected by Circle', + }, + ); // Check if the account exists const existingBankAccount = await bankAccountDb.get(response?.id, false); - - if(existingBankAccount) { + + if (existingBankAccount) { return existingBankAccount; } @@ -127,11 +146,11 @@ export const createBankAccount = async (payload: CreateBankAccountPayload): Prom description: response.description, bank: response.bankAddress as any, - billingDetails: response.billingDetails as any + billingDetails: response.billingDetails as any, }); // @todo Create event // Return the created bank account return bankAccount; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/backAccounts/database/addBankAccount.ts b/functions/src/circlepay/backAccounts/database/addBankAccount.ts index 719c76b2..516de561 100644 --- a/functions/src/circlepay/backAccounts/database/addBankAccount.ts +++ b/functions/src/circlepay/backAccounts/database/addBankAccount.ts @@ -1,11 +1,11 @@ -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { BaseEntityType, SharedOmit } from '../../../util/types'; +import {BaseEntityType, SharedOmit} from '../../../util/types'; -import { IBankAccountEntity } from '../types'; -import { BankAccountCollection } from './index'; +import {IBankAccountEntity} from '../types'; +import {BankAccountCollection} from './index'; /** * Prepares the passed bank account for saving and saves it. Please note that @@ -13,23 +13,23 @@ import { BankAccountCollection } from './index'; * * @param bankAccount - the bankAccount to be saved */ -export const addBankAccount = async (bankAccount: SharedOmit): Promise => { +export const addBankAccount = async ( + bankAccount: SharedOmit, +): Promise => { const bankAccountDoc: IBankAccountEntity = { id: v4(), createdAt: Timestamp.now(), updatedAt: Timestamp.now(), - ...bankAccount + ...bankAccount, }; if (process.env.NODE_ENV === 'test') { bankAccountDoc['testCreated'] = true; } - await BankAccountCollection - .doc(bankAccountDoc.id) - .set(bankAccountDoc); + await BankAccountCollection.doc(bankAccountDoc.id).set(bankAccountDoc); return bankAccountDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/backAccounts/database/bankAccountExists.ts b/functions/src/circlepay/backAccounts/database/bankAccountExists.ts index 6519e620..eef62be8 100644 --- a/functions/src/circlepay/backAccounts/database/bankAccountExists.ts +++ b/functions/src/circlepay/backAccounts/database/bankAccountExists.ts @@ -1,7 +1,6 @@ import admin from 'firebase-admin'; -import { IBankAccountEntity } from '../types'; -import { BankAccountCollection } from './index'; - +import {IBankAccountEntity} from '../types'; +import {BankAccountCollection} from './index'; import DocumentSnapshot = admin.firestore.DocumentSnapshot; @@ -16,15 +15,23 @@ interface IBankAccountExistsArgs { * * @param args - Arguments against we will check */ -export const bankAccountExists = async (args: IBankAccountExistsArgs): Promise => { +export const bankAccountExists = async ( + args: IBankAccountExistsArgs, +): Promise => { let bankAccount: DocumentSnapshot; if (args.id) { - bankAccount = (await BankAccountCollection.doc(args.id).get()) as DocumentSnapshot; + bankAccount = (await BankAccountCollection.doc( + args.id, + ).get()) as DocumentSnapshot; } if (args.iban) { - const where = await BankAccountCollection.where('iban', '==', args.iban.toUpperCase()).get(); + const where = await BankAccountCollection.where( + 'iban', + '==', + args.iban.toUpperCase(), + ).get(); if (where.empty) { return false; @@ -34,4 +41,4 @@ export const bankAccountExists = async (args: IBankAccountExistsArgs): Promise => { - if(!bankAccountId) { +export const getBankAccount = async ( + bankAccountId: string, + throwErr = true, +): Promise => { + if (!bankAccountId) { throw new ArgumentError('bankAccountId', bankAccountId); } - const bankAccount = (await BankAccountCollection - .doc(bankAccountId) - .get()).data(); + const bankAccount = ( + await BankAccountCollection.doc(bankAccountId).get() + ).data(); - if(!bankAccount && throwErr) { + if (!bankAccount && throwErr) { throw new NotFoundError(bankAccountId, 'bankAccount'); } return bankAccount; -} \ No newline at end of file +}; diff --git a/functions/src/circlepay/backAccounts/database/getBankAccounts.ts b/functions/src/circlepay/backAccounts/database/getBankAccounts.ts index 12e7b246..6eb07be7 100644 --- a/functions/src/circlepay/backAccounts/database/getBankAccounts.ts +++ b/functions/src/circlepay/backAccounts/database/getBankAccounts.ts @@ -1,5 +1,5 @@ -import { IBankAccountEntity } from '../types'; -import { BankAccountCollection } from './index'; +import {IBankAccountEntity} from '../types'; +import {BankAccountCollection} from './index'; interface IGetBankAccountOptions { /** @@ -14,13 +14,20 @@ interface IGetBankAccountOptions { * * @param options - The options for filtering the bankAccounts */ -export const getBankAccounts = async (options: IGetBankAccountOptions): Promise => { +export const getBankAccounts = async ( + options: IGetBankAccountOptions, +): Promise => { let bankAccountsQuery: any = BankAccountCollection; if (options.fingerprint) { - bankAccountsQuery = bankAccountsQuery.where('circleFingerprint', '==', options.fingerprint); + bankAccountsQuery = bankAccountsQuery.where( + 'circleFingerprint', + '==', + options.fingerprint, + ); } - return (await bankAccountsQuery.get()).docs - .map(bankAccount => bankAccount.data()); -}; \ No newline at end of file + return (await bankAccountsQuery.get()).docs.map((bankAccount) => + bankAccount.data(), + ); +}; diff --git a/functions/src/circlepay/backAccounts/database/index.ts b/functions/src/circlepay/backAccounts/database/index.ts index 43f9a08b..5271a2f5 100644 --- a/functions/src/circlepay/backAccounts/database/index.ts +++ b/functions/src/circlepay/backAccounts/database/index.ts @@ -1,22 +1,26 @@ -import { db } from '../../../util'; -import { Collections } from '../../../constants'; -import { IBankAccountEntity } from '../types'; -import { addBankAccount } from './addBankAccount'; -import { getBankAccounts } from './getBankAccounts'; -import { bankAccountExists } from './bankAccountExists'; -import { getBankAccount } from './getBankAccount'; -import { updateBankAccountInDatabase } from './updateBankAccount'; +import {db} from '../../../util'; +import {Collections} from '../../../constants'; +import {IBankAccountEntity} from '../types'; +import {addBankAccount} from './addBankAccount'; +import {getBankAccounts} from './getBankAccounts'; +import {bankAccountExists} from './bankAccountExists'; +import {getBankAccount} from './getBankAccount'; +import {updateBankAccountInDatabase} from './updateBankAccount'; - -export const BankAccountCollection = db.collection(Collections.BankAccounts) +export const BankAccountCollection = db + .collection(Collections.BankAccounts) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IBankAccountEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IBankAccountEntity { return snapshot.data() as IBankAccountEntity; }, - toFirestore(object: IBankAccountEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: IBankAccountEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); export const bankAccountDb = { @@ -24,5 +28,5 @@ export const bankAccountDb = { get: getBankAccount, getMany: getBankAccounts, exists: bankAccountExists, - update: updateBankAccountInDatabase -}; \ No newline at end of file + update: updateBankAccountInDatabase, +}; diff --git a/functions/src/circlepay/backAccounts/database/updateBankAccount.ts b/functions/src/circlepay/backAccounts/database/updateBankAccount.ts index c830b493..e699063a 100644 --- a/functions/src/circlepay/backAccounts/database/updateBankAccount.ts +++ b/functions/src/circlepay/backAccounts/database/updateBankAccount.ts @@ -1,8 +1,7 @@ -import { firestore } from 'firebase-admin'; - -import { IBankAccountEntity } from '../types'; -import { BankAccountCollection } from './index'; +import {firestore} from 'firebase-admin'; +import {IBankAccountEntity} from '../types'; +import {BankAccountCollection} from './index'; /** * Updates the passed entity in the database. Reference @@ -13,20 +12,20 @@ import { BankAccountCollection } from './index'; * * @returns - The updated bank account entity */ -export const updateBankAccountInDatabase = async (bankAccount: IBankAccountEntity): Promise => { +export const updateBankAccountInDatabase = async ( + bankAccount: IBankAccountEntity, +): Promise => { const bankAccountDoc = { ...bankAccount, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; logger.debug('Updating bank account', { - bankAccount + bankAccount, }); - await BankAccountCollection - .doc(bankAccountDoc.id) - .update(bankAccountDoc); + await BankAccountCollection.doc(bankAccountDoc.id).update(bankAccountDoc); return bankAccountDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/backAccounts/types.ts b/functions/src/circlepay/backAccounts/types.ts index 07f8765f..f19d083f 100644 --- a/functions/src/circlepay/backAccounts/types.ts +++ b/functions/src/circlepay/backAccounts/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../../util/types'; +import {IBaseEntity} from '../../util/types'; export interface IBankAccountEntity extends IBaseEntity { /** @@ -102,4 +102,4 @@ export interface IBankAccountBank { * Line two of the street address. */ line2?: string; -} \ No newline at end of file +} diff --git a/functions/src/circlepay/cards/business/createCard.ts b/functions/src/circlepay/cards/business/createCard.ts index d0c3e41c..18d0ed91 100644 --- a/functions/src/circlepay/cards/business/createCard.ts +++ b/functions/src/circlepay/cards/business/createCard.ts @@ -1,55 +1,41 @@ import axios from 'axios'; import * as yup from 'yup'; -import { ErrorCodes } from '../../../constants'; -import { circlePayApi } from '../../../settings'; -import { validate } from '../../../util/validate'; -import { externalRequestExecutor } from '../../../util'; -import { billingDetailsValidationSchema } from '../../../util/schemas'; - -import { ICircleCreateCardPayload, ICircleCreateCardResponse } from '../../types'; -import { getCircleHeaders } from '../../index'; -import { ICardEntity } from '../types'; -import { userDb } from '../../../users/database'; -import { cardDb } from '../database'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; -import { pollCard } from './pollCard'; +import {ErrorCodes} from '../../../constants'; +import {circlePayApi} from '../../../settings'; +import {validate} from '../../../util/validate'; +import {externalRequestExecutor} from '../../../util'; +import {billingDetailsValidationSchema} from '../../../util/schemas'; + +import {ICircleCreateCardPayload, ICircleCreateCardResponse} from '../../types'; +import {getCircleHeaders} from '../../index'; +import {ICardEntity} from '../types'; +import {userDb} from '../../../users/database'; +import {cardDb} from '../database'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; +import {pollCard} from './pollCard'; const createCardValidationSchema = yup.object({ - ownerId: yup.string() - .required(), + ownerId: yup.string().required(), - billingDetails: billingDetailsValidationSchema - .required(), + billingDetails: billingDetailsValidationSchema.required(), - keyId: yup - .string() - .required(), + keyId: yup.string().required(), - sessionId: yup - .string() - .required(), + sessionId: yup.string().required(), - ipAddress: yup - .string() - .required(), + ipAddress: yup.string().required(), - encryptedData: yup - .string() - .required(), + encryptedData: yup.string().required(), - expMonth: yup - .number() - .min(1) - .max(12) - .required(), + expMonth: yup.number().min(1).max(12).required(), expYear: yup .number() .min(new Date().getFullYear()) .max(new Date().getFullYear() + 50) - .required() + .required(), }); type CreateCardPayload = yup.InferType; @@ -59,7 +45,9 @@ type CreateCardPayload = yup.InferType; * * @param payload */ -export const createCard = async (payload: CreateCardPayload): Promise => { +export const createCard = async ( + payload: CreateCardPayload, +): Promise => { // Validate the passed data await validate(payload, createCardValidationSchema); @@ -78,30 +66,38 @@ export const createCard = async (payload: CreateCardPayload): Promise(async () => { - logger.debug('Trying to create new card with circle', { - data - }); - - return (await axios.post(`${circlePayApi}/cards`, - data, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Cannot create the card, because it was rejected by Circle' - }); + const { + data: response, + } = await externalRequestExecutor( + async () => { + logger.debug('Trying to create new card with circle', { + data, + }); + + return ( + await axios.post( + `${circlePayApi}/cards`, + data, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: 'Cannot create the card, because it was rejected by Circle', + }, + ); // Check if the use already has the same card const existingCards = await cardDb.getMany({ ownerId: user.uid, - circleCardId: response.id + circleCardId: response.id, }); if (existingCards.length) { @@ -120,7 +116,6 @@ export const createCard = async (payload: CreateCardPayload): Promise => { +export const isCardOwner = async ( + userId: string, + cardId: string, +): Promise => { return (await cardDb.get(cardId)).ownerId === userId; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/cards/business/pollCard.ts b/functions/src/circlepay/cards/business/pollCard.ts index b11a9bca..148004a6 100644 --- a/functions/src/circlepay/cards/business/pollCard.ts +++ b/functions/src/circlepay/cards/business/pollCard.ts @@ -1,11 +1,11 @@ -import { poll } from '../../../util'; -import { IPollAction, IPollValidator } from '../../../util/poll'; +import {poll} from '../../../util'; +import {IPollAction, IPollValidator} from '../../../util/poll'; -import { ArgumentError, CvvVerificationError } from '../../../util/errors'; +import {ArgumentError, CvvVerificationError} from '../../../util/errors'; -import { circleClient, ICircleCard } from '../../client'; -import { ICardEntity } from '../types'; -import { cardDb } from '../database'; +import {circleClient, ICircleCard} from '../../client'; +import {ICardEntity} from '../types'; +import {cardDb} from '../database'; interface IPollCardOptions { interval: number; @@ -20,7 +20,7 @@ const defaultCardOptions: IPollCardOptions = { maxRetries: 32, throwOnCvvFail: true, - deleteCardOnCvvFail: false + deleteCardOnCvvFail: false, }; /** @@ -29,14 +29,17 @@ const defaultCardOptions: IPollCardOptions = { * @param card - The entity of the card we want to poll * @param pollCardOptions - *Optional* Options for the card polling */ -export const pollCard = async (card: ICardEntity, pollCardOptions?: Partial): Promise => { +export const pollCard = async ( + card: ICardEntity, + pollCardOptions?: Partial, +): Promise => { if (!card) { throw new ArgumentError('card', card); } const options = { ...defaultCardOptions, - ...pollCardOptions + ...pollCardOptions, }; const pollAction: IPollAction = async () => { @@ -44,11 +47,17 @@ export const pollCard = async (card: ICardEntity, pollCardOptions?: Partial = (card) => { - return card.verification.avs !== 'pending' && - card.verification.cvv !== 'pending'; + return ( + card.verification.avs !== 'pending' && card.verification.cvv !== 'pending' + ); }; - const circleCardObj = await poll(pollAction, pollValidator, options.interval, options.maxRetries); + const circleCardObj = await poll( + pollAction, + pollValidator, + options.interval, + options.maxRetries, + ); // Update the CVV verification check of the entity if it has changed if (circleCardObj.verification.cvv !== card.verification.cvv) { @@ -56,14 +65,14 @@ export const pollCard = async (card: ICardEntity, pollCardOptions?: Partial): Promise => { +export const addCard = async ( + card: SharedOmit, +): Promise => { const cardDoc: ICardEntity = { id: v4(), createdAt: Timestamp.now(), updatedAt: Timestamp.now(), - ...card + ...card, }; if (process.env.NODE_ENV === 'test') { cardDoc['testCreated'] = true; } - await CardCollection - .doc(cardDoc.id) - .set(cardDoc); + await CardCollection.doc(cardDoc.id).set(cardDoc); return cardDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/cards/database/getCard.ts b/functions/src/circlepay/cards/database/getCard.ts index e919bf80..72eef05d 100644 --- a/functions/src/circlepay/cards/database/getCard.ts +++ b/functions/src/circlepay/cards/database/getCard.ts @@ -1,7 +1,7 @@ -import { ArgumentError, NotFoundError } from '../../../util/errors'; +import {ArgumentError, NotFoundError} from '../../../util/errors'; -import { ICardEntity } from '../types'; -import { CardCollection } from './index'; +import {ICardEntity} from '../types'; +import {CardCollection} from './index'; /** * Gets card by id @@ -20,13 +20,11 @@ export const getCard = async (cardId: string): Promise => { throw new ArgumentError('cardId', cardId); } - const card = (await CardCollection - .doc(cardId) - .get()).data(); + const card = (await CardCollection.doc(cardId).get()).data(); if (!card) { throw new NotFoundError(cardId, 'card'); } return card; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/cards/database/getCards.ts b/functions/src/circlepay/cards/database/getCards.ts index 0acca6db..b30fb42e 100644 --- a/functions/src/circlepay/cards/database/getCards.ts +++ b/functions/src/circlepay/cards/database/getCards.ts @@ -1,5 +1,5 @@ -import { ICardEntity } from '../types'; -import { CardCollection } from './index'; +import {ICardEntity} from '../types'; +import {CardCollection} from './index'; interface IGetCardOptions { /** @@ -31,7 +31,7 @@ interface IGetCardOptions { * be returned */ limit?: number; - } + }; } /** @@ -39,7 +39,9 @@ interface IGetCardOptions { * * @param options - The options for filtering the cards */ -export const getCards = async (options: IGetCardOptions): Promise => { +export const getCards = async ( + options: IGetCardOptions, +): Promise => { let cardsQuery: any = CardCollection; if (options.ownerId) { @@ -51,7 +53,7 @@ export const getCards = async (options: IGetCardOptions): Promise } if (options.sort) { - const { sort } = options; + const {sort} = options; if (sort.orderByAsc) { cardsQuery = cardsQuery.orderBy(sort.orderByAsc); @@ -64,6 +66,5 @@ export const getCards = async (options: IGetCardOptions): Promise } } - return (await cardsQuery.get()).docs - .map(card => card.data()); -}; \ No newline at end of file + return (await cardsQuery.get()).docs.map((card) => card.data()); +}; diff --git a/functions/src/circlepay/cards/database/index.ts b/functions/src/circlepay/cards/database/index.ts index 22498ed0..c07ac78a 100644 --- a/functions/src/circlepay/cards/database/index.ts +++ b/functions/src/circlepay/cards/database/index.ts @@ -1,20 +1,25 @@ -import { db } from '../../../util'; -import { Collections } from '../../../constants'; -import { ICardEntity } from '../types'; +import {db} from '../../../util'; +import {Collections} from '../../../constants'; +import {ICardEntity} from '../types'; -import { addCard } from './addCard'; -import { getCard } from './getCard'; -import { getCards } from './getCards'; -import { updateCardInDatabase } from './updateCard'; +import {addCard} from './addCard'; +import {getCard} from './getCard'; +import {getCards} from './getCards'; +import {updateCardInDatabase} from './updateCard'; -export const CardCollection = db.collection(Collections.Cards) +export const CardCollection = db + .collection(Collections.Cards) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): ICardEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): ICardEntity { return snapshot.data() as ICardEntity; }, - toFirestore(object: ICardEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: ICardEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); export const cardDb = { @@ -38,5 +43,5 @@ export const cardDb = { */ getMany: getCards, - update: updateCardInDatabase -}; \ No newline at end of file + update: updateCardInDatabase, +}; diff --git a/functions/src/circlepay/cards/database/updateCard.ts b/functions/src/circlepay/cards/database/updateCard.ts index cee2be8f..433ad90c 100644 --- a/functions/src/circlepay/cards/database/updateCard.ts +++ b/functions/src/circlepay/cards/database/updateCard.ts @@ -1,8 +1,7 @@ -import { firestore } from 'firebase-admin'; - -import { ICardEntity } from '../types'; -import { CardCollection } from './index'; +import {firestore} from 'firebase-admin'; +import {ICardEntity} from '../types'; +import {CardCollection} from './index'; /** * Updates the passed entity in the database. Reference @@ -13,16 +12,16 @@ import { CardCollection } from './index'; * * @returns - The updated card entity */ -export const updateCardInDatabase = async (card: ICardEntity): Promise => { +export const updateCardInDatabase = async ( + card: ICardEntity, +): Promise => { const cardDoc = { ...card, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; - await CardCollection - .doc(cardDoc.id) - .update(cardDoc); + await CardCollection.doc(cardDoc.id).update(cardDoc); return cardDoc; -} \ No newline at end of file +}; diff --git a/functions/src/circlepay/cards/types.ts b/functions/src/circlepay/cards/types.ts index 85012165..ac002589 100644 --- a/functions/src/circlepay/cards/types.ts +++ b/functions/src/circlepay/cards/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../../util/types'; +import {IBaseEntity} from '../../util/types'; export interface ICardEntity extends IBaseEntity { /** @@ -98,4 +98,4 @@ export interface ICardVerification { // ---- Type helpers -export type CardCvvCheck = 'pending' | 'pass' | 'fail' | 'unavailable'; \ No newline at end of file +export type CardCvvCheck = 'pending' | 'pass' | 'fail' | 'unavailable'; diff --git a/functions/src/circlepay/client/getBankAccountFromCircle.ts b/functions/src/circlepay/client/getBankAccountFromCircle.ts index c4b7ef36..0f3a9f27 100644 --- a/functions/src/circlepay/client/getBankAccountFromCircle.ts +++ b/functions/src/circlepay/client/getBankAccountFromCircle.ts @@ -1,21 +1,29 @@ import axios from 'axios'; -import { externalRequestExecutor } from '../../util'; -import { circlePayApi } from '../../settings'; -import { ErrorCodes } from '../../constants'; +import {externalRequestExecutor} from '../../util'; +import {circlePayApi} from '../../settings'; +import {ErrorCodes} from '../../constants'; -import { ICircleGetBankAccountResponse } from '../cards/circleTypes'; -import { getCircleHeaders } from '../index'; +import {ICircleGetBankAccountResponse} from '../cards/circleTypes'; +import {getCircleHeaders} from '../index'; -export const getBankAccountFromCircle = async (bankAccountId: string): Promise => { +export const getBankAccountFromCircle = async ( + bankAccountId: string, +): Promise => { const headers = await getCircleHeaders(); - return await externalRequestExecutor(async () => { - return (await axios.get(`${circlePayApi}/banks/wires/${bankAccountId}`, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Cannot get the bank account' - }); -} \ No newline at end of file + return await externalRequestExecutor( + async () => { + return ( + await axios.get( + `${circlePayApi}/banks/wires/${bankAccountId}`, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: 'Cannot get the bank account', + }, + ); +}; diff --git a/functions/src/circlepay/client/getCardFromCircle.ts b/functions/src/circlepay/client/getCardFromCircle.ts index ba7025b7..2e4b7618 100644 --- a/functions/src/circlepay/client/getCardFromCircle.ts +++ b/functions/src/circlepay/client/getCardFromCircle.ts @@ -1,15 +1,20 @@ import axios from 'axios'; -import { externalRequestExecutor } from '../../util'; -import { circlePayApi } from '../../settings'; -import { ErrorCodes } from '../../constants'; +import {externalRequestExecutor} from '../../util'; +import {circlePayApi} from '../../settings'; +import {ErrorCodes} from '../../constants'; -import { getCircleHeaders } from '../index'; +import {getCircleHeaders} from '../index'; // ---- Helper types export type CircleCardNetwork = 'VISA' | 'MASTERCARD'; -export type CircleCvvCheck = 'pending' | 'pass' | 'fail' | 'unavailable' | 'not_requested'; +export type CircleCvvCheck = + | 'pending' + | 'pass' + | 'fail' + | 'unavailable' + | 'not_requested'; // ---- Helper interfaces @@ -68,13 +73,23 @@ export interface IGetCircleCardResponse { * @param circleCardId - The ID of the card as is in circle (the ID they gave us * on card creation) */ -export const getCardFromCircle = async (circleCardId: string): Promise => { +export const getCardFromCircle = async ( + circleCardId: string, +): Promise => { const headers = await getCircleHeaders(); - return externalRequestExecutor(async () => { - return (await axios.get(`${circlePayApi}/cards/${circleCardId}`, headers)).data; - }, { - errorCode: ErrorCodes.CirclePayError, - message: 'Failed getting card details from circle' - }); -}; \ No newline at end of file + return externalRequestExecutor( + async () => { + return ( + await axios.get( + `${circlePayApi}/cards/${circleCardId}`, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + message: 'Failed getting card details from circle', + }, + ); +}; diff --git a/functions/src/circlepay/client/getPaymentFromCircle.ts b/functions/src/circlepay/client/getPaymentFromCircle.ts index dd048bb5..278a1d64 100644 --- a/functions/src/circlepay/client/getPaymentFromCircle.ts +++ b/functions/src/circlepay/client/getPaymentFromCircle.ts @@ -1,25 +1,34 @@ import axios from 'axios'; -import { externalRequestExecutor } from '../../util'; -import { circlePayApi } from '../../settings'; -import { ErrorCodes } from '../../constants'; +import {externalRequestExecutor} from '../../util'; +import {circlePayApi} from '../../settings'; +import {ErrorCodes} from '../../constants'; -import { getCircleHeaders } from '../index'; -import { ICirclePayment } from '../types'; +import {getCircleHeaders} from '../index'; +import {ICirclePayment} from '../types'; /** * Gets the current state of the payment from Circle * * @param paymentId - the Circle payment ID (not the local one) */ -export const getPaymentFromCircle = async (paymentId: string): Promise => { +export const getPaymentFromCircle = async ( + paymentId: string, +): Promise => { const headers = await getCircleHeaders(); - return externalRequestExecutor(async () => { - return (await axios.get(`${circlePayApi}/payments/${paymentId}`, headers)).data; - }, { - errorCode: ErrorCodes.CirclePayError, - message: `Circle call to GET payment with id ${paymentId} failed` - }); + return externalRequestExecutor( + async () => { + return ( + await axios.get( + `${circlePayApi}/payments/${paymentId}`, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + message: `Circle call to GET payment with id ${paymentId} failed`, + }, + ); }; - diff --git a/functions/src/circlepay/client/index.ts b/functions/src/circlepay/client/index.ts index 0ae98770..640262e0 100644 --- a/functions/src/circlepay/client/index.ts +++ b/functions/src/circlepay/client/index.ts @@ -1,11 +1,11 @@ -import { getPaymentFromCircle } from './getPaymentFromCircle'; -import { getCardFromCircle } from './getCardFromCircle'; -import { getBankAccountFromCircle } from './getBankAccountFromCircle'; +import {getPaymentFromCircle} from './getPaymentFromCircle'; +import {getCardFromCircle} from './getCardFromCircle'; +import {getBankAccountFromCircle} from './getBankAccountFromCircle'; -export { ICircleCard, CircleCardNetwork } from './getCardFromCircle'; +export {ICircleCard, CircleCardNetwork} from './getCardFromCircle'; export const circleClient = { getPayment: getPaymentFromCircle, getCard: getCardFromCircle, - getBankAccount: getBankAccountFromCircle -} \ No newline at end of file + getBankAccount: getBankAccountFromCircle, +}; diff --git a/functions/src/circlepay/index.ts b/functions/src/circlepay/index.ts index 85c68045..937c6871 100644 --- a/functions/src/circlepay/index.ts +++ b/functions/src/circlepay/index.ts @@ -1,71 +1,72 @@ import * as functions from 'firebase-functions'; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import axios from 'axios'; import * as payoutCrons from './payouts/crons'; -import { commonApp, commonRouter, externalRequestExecutor } from '../util'; -import { responseExecutor } from '../util/responseExecutor'; -import { circlePayApi, getSecret } from '../settings'; -import { ArgumentError } from '../util/errors'; -import { ErrorCodes } from '../constants'; +import {commonApp, commonRouter, externalRequestExecutor} from '../util'; +import {responseExecutor} from '../util/responseExecutor'; +import {circlePayApi, getSecret} from '../settings'; +import {ArgumentError} from '../util/errors'; +import {ErrorCodes} from '../constants'; -import { createCard } from './cards/business/createCard'; -import { createBankAccount } from './backAccounts/bussiness/createBankAccount'; +import {createCard} from './cards/business/createCard'; +import {createBankAccount} from './backAccounts/bussiness/createBankAccount'; -import { approvePayout } from './payouts/business/approvePayout'; -import { createProposalPayout } from './payouts/business/createProposalPayout'; -import { createIndependentPayout } from './payouts/business/createIndependentPayout'; -import { updatePaymentFromCircle } from './payments/business/updatePaymentFromCircle'; +import {approvePayout} from './payouts/business/approvePayout'; +import {createProposalPayout} from './payouts/business/createProposalPayout'; +import {createIndependentPayout} from './payouts/business/createIndependentPayout'; +import {updatePaymentFromCircle} from './payments/business/updatePaymentFromCircle'; const runtimeOptions = { - timeoutSeconds: 540 + timeoutSeconds: 540, }; const CIRCLEPAY_APIKEY = 'CIRCLEPAY_APIKEY'; -export const getCircleHeaders = async (): Promise => ( - getSecret(CIRCLEPAY_APIKEY).then((apiKey) => ( - { - headers: { - Accept: 'application/json', - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } - }) - ) -); - +export const getCircleHeaders = async (): Promise => + getSecret(CIRCLEPAY_APIKEY).then((apiKey) => ({ + headers: { + Accept: 'application/json', + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + })); const circlepay = commonRouter(); circlepay.post('/create-card', async (req, res, next) => { await responseExecutor( - async () => (await createCard({ - ...req.body, - ipAddress: '127.0.0.1', // @todo Strange. There is no Ip to be find in the request object. Make it be :D - ownerId: req.user.uid, - sessionId: req.requestId - })), + async () => + await createCard({ + ...req.body, + ipAddress: '127.0.0.1', // @todo Strange. There is no Ip to be find in the request object. Make it be :D + ownerId: req.user.uid, + sessionId: req.requestId, + }), { req, res, next, - successMessage: `CirclePay card created successfully!` - }); + successMessage: `CirclePay card created successfully!`, + }, + ); }); circlepay.get('/encryption', async (req, res, next) => { await responseExecutor( async () => { - const options = await getCircleHeaders(); - const response = await externalRequestExecutor(async () => { - return await axios.get(`${circlePayApi}/encryption/public`, options); - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Call to CirclePay failed. Please try again later and if the issue persist contact us.' - }); + const response = await externalRequestExecutor( + async () => { + return await axios.get(`${circlePayApi}/encryption/public`, options); + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Call to CirclePay failed. Please try again later and if the issue persist contact us.', + }, + ); return response.data; }, @@ -73,135 +74,147 @@ circlepay.get('/encryption', async (req, res, next) => { req, res, next, - successMessage: `PCI encryption key generated!` - }); + successMessage: `PCI encryption key generated!`, + }, + ); }); // ----- Payment Related requests circlepay.get('/payments/update/data', async (req, res, next) => { - await responseExecutor(async () => { - const trackId = req.requestId || v4(); - const paymentId = req.query.paymentId; - - if (!paymentId) { - throw new ArgumentError('paymentId'); - } - - logger.notice('User requested update for all payments from circle', { - userId: req.user?.uid - }); - - await updatePaymentFromCircle(paymentId as string, trackId); - }, { - req, - res, - next, - successMessage: 'Payment update succeeded' - }); -}); + await responseExecutor( + async () => { + const trackId = req.requestId || v4(); + const paymentId = req.query.paymentId; + + if (!paymentId) { + throw new ArgumentError('paymentId'); + } + + logger.notice('User requested update for all payments from circle', { + userId: req.user?.uid, + }); + await updatePaymentFromCircle(paymentId as string, trackId); + }, + { + req, + res, + next, + successMessage: 'Payment update succeeded', + }, + ); +}); circlepay.get('/testIP', async (req, res, next) => { await responseExecutor( async () => { const response = await axios.get('https://api.ipify.org?format=json'); return { - ip: response.data + ip: response.data, }; - }, { + }, + { req, res, next, - successMessage: `Test Ip generated` - }); + successMessage: `Test Ip generated`, + }, + ); }); // ----- Bank Accounts circlepay.post('/wires/create', async (req, res, next) => { - await responseExecutor(async () => { - const data = await createBankAccount(req.body); - - return { - id: data.id - }; - }, { - req, - res, - next, - successMessage: 'Done!' - }); + await responseExecutor( + async () => { + const data = await createBankAccount(req.body); + + return { + id: data.id, + }; + }, + { + req, + res, + next, + successMessage: 'Done!', + }, + ); }); // ----- Payouts circlepay.get('/payouts/create', async (req, res, next) => { - await responseExecutor(async () => { - - const obj = JSON.parse(JSON.stringify(req.query)); - const payload = JSON.parse(obj.payload); - - - if (payload.wire) { - const bankAccount = await createBankAccount(payload.wire); - - if (payload.payout.type === 'proposal') { - await createProposalPayout({ - proposalId: payload.proposalId, - bankAccountId: bankAccount.id - }); - } else if (payload.payout.type === 'independent') { - await createIndependentPayout({ - amount: payload.amount, - bankAccountId: bankAccount.id - }); - } - } else { - if (payload.type === 'proposal') { - await createProposalPayout({ - proposalId: payload.proposalId, - bankAccountId: payload.bankAccountId - }); - } else if (payload.type === 'independent') { - await createIndependentPayout({ - amount: payload.amount, - bankAccountId: payload.bankAccountId - }); + await responseExecutor( + async () => { + const obj = JSON.parse(JSON.stringify(req.query)); + const payload = JSON.parse(obj.payload); + + if (payload.wire) { + const bankAccount = await createBankAccount(payload.wire); + + if (payload.payout.type === 'proposal') { + await createProposalPayout({ + proposalId: payload.proposalId, + bankAccountId: bankAccount.id, + }); + } else if (payload.payout.type === 'independent') { + await createIndependentPayout({ + amount: payload.amount, + bankAccountId: bankAccount.id, + }); + } + } else { + if (payload.type === 'proposal') { + await createProposalPayout({ + proposalId: payload.proposalId, + bankAccountId: payload.bankAccountId, + }); + } else if (payload.type === 'independent') { + await createIndependentPayout({ + amount: payload.amount, + bankAccountId: payload.bankAccountId, + }); + } } - } - }, { - req, - res, - next, - successMessage: 'Payout created' - }); + }, + { + req, + res, + next, + successMessage: 'Payout created', + }, + ); }); circlepay.get('/payouts/approve', async (req, res, next) => { - await responseExecutor(async () => { - return { - approved: await approvePayout(req.query) - }; - }, { - req, - res, - next, - successMessage: 'Payout created' - }); + await responseExecutor( + async () => { + return { + approved: await approvePayout(req.query), + }; + }, + { + req, + res, + next, + successMessage: 'Payout created', + }, + ); }); export const circlePayCrons = { - ...payoutCrons + ...payoutCrons, }; -export const circlePayApp = functions - .runWith(runtimeOptions) - .https.onRequest(commonApp(circlepay, { +export const circlePayApp = functions.runWith(runtimeOptions).https.onRequest( + commonApp(circlepay, { unauthenticatedRoutes: [ '/payouts/approve', '/charge/subscription', '/payouts/create', - '/testIP' - ] - })); + '/testIP', + ], + }), +); diff --git a/functions/src/circlepay/payments/business/createPayment.ts b/functions/src/circlepay/payments/business/createPayment.ts index 0f5c9a2b..a880f8d1 100644 --- a/functions/src/circlepay/payments/business/createPayment.ts +++ b/functions/src/circlepay/payments/business/createPayment.ts @@ -1,65 +1,57 @@ import axios from 'axios'; import * as yup from 'yup'; -import { v4 } from 'uuid'; - -import { IPaymentEntity } from '../types'; -import { validate } from '../../../util/validate'; -import { getCircleHeaders } from '../../index'; -import { ICircleCreatePaymentPayload, ICircleCreatePaymentResponse } from '../../types'; -import { userDb } from '../../../users/database'; -import { externalRequestExecutor } from '../../../util'; -import { circlePayApi } from '../../../settings'; -import { ErrorCodes } from '../../../constants'; -import { paymentDb } from '../database'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; -import { cardDb } from '../../cards/database'; -import { CommonError, CvvVerificationError } from '../../../util/errors'; +import {v4} from 'uuid'; + +import {IPaymentEntity} from '../types'; +import {validate} from '../../../util/validate'; +import {getCircleHeaders} from '../../index'; +import { + ICircleCreatePaymentPayload, + ICircleCreatePaymentResponse, +} from '../../types'; +import {userDb} from '../../../users/database'; +import {externalRequestExecutor} from '../../../util'; +import {circlePayApi} from '../../../settings'; +import {ErrorCodes} from '../../../constants'; +import {paymentDb} from '../database'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; +import {cardDb} from '../../cards/database'; +import {CommonError, CvvVerificationError} from '../../../util/errors'; const createPaymentValidationSchema = yup.object({ - userId: yup.string() - .required(), - - cardId: yup.string() - .uuid() - .required(), - - proposalId: yup.string() - .required(), - - subscriptionId: yup.string() - .when('type', { - is: 'subscription', - then: yup.string().required() - }), - - ipAddress: yup.string() - .required(), - - sessionId: yup.string() - .required(), - - amount: yup.number() - .required(), - - type: yup.string() - .oneOf(['one-time', 'subscription']) - .required(), - - encryptedData: yup.string() - .when('type', { - is: 'one-time', - then: yup.string() //.required() - }), - - keyId: yup.string() - .when('type', { - is: 'one-time', - then: yup.string() //.required() - }) + userId: yup.string().required(), + + cardId: yup.string().uuid().required(), + + proposalId: yup.string().required(), + + subscriptionId: yup.string().when('type', { + is: 'subscription', + then: yup.string().required(), + }), + + ipAddress: yup.string().required(), + + sessionId: yup.string().required(), + + amount: yup.number().required(), + + type: yup.string().oneOf(['one-time', 'subscription']).required(), + + encryptedData: yup.string().when('type', { + is: 'one-time', + then: yup.string(), //.required() + }), + + keyId: yup.string().when('type', { + is: 'one-time', + then: yup.string(), //.required() + }), }); -interface ICreatePaymentPayload extends yup.InferType { +interface ICreatePaymentPayload + extends yup.InferType { /** * This is the ID of the object that we are charging (the proposal in this case). It is used * as idempotency key, so we don't charge more than one time for one thing @@ -80,7 +72,9 @@ interface ICreatePaymentPayload extends yup.InferType => { +export const createPayment = async ( + payload: ICreatePaymentPayload, +): Promise => { // Validate the data await validate(payload, createPaymentValidationSchema); @@ -92,7 +86,7 @@ export const createPayment = async (payload: ICreatePaymentPayload): Promise(async () => { - logger.debug('Creating payment in Circle', { - data: circleData - }); - - return (await axios.post(`${circlePayApi}/payments`, - circleData, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Call to CirclePay failed. Please try again later and if the issue persist contact us.' - }); + const { + data: response, + } = await externalRequestExecutor( + async () => { + logger.debug('Creating payment in Circle', { + data: circleData, + }); + + return ( + await axios.post( + `${circlePayApi}/payments`, + circleData, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Call to CirclePay failed. Please try again later and if the issue persist contact us.', + }, + ); // Save the payment const payment = await paymentDb.add({ amount: { amount: payload.amount, - currency: 'USD' + currency: 'USD', }, source: { type: 'card', - id: card.id + id: card.id, }, type: payload.type, @@ -169,24 +170,24 @@ export const createPayment = async (payload: ICreatePaymentPayload): Promise, createPaymentOptions?: Partial): Promise => { +export const createProposalPayment = async ( + payload: yup.InferType, + createPaymentOptions?: Partial, +): Promise => { const options = { ...createPaymentDefaultOptions, - ...createPaymentOptions + ...createPaymentOptions, }; // Validate the data @@ -59,9 +58,11 @@ export const createProposalPayment = async (payload: yup.InferType): Promise => { +export const createSubscriptionPayment = async ( + payload: yup.InferType, +): Promise => { // Validate the data await validate(payload, createSubscriptionPaymentValidationSchema); @@ -44,13 +44,12 @@ export const createSubscriptionPayment = async (payload: yup.InferType => { +export const pollPayment = async ( + payment: IPaymentEntity, + pollPaymentOptions?: IPollPaymentOptions, +): Promise => { if (!payment) { throw new ArgumentError('payment', payment); } @@ -48,32 +51,45 @@ export const pollPayment = async (payment: IPaymentEntity, pollPaymentOptions?: const headers = await getCircleHeaders(); const options = { ...defaultPaymentOptions, - ...pollPaymentOptions + ...pollPaymentOptions, }; const pollFn = async (): Promise => { - return externalRequestExecutor(async () => { - return (await axios.get(`${circlePayApi}/payments/${payment.circlePaymentId}`, headers)).data; - }, { - errorCode: ErrorCodes.CirclePayError, - message: `Polling Circle call to get payment with id ${payment.circlePaymentId} failed` - }); + return externalRequestExecutor( + async () => { + return ( + await axios.get( + `${circlePayApi}/payments/${payment.circlePaymentId}`, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + message: `Polling Circle call to get payment with id ${payment.circlePaymentId} failed`, + }, + ); }; const validateFn = (payment: ICirclePayment): boolean => { - return options.desiredStatus.some(s => s === payment.data.status); + return options.desiredStatus.some((s) => s === payment.data.status); }; - const circlePaymentObj = await poll(pollFn, validateFn, options.interval, options.maxRetries); - + const circlePaymentObj = await poll( + pollFn, + validateFn, + options.interval, + options.maxRetries, + ); + const updatedPaymentObj = await updatePayment(payment, circlePaymentObj); if (options.throwOnPaymentFailed && updatedPaymentObj.status === 'failed') { throw new CommonError('Payment failed', { - payment: updatedPaymentObj + payment: updatedPaymentObj, }); } // Return the updated payment return updatedPaymentObj; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/business/updatePayment.ts b/functions/src/circlepay/payments/business/updatePayment.ts index ec48a664..71a11a83 100644 --- a/functions/src/circlepay/payments/business/updatePayment.ts +++ b/functions/src/circlepay/payments/business/updatePayment.ts @@ -1,8 +1,8 @@ -import { IPaymentEntity } from '../types'; -import { ICirclePayment } from '../../types'; -import { failureHelper, feesHelper } from '../helpers'; -import { paymentDb } from '../database'; -import { CommonError } from '../../../util/errors'; +import {IPaymentEntity} from '../types'; +import {ICirclePayment} from '../../types'; +import {failureHelper, feesHelper} from '../helpers'; +import {paymentDb} from '../database'; +import {CommonError} from '../../../util/errors'; /** * Handles update from circle and saves it to the database @@ -10,7 +10,10 @@ import { CommonError } from '../../../util/errors'; * @param payment - the current version of the payment from our FireStore * @param circlePayment - the current version of the payment as is from Circle */ -export const updatePayment = async (payment: IPaymentEntity, circlePayment: ICirclePayment): Promise => { +export const updatePayment = async ( + payment: IPaymentEntity, + circlePayment: ICirclePayment, +): Promise => { let updatedPayment: IPaymentEntity = payment; switch (circlePayment.data.status) { @@ -19,7 +22,7 @@ export const updatePayment = async (payment: IPaymentEntity, circlePayment: ICir ...payment, status: circlePayment.data.status, - failure: failureHelper.processFailedPayment(circlePayment) + failure: failureHelper.processFailedPayment(circlePayment), }; break; @@ -29,16 +32,19 @@ export const updatePayment = async (payment: IPaymentEntity, circlePayment: ICir ...payment, status: circlePayment.data.status, - fees: feesHelper.processCircleFee(circlePayment) + fees: feesHelper.processCircleFee(circlePayment), }; break; default: - logger.warn('Unknown payment state occurred. Not knowing how to handle the payment update', { - payment, - circlePayment: circlePayment.data, - unknownStatus: circlePayment.data.status - }); + logger.warn( + 'Unknown payment state occurred. Not knowing how to handle the payment update', + { + payment, + circlePayment: circlePayment.data, + unknownStatus: circlePayment.data.status, + }, + ); break; } @@ -58,9 +64,12 @@ export const updatePayment = async (payment: IPaymentEntity, circlePayment: ICir break; default: - throw new CommonError(`The payment status has updated, but is not known.`, { - payment: updatedPayment - }); + throw new CommonError( + `The payment status has updated, but is not known.`, + { + payment: updatedPayment, + }, + ); } // @todo If this is subscription payment handle subscription update @@ -68,4 +77,4 @@ export const updatePayment = async (payment: IPaymentEntity, circlePayment: ICir } return updatedPayment; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/business/updatePaymentFromCircle.ts b/functions/src/circlepay/payments/business/updatePaymentFromCircle.ts index e138916b..3b4288ed 100644 --- a/functions/src/circlepay/payments/business/updatePaymentFromCircle.ts +++ b/functions/src/circlepay/payments/business/updatePaymentFromCircle.ts @@ -1,11 +1,11 @@ -import { circleClient } from '../../client'; +import {circleClient} from '../../client'; -import { IPaymentEntity } from '../types'; -import { isFinalized } from '../helpers'; -import { paymentDb } from '../database'; +import {IPaymentEntity} from '../types'; +import {isFinalized} from '../helpers'; +import {paymentDb} from '../database'; -import { updatePayment } from './updatePayment'; -import { ArgumentError } from '../../../util/errors'; +import {updatePayment} from './updatePayment'; +import {ArgumentError} from '../../../util/errors'; interface IUpdatePaymentFromCircleOptions { /** @@ -17,7 +17,7 @@ interface IUpdatePaymentFromCircleOptions { } const defaultOptions: IUpdatePaymentFromCircleOptions = { - skipIfFinalized: false + skipIfFinalized: false, }; /** @@ -29,7 +29,11 @@ const defaultOptions: IUpdatePaymentFromCircleOptions = { * * @returns - The updated payment entity */ -export const updatePaymentFromCircle = async (paymentId: string, trackId: string, customOptions?: Partial,): Promise => { +export const updatePaymentFromCircle = async ( + paymentId: string, + trackId: string, + customOptions?: Partial, +): Promise => { // Verify the required arguments if (typeof paymentId !== 'string') { throw new ArgumentError('paymentId', paymentId); @@ -38,7 +42,7 @@ export const updatePaymentFromCircle = async (paymentId: string, trackId: string // Create the final options const options = { ...defaultOptions, - ...customOptions + ...customOptions, }; // Find the payment in the database @@ -54,11 +58,8 @@ export const updatePaymentFromCircle = async (paymentId: string, trackId: string const circlePayment = await circleClient.getPayment(payment.circlePaymentId); // Add the track ID to the list - payment['trackIds'] = [ - trackId, - ...(payment['trackIds'] || []) - ]; + payment['trackIds'] = [trackId, ...(payment['trackIds'] || [])]; // Update the payment and return it return updatePayment(payment, circlePayment); -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/business/updatePaymentsFromCircle.ts b/functions/src/circlepay/payments/business/updatePaymentsFromCircle.ts index f907ff80..f47c77d5 100644 --- a/functions/src/circlepay/payments/business/updatePaymentsFromCircle.ts +++ b/functions/src/circlepay/payments/business/updatePaymentsFromCircle.ts @@ -1,42 +1,49 @@ -import { getPayments } from '../database/getPayments'; -import { updatePaymentFromCircle } from './updatePaymentFromCircle'; -import { paymentDb } from '../database'; +import {getPayments} from '../database/getPayments'; +import {updatePaymentFromCircle} from './updatePaymentFromCircle'; +import {paymentDb} from '../database'; -export const updatePaymentsFromCircle = async (trackId: string): Promise => { +export const updatePaymentsFromCircle = async ( + trackId: string, +): Promise => { const payments = await getPayments({}); const paymentUpdatePromiseArr: Promise[] = []; - payments.forEach(payment => { + payments.forEach((payment) => { if (payment.createdAt) { - paymentUpdatePromiseArr.push((async () => { - try { - await updatePaymentFromCircle(payment.id, trackId); - } catch (e) { - logger.warn('Unable to update payment from circle because error occurred trying to do so', { - payment, - error: e - }); + paymentUpdatePromiseArr.push( + (async () => { + try { + await updatePaymentFromCircle(payment.id, trackId); + } catch (e) { + logger.warn( + 'Unable to update payment from circle because error occurred trying to do so', + { + payment, + error: e, + }, + ); - payment['updateFailed'] = true; + payment['updateFailed'] = true; - try { - payment['updateFailedData'] = { - message: e.message, - response: JSON.parse(e.data?.response) - }; - } catch (ex) { - payment['updateFailedData'] = { - message: ex.message, - response: ex.data?.response - }; - } + try { + payment['updateFailedData'] = { + message: e.message, + response: JSON.parse(e.data?.response), + }; + } catch (ex) { + payment['updateFailedData'] = { + message: ex.message, + response: ex.data?.response, + }; + } - await paymentDb.update(payment); - } - })()); + await paymentDb.update(payment); + } + })(), + ); } else { logger.debug('Skipping update on older type payment.', { - payment + payment, }); } }); @@ -44,4 +51,4 @@ export const updatePaymentsFromCircle = async (trackId: string): Promise = await Promise.all(paymentUpdatePromiseArr); logger.info('Successfully updated all payments'); -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/database/addPayment.ts b/functions/src/circlepay/payments/database/addPayment.ts index 61e46b25..6c932858 100644 --- a/functions/src/circlepay/payments/database/addPayment.ts +++ b/functions/src/circlepay/payments/database/addPayment.ts @@ -1,12 +1,11 @@ -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { BaseEntityType, SharedOmit } from '../../../util/types'; - -import { IPaymentEntity } from '../types'; -import { PaymentsCollection } from './index'; +import {BaseEntityType, SharedOmit} from '../../../util/types'; +import {IPaymentEntity} from '../types'; +import {PaymentsCollection} from './index'; type OmittedProperties = BaseEntityType | 'fees'; @@ -16,7 +15,9 @@ type OmittedProperties = BaseEntityType | 'fees'; * * @param payment - the payment to be saved */ -export const addPayment = async (payment: SharedOmit): Promise => { +export const addPayment = async ( + payment: SharedOmit, +): Promise => { const paymentDoc: IPaymentEntity = { id: v4(), @@ -25,19 +26,17 @@ export const addPayment = async (payment: SharedOmit> => { +export const deletePayment = async ( + paymentId: string, + deletionId = v4(), +): Promise> => { const latestPaymentSnapshot = await getPayment(paymentId, false); if (!latestPaymentSnapshot) { - logger.notice(`Cannot delete payment with ID ${paymentId} cause it does not exist`); + logger.notice( + `Cannot delete payment with ID ${paymentId} cause it does not exist`, + ); return null; } logger.notice(`Deleting payment with ID ${paymentId}`, { - snapshot: latestPaymentSnapshot + snapshot: latestPaymentSnapshot, }); // Delete the payment await PaymentsCollection.doc(paymentId).delete(); logger.debug('Payment was successfully deleted. Creating deleted entity', { - payment: latestPaymentSnapshot + payment: latestPaymentSnapshot, }); // Save the deleted entity return deletedDb.add(latestPaymentSnapshot, deletionId); -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/database/getPayment.ts b/functions/src/circlepay/payments/database/getPayment.ts index 7a4bc6b9..bb85d4af 100644 --- a/functions/src/circlepay/payments/database/getPayment.ts +++ b/functions/src/circlepay/payments/database/getPayment.ts @@ -1,7 +1,7 @@ -import { ArgumentError, NotFoundError } from '../../../util/errors'; +import {ArgumentError, NotFoundError} from '../../../util/errors'; -import { IPaymentEntity } from '../types'; -import { PaymentsCollection } from './index'; +import {IPaymentEntity} from '../types'; +import {PaymentsCollection} from './index'; /** * Gets payment by id @@ -15,18 +15,19 @@ import { PaymentsCollection } from './index'; * * @returns - The found payment */ -export const getPayment = async (paymentId: string, throwErr = true): Promise => { +export const getPayment = async ( + paymentId: string, + throwErr = true, +): Promise => { if (!paymentId) { throw new ArgumentError('paymentId', paymentId); } - const payment = (await PaymentsCollection - .doc(paymentId) - .get()).data(); + const payment = (await PaymentsCollection.doc(paymentId).get()).data(); if (!payment && throwErr) { throw new NotFoundError(paymentId, 'payment'); } return payment; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/database/getPayments.ts b/functions/src/circlepay/payments/database/getPayments.ts index a700ad40..ead63006 100644 --- a/functions/src/circlepay/payments/database/getPayments.ts +++ b/functions/src/circlepay/payments/database/getPayments.ts @@ -1,5 +1,5 @@ -import { IPaymentEntity, PaymentStatus } from '../types'; -import { PaymentsCollection } from './index'; +import {IPaymentEntity, PaymentStatus} from '../types'; +import {PaymentsCollection} from './index'; interface IGetPaymentsOptions { /** @@ -20,7 +20,7 @@ interface IGetPaymentsOptions { createdFromObject?: { id?: string; - } + }; } /** @@ -28,7 +28,9 @@ interface IGetPaymentsOptions { * * @param options - The options for filtering the payments */ -export const getPayments = async (options: IGetPaymentsOptions): Promise => { +export const getPayments = async ( + options: IGetPaymentsOptions, +): Promise => { let paymentsQuery: any = PaymentsCollection; if (options.id) { @@ -42,18 +44,22 @@ export const getPayments = async (options: IGetPaymentsOptions): Promise payment.data()) || []; + return ( + (await paymentsQuery.get()).docs.map((payment) => payment.data()) || [] + ); }; diff --git a/functions/src/circlepay/payments/database/index.ts b/functions/src/circlepay/payments/database/index.ts index 2c91bd8c..56fee322 100644 --- a/functions/src/circlepay/payments/database/index.ts +++ b/functions/src/circlepay/payments/database/index.ts @@ -1,22 +1,27 @@ -import { db } from '../../../util'; -import { Collections } from '../../../constants'; - -import { IPaymentEntity } from '../types'; -import { addPayment } from './addPayment'; -import { updatePaymentInDatabase } from './updatePayment'; -import { getPayments } from './getPayments'; -import { getPayment } from './getPayment'; -import { deletePayment } from './deletePayment'; - -export const PaymentsCollection = db.collection(Collections.Payments) +import {db} from '../../../util'; +import {Collections} from '../../../constants'; + +import {IPaymentEntity} from '../types'; +import {addPayment} from './addPayment'; +import {updatePaymentInDatabase} from './updatePayment'; +import {getPayments} from './getPayments'; +import {getPayment} from './getPayment'; +import {deletePayment} from './deletePayment'; + +export const PaymentsCollection = db + .collection(Collections.Payments) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IPaymentEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IPaymentEntity { return snapshot.data() as IPaymentEntity; }, - toFirestore(object: IPaymentEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: IPaymentEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); export const paymentDb = { @@ -48,5 +53,5 @@ export const paymentDb = { * Delete payment from the payments collection and * created deleted entity for it */ - delete: deletePayment -}; \ No newline at end of file + delete: deletePayment, +}; diff --git a/functions/src/circlepay/payments/database/updatePayment.ts b/functions/src/circlepay/payments/database/updatePayment.ts index 853e2c58..11f4ac1c 100644 --- a/functions/src/circlepay/payments/database/updatePayment.ts +++ b/functions/src/circlepay/payments/database/updatePayment.ts @@ -1,7 +1,7 @@ -import { firestore } from 'firebase-admin'; +import {firestore} from 'firebase-admin'; -import { IPaymentEntity } from '../types'; -import { PaymentsCollection } from './index'; +import {IPaymentEntity} from '../types'; +import {PaymentsCollection} from './index'; interface IUpdatePaymentOptions { useSet: boolean; @@ -13,28 +13,27 @@ interface IUpdatePaymentOptions { * @param payment - The updated payment * @param options - Options object, modifying the set behaviour */ -export const updatePaymentInDatabase = async (payment: IPaymentEntity, options: Partial = {}): Promise => { +export const updatePaymentInDatabase = async ( + payment: IPaymentEntity, + options: Partial = {}, +): Promise => { const paymentDoc = { ...payment, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; if (options.useSet) { - await PaymentsCollection - .doc(paymentDoc.id) - .set(paymentDoc); + await PaymentsCollection.doc(paymentDoc.id).set(paymentDoc); } else { - await PaymentsCollection - .doc(paymentDoc.id) - .update(paymentDoc); + await PaymentsCollection.doc(paymentDoc.id).update(paymentDoc); } logger.info('Updating payment', { updatedPayment: paymentDoc, updatedAt: paymentDoc.updatedAt, - previousUpdateAt: payment.updatedAt + previousUpdateAt: payment.updatedAt, }); return paymentDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payments/helpers/failureHelper.ts b/functions/src/circlepay/payments/helpers/failureHelper.ts index 19f9c975..351375e3 100644 --- a/functions/src/circlepay/payments/helpers/failureHelper.ts +++ b/functions/src/circlepay/payments/helpers/failureHelper.ts @@ -1,6 +1,6 @@ -import { ICirclePayment } from '../../types'; +import {ICirclePayment} from '../../types'; -import { IPaymentFailureReason, PaymentFailureResponseCodes } from '../types'; +import {IPaymentFailureReason, PaymentFailureResponseCodes} from '../types'; const failureCodeDescription: { [key in PaymentFailureResponseCodes]: string; @@ -9,42 +9,49 @@ const failureCodeDescription: { card_invalid: 'Invalid card number', card_limit_violated: 'Exceeded amount or frequency limits', card_not_honored: 'Contact card issuer to query why payment failed', - credit_card_not_allowed: 'Issuer did not support using a credit card for payment', - payment_denied: 'Payment denied by Circle Risk Service or card processor risk controls', + credit_card_not_allowed: + 'Issuer did not support using a credit card for payment', + payment_denied: + 'Payment denied by Circle Risk Service or card processor risk controls', payment_failed: 'Payment failed due to unspecified error', payment_fraud_detected: 'Payment suspected of being associated with fraud', payment_not_funded: 'Insufficient funds in account to fund payment', payment_not_supported_by_issuer: 'Issuer did not support the payment', - payment_stopped_by_issuer: 'A stop has been placed on the payment or card' - + payment_stopped_by_issuer: 'A stop has been placed on the payment or card', }; -const getFailureDescription = (responseCode: PaymentFailureResponseCodes = 'payment_failed'): string => - failureCodeDescription[responseCode]; +const getFailureDescription = ( + responseCode: PaymentFailureResponseCodes = 'payment_failed', +): string => failureCodeDescription[responseCode]; -export const processFailedPayment = (payment: ICirclePayment): IPaymentFailureReason => { +export const processFailedPayment = ( + payment: ICirclePayment, +): IPaymentFailureReason => { if (payment.data.status !== 'failed') { - logger.warn('Trying to process failed payment that is not actually failed', { - payment - }); + logger.warn( + 'Trying to process failed payment that is not actually failed', + { + payment, + }, + ); return null; } const failureReason: IPaymentFailureReason = { errorCode: payment.data.errorCode || 'payment_failed', - errorDescription: getFailureDescription(payment.data.errorCode) + errorDescription: getFailureDescription(payment.data.errorCode), }; logger.debug(`Processed failure reason for payment}`, { failureReasonFromCircle: payment.data.errorCode, processedFailureReason: failureReason, - payment + payment, }); return failureReason; }; export const failureHelper = { - processFailedPayment -}; \ No newline at end of file + processFailedPayment, +}; diff --git a/functions/src/circlepay/payments/helpers/feesHelper.ts b/functions/src/circlepay/payments/helpers/feesHelper.ts index 586a58c8..ca8cdd07 100644 --- a/functions/src/circlepay/payments/helpers/feesHelper.ts +++ b/functions/src/circlepay/payments/helpers/feesHelper.ts @@ -1,18 +1,23 @@ -import { ICirclePayment } from '../../types'; -import { IPaymentFees } from '../types'; +import {ICirclePayment} from '../../types'; +import {IPaymentFees} from '../types'; const defaultFees: IPaymentFees = { amount: 0, - currency: 'USD' + currency: 'USD', }; -export const processCircleFee = (circlePayment: ICirclePayment): IPaymentFees => { - const { data: payment } = circlePayment; +export const processCircleFee = ( + circlePayment: ICirclePayment, +): IPaymentFees => { + const {data: payment} = circlePayment; if (!payment.fees) { - logger.warn('Trying to process payment fees, but there is no fees object on the payment', { - payment - }); + logger.warn( + 'Trying to process payment fees, but there is no fees object on the payment', + { + payment, + }, + ); return defaultFees; } @@ -20,18 +25,18 @@ export const processCircleFee = (circlePayment: ICirclePayment): IPaymentFees => const fees: IPaymentFees = { // Circle returns the fees in dollars. Convert it to cents, amount: Number(payment.fees.amount) * 100, - currency: payment.fees.currency + currency: payment.fees.currency, }; logger.debug('Converted circle fees', { circleFees: payment.fees, convertedFees: fees, - circlePaymentId: circlePayment.data.id + circlePaymentId: circlePayment.data.id, }); return fees; }; export const feesHelper = { - processCircleFee -} \ No newline at end of file + processCircleFee, +}; diff --git a/functions/src/circlepay/payments/helpers/index.ts b/functions/src/circlepay/payments/helpers/index.ts index 8cbf7f75..a821770d 100644 --- a/functions/src/circlepay/payments/helpers/index.ts +++ b/functions/src/circlepay/payments/helpers/index.ts @@ -1,4 +1,4 @@ -export { isFinalized, isSuccessful } from './statusHelper'; +export {isFinalized, isSuccessful} from './statusHelper'; -export { feesHelper } from './feesHelper'; -export { failureHelper } from './failureHelper'; \ No newline at end of file +export {feesHelper} from './feesHelper'; +export {failureHelper} from './failureHelper'; diff --git a/functions/src/circlepay/payments/helpers/statusHelper.ts b/functions/src/circlepay/payments/helpers/statusHelper.ts index eb44b235..ec01aed3 100644 --- a/functions/src/circlepay/payments/helpers/statusHelper.ts +++ b/functions/src/circlepay/payments/helpers/statusHelper.ts @@ -1,14 +1,14 @@ -import { IPaymentEntity } from '../types'; +import {IPaymentEntity} from '../types'; const successfulStatuses = ['confirmed', 'paid']; const terminalStatuses = ['paid', 'failed']; const failedStatuses = ['failed']; export const isSuccessful = (payment: IPaymentEntity): boolean => - successfulStatuses.some(status => status === payment.status); + successfulStatuses.some((status) => status === payment.status); export const isFinalized = (payment: IPaymentEntity): boolean => - terminalStatuses.some(status => status === payment.status); + terminalStatuses.some((status) => status === payment.status); export const isFailed = (payment: IPaymentEntity): boolean => - failedStatuses.some(status => status === payment.status); + failedStatuses.some((status) => status === payment.status); diff --git a/functions/src/circlepay/payments/types.ts b/functions/src/circlepay/payments/types.ts index 63822ec6..3822d23d 100644 --- a/functions/src/circlepay/payments/types.ts +++ b/functions/src/circlepay/payments/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity, Nullable } from '../../util/types'; +import {IBaseEntity, Nullable} from '../../util/types'; export type PaymentType = 'one-time' | 'subscription'; export type PaymentStatus = 'pending' | 'confirmed' | 'paid' | 'failed'; @@ -6,17 +6,17 @@ export type PaymentSource = 'card'; export type PaymentCurrency = 'USD'; export type PaymentFailureResponseCodes = - 'payment_failed' | - 'card_not_honored' | - 'payment_not_supported_by_issuer' | - 'payment_not_funded' | - 'card_invalid' | - 'card_limit_violated' | - 'payment_denied' | - 'payment_fraud_detected' | - 'credit_card_not_allowed' | - 'payment_stopped_by_issuer' | - 'card_account_ineligible'; + | 'payment_failed' + | 'card_not_honored' + | 'payment_not_supported_by_issuer' + | 'payment_not_funded' + | 'card_invalid' + | 'card_limit_violated' + | 'payment_denied' + | 'payment_fraud_detected' + | 'credit_card_not_allowed' + | 'payment_stopped_by_issuer' + | 'card_account_ineligible'; interface IPaymentEntityBase extends IBaseEntity { /** @@ -137,5 +137,9 @@ export interface IPaidPayment extends IPaymentEntityBase { status: 'paid'; } - -export type IPaymentEntity = IPaymentEntityBase | IPendingPayment | IConfirmedPayment | IPaidPayment | IFailedPayment; \ No newline at end of file +export type IPaymentEntity = + | IPaymentEntityBase + | IPendingPayment + | IConfirmedPayment + | IPaidPayment + | IFailedPayment; diff --git a/functions/src/circlepay/payouts/business/approvePayout.ts b/functions/src/circlepay/payouts/business/approvePayout.ts index 7ef210fa..67f2fc93 100644 --- a/functions/src/circlepay/payouts/business/approvePayout.ts +++ b/functions/src/circlepay/payouts/business/approvePayout.ts @@ -1,29 +1,24 @@ import * as yup from 'yup'; -import { validate } from '../../../util/validate'; -import { payoutDb } from '../database'; -import { CommonError } from '../../../util/errors'; -import { updatePayout } from '../database/updatePayout'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; +import {validate} from '../../../util/validate'; +import {payoutDb} from '../database'; +import {CommonError} from '../../../util/errors'; +import {updatePayout} from '../database/updatePayout'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; const approvePayoutSchema = yup.object({ - payoutId: yup - .string() - .uuid() - .required(), - - index: yup - .number() - .required(), - - token: yup - .string() - .required() + payoutId: yup.string().uuid().required(), + + index: yup.number().required(), + + token: yup.string().required(), }); type ApprovePayoutPayload = yup.InferType; -export const approvePayout = async (payload: ApprovePayoutPayload): Promise => { +export const approvePayout = async ( + payload: ApprovePayoutPayload, +): Promise => { // Validate the payload await validate(payload, approvePayoutSchema); @@ -33,7 +28,7 @@ export const approvePayout = async (payload: ApprovePayoutPayload): Promise x.id === Number(payload.index)); + const token = payout.security.find((x) => x.id === Number(payload.index)); if (!token) { throw new CommonError(`There is no token with ID ${payload.index}`); @@ -61,7 +56,7 @@ export const approvePayout = async (payload: ApprovePayoutPayload): Promise x.id === token.id); + const tokenIndex = payout.security.findIndex((x) => x.id === token.id); payout.security[tokenIndex] = token; // Save the changes await updatePayout(payout); - // Return the current status of the payout return token.redeemed; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/business/createIndependentPayout.ts b/functions/src/circlepay/payouts/business/createIndependentPayout.ts index 4771b910..69b40c5d 100644 --- a/functions/src/circlepay/payouts/business/createIndependentPayout.ts +++ b/functions/src/circlepay/payouts/business/createIndependentPayout.ts @@ -1,29 +1,25 @@ import * as yup from 'yup'; import crypto from 'crypto'; - -import { IPayoutEntity } from '../types'; -import { validate } from '../../../util/validate'; -import { bankAccountDb } from '../../backAccounts/database'; -import { payoutDb } from '../database'; -import { env } from '../../../constants'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; +import {IPayoutEntity} from '../types'; +import {validate} from '../../../util/validate'; +import {bankAccountDb} from '../../backAccounts/database'; +import {payoutDb} from '../database'; +import {env} from '../../../constants'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; const createPayoutValidationSchema = yup.object({ - amount: yup - .number() - .required(), - - bankAccountId: yup - .string() - .uuid() - .required() + amount: yup.number().required(), + + bankAccountId: yup.string().uuid().required(), }); type CreatePayoutPayload = yup.InferType; -export const createIndependentPayout = async (payload: CreatePayoutPayload): Promise => { +export const createIndependentPayout = async ( + payload: CreatePayoutPayload, +): Promise => { // Validate the payload await validate(payload, createPayoutValidationSchema); @@ -38,26 +34,26 @@ export const createIndependentPayout = async (payload: CreatePayoutPayload): Pro destination: { circleId: bankAccount.circleId, - id: bankAccount.id + id: bankAccount.id, }, security: env.payouts.approvers.map((approver, index) => ({ id: index, token: crypto.randomBytes(32).toString('hex'), redeemed: false, - redemptionAttempts: 0 + redemptionAttempts: 0, })), executed: false, - voided: false + voided: false, }); // Create the event await createEvent({ objectId: payout.id, - type: EVENT_TYPES.PAYOUT_CREATED + type: EVENT_TYPES.PAYOUT_CREATED, }); // Return the created proposal return payout; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/business/createProposalPayout.ts b/functions/src/circlepay/payouts/business/createProposalPayout.ts index d4737d4e..771a65c0 100644 --- a/functions/src/circlepay/payouts/business/createProposalPayout.ts +++ b/functions/src/circlepay/payouts/business/createProposalPayout.ts @@ -1,31 +1,29 @@ import * as yup from 'yup'; import crypto from 'crypto'; -import { IPayoutEntity } from '../types'; -import { validate } from '../../../util/validate'; -import { bankAccountDb } from '../../backAccounts/database'; -import { payoutDb } from '../database'; -import { proposalDb } from '../../../proposals/database'; -import { env } from '../../../constants'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; -import { CommonError } from '../../../util/errors'; +import {IPayoutEntity} from '../types'; +import {validate} from '../../../util/validate'; +import {bankAccountDb} from '../../backAccounts/database'; +import {payoutDb} from '../database'; +import {proposalDb} from '../../../proposals/database'; +import {env} from '../../../constants'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; +import {CommonError} from '../../../util/errors'; const createProposalPayoutValidationSchema = yup.object({ - proposalId: yup - .string() - .uuid() - .required(), - - bankAccountId: yup - .string() - .uuid() - .required() + proposalId: yup.string().uuid().required(), + + bankAccountId: yup.string().uuid().required(), }); -type CreateProposalPayoutPayload = yup.InferType; +type CreateProposalPayoutPayload = yup.InferType< + typeof createProposalPayoutValidationSchema +>; -export const createProposalPayout = async (payload: CreateProposalPayoutPayload): Promise => { +export const createProposalPayout = async ( + payload: CreateProposalPayoutPayload, +): Promise => { // Validate the payload await validate(payload, createProposalPayoutValidationSchema); @@ -35,26 +33,32 @@ export const createProposalPayout = async (payload: CreateProposalPayoutPayload) // Do some validation on the proposal if (proposal.state !== 'passed') { - throw new CommonError('Cannot create payout for proposals that are not in the passed state'); + throw new CommonError( + 'Cannot create payout for proposals that are not in the passed state', + ); } // Make sure there are no (or all are expired) payouts for that proposal const activeOrCompletedPayoutForProposal = await payoutDb.getMany({ proposalId: proposal.id, - status: [ - 'complete', - 'pending' - ] + status: ['complete', 'pending'], }); if (activeOrCompletedPayoutForProposal.length) { - logger.warn('Payout was attempted to be created for proposal that has completed or pending payout'); - - throw new CommonError(`Cannot create payout for proposal with completed or pending payout`, { - proposal, - // If I just left it all it would return the security tokens - activeOrCompletedPayoutForProposal: activeOrCompletedPayoutForProposal.map(payout => payout.id) - }); + logger.warn( + 'Payout was attempted to be created for proposal that has completed or pending payout', + ); + + throw new CommonError( + `Cannot create payout for proposal with completed or pending payout`, + { + proposal, + // If I just left it all it would return the security tokens + activeOrCompletedPayoutForProposal: activeOrCompletedPayoutForProposal.map( + (payout) => payout.id, + ), + }, + ); } // Create the payout @@ -67,27 +71,27 @@ export const createProposalPayout = async (payload: CreateProposalPayoutPayload) destination: { circleId: bankAccount.circleId, - id: bankAccount.id + id: bankAccount.id, }, security: env.payouts.approvers.map((approver, index) => ({ id: index, token: crypto.randomBytes(32).toString('hex'), redeemed: false, - redemptionAttempts: 0 + redemptionAttempts: 0, })), status: 'pending', executed: false, - voided: false + voided: false, }); // Create the event await createEvent({ objectId: payout.id, - type: EVENT_TYPES.PAYOUT_CREATED + type: EVENT_TYPES.PAYOUT_CREATED, }); // Return the created proposal return payout; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/business/executePayout.ts b/functions/src/circlepay/payouts/business/executePayout.ts index 899338a0..8fee8bd0 100644 --- a/functions/src/circlepay/payouts/business/executePayout.ts +++ b/functions/src/circlepay/payouts/business/executePayout.ts @@ -1,16 +1,18 @@ -import { IPayoutEntity } from '../types'; -import { CommonError } from '../../../util/errors'; -import { externalRequestExecutor } from '../../../util'; +import {IPayoutEntity} from '../types'; +import {CommonError} from '../../../util/errors'; +import {externalRequestExecutor} from '../../../util'; import axios from 'axios'; -import { circlePayApi } from '../../../settings'; -import { ErrorCodes } from '../../../constants'; -import { getCircleHeaders } from '../../index'; -import { ICircleCreatePayoutPayload, ICircleCreatePayoutResponse } from '../../types'; -import { env } from '../../../constants'; -import { payoutDb } from '../database'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; - +import {circlePayApi} from '../../../settings'; +import {ErrorCodes} from '../../../constants'; +import {getCircleHeaders} from '../../index'; +import { + ICircleCreatePayoutPayload, + ICircleCreatePayoutResponse, +} from '../../types'; +import {env} from '../../../constants'; +import {payoutDb} from '../database'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; /** * Sends the payout to circle for execution @@ -20,7 +22,7 @@ import { EVENT_TYPES } from '../../../event/event'; export const executePayout = async (payout: IPayoutEntity): Promise => { if (payout.executed) { throw new CommonError('Cannot reexecute payout!', { - payout + payout, }); } @@ -30,32 +32,40 @@ export const executePayout = async (payout: IPayoutEntity): Promise => { idempotencyKey: payout.id, amount: { amount: payout.amount / 100, - currency: 'USD' + currency: 'USD', }, destination: { id: payout.destination.circleId, - type: 'wire' + type: 'wire', }, metadata: { - beneficiaryEmail: env.payouts.approvers[0] // @todo If it is payout based proposal use the email of the proposer - } + beneficiaryEmail: env.payouts.approvers[0], // @todo If it is payout based proposal use the email of the proposer + }, }; // Make the request to circle - const { data: response } = await externalRequestExecutor(async () => { - logger.debug('Creating new payout with circle', { - data - }); - - return (await axios.post(`${circlePayApi}/payouts`, - data, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Cannot create the bank account, because it was rejected by Circle' - }); + const { + data: response, + } = await externalRequestExecutor( + async () => { + logger.debug('Creating new payout with circle', { + data, + }); + return ( + await axios.post( + `${circlePayApi}/payouts`, + data, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Cannot create the bank account, because it was rejected by Circle', + }, + ); // Update the entities const updatedPayout = await payoutDb.update({ @@ -63,12 +73,12 @@ export const executePayout = async (payout: IPayoutEntity): Promise => { circlePayoutId: response.id, status: response.status, - executed: true + executed: true, }); // Broadcast the events await createEvent({ objectId: updatedPayout.id, - type: EVENT_TYPES.PAYOUT_EXECUTED + type: EVENT_TYPES.PAYOUT_EXECUTED, }); -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/business/updatePayoutStatus.ts b/functions/src/circlepay/payouts/business/updatePayoutStatus.ts index ba05cca9..d2011335 100644 --- a/functions/src/circlepay/payouts/business/updatePayoutStatus.ts +++ b/functions/src/circlepay/payouts/business/updatePayoutStatus.ts @@ -1,24 +1,26 @@ import axios from 'axios'; -import { IPayoutEntity } from '../types'; -import { externalRequestExecutor } from '../../../util'; -import { ICircleGetPayoutResponse } from '../../types'; -import { circlePayApi } from '../../../settings'; -import { ErrorCodes } from '../../../constants'; -import { getCircleHeaders } from '../../index'; -import { payoutDb } from '../database'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; - -export const updatePayoutStatus = async (payout: IPayoutEntity): Promise => { +import {IPayoutEntity} from '../types'; +import {externalRequestExecutor} from '../../../util'; +import {ICircleGetPayoutResponse} from '../../types'; +import {circlePayApi} from '../../../settings'; +import {ErrorCodes} from '../../../constants'; +import {getCircleHeaders} from '../../index'; +import {payoutDb} from '../database'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; + +export const updatePayoutStatus = async ( + payout: IPayoutEntity, +): Promise => { if (!payout.executed) { - logger.error('Only executed proposal have statuses', { payout }); + logger.error('Only executed proposal have statuses', {payout}); return; } if (payout.status !== 'pending') { - logger.warn('The payout is in it\'s final state', { payout }); + logger.warn("The payout is in it's final state", {payout}); return; } @@ -26,27 +28,37 @@ export const updatePayoutStatus = async (payout: IPayoutEntity): Promise = // Get the headers, needed for request, ang get the status from circle const headers = await getCircleHeaders(); - const { data: response } = await externalRequestExecutor(async () => { - return (await axios.get(`${circlePayApi}/payout/${payout.circlePayoutId}`, - headers - )).data; - }, { - errorCode: ErrorCodes.CirclePayError, - userMessage: 'Cannot create the bank account, because it was rejected by Circle' - }); + const { + data: response, + } = await externalRequestExecutor( + async () => { + return ( + await axios.get( + `${circlePayApi}/payout/${payout.circlePayoutId}`, + headers, + ) + ).data; + }, + { + errorCode: ErrorCodes.CirclePayError, + userMessage: + 'Cannot create the bank account, because it was rejected by Circle', + }, + ); // If the status have changed broadcast the event if (response.status !== 'pending') { await createEvent({ objectId: payout.id, - type: response.status === 'complete' - ? EVENT_TYPES.PAYOUT_COMPLETED - : EVENT_TYPES.PAYOUT_FAILED + type: + response.status === 'complete' + ? EVENT_TYPES.PAYOUT_COMPLETED + : EVENT_TYPES.PAYOUT_FAILED, }); } await payoutDb.update({ ...payout, - status: response.status + status: response.status, }); -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/crons/index.ts b/functions/src/circlepay/payouts/crons/index.ts index 024d6115..ec76b878 100644 --- a/functions/src/circlepay/payouts/crons/index.ts +++ b/functions/src/circlepay/payouts/crons/index.ts @@ -1 +1 @@ -export { payoutStatusCron } from './payoutStatusCron'; \ No newline at end of file +export {payoutStatusCron} from './payoutStatusCron'; diff --git a/functions/src/circlepay/payouts/crons/payoutStatusCron.ts b/functions/src/circlepay/payouts/crons/payoutStatusCron.ts index b8272005..046c2012 100644 --- a/functions/src/circlepay/payouts/crons/payoutStatusCron.ts +++ b/functions/src/circlepay/payouts/crons/payoutStatusCron.ts @@ -1,24 +1,26 @@ import * as functions from 'firebase-functions'; -import { payoutDb } from '../database'; -import { updatePayoutStatus } from '../business/updatePayoutStatus'; +import {payoutDb} from '../database'; +import {updatePayoutStatus} from '../business/updatePayoutStatus'; // Update the payout statuses every 12 hours export const payoutStatusCron = functions.pubsub .schedule('0 */12 * * *') .onRun(async () => { const pendingPayouts = await payoutDb.getMany({ - status: 'pending' + status: 'pending', }); if (pendingPayouts && pendingPayouts.length) { const promiseArr: Promise[] = []; - pendingPayouts.forEach(payout => { - promiseArr.push((async () => { - logger.info(`Updating the status of payout ${payout.id}`); + pendingPayouts.forEach((payout) => { + promiseArr.push( + (async () => { + logger.info(`Updating the status of payout ${payout.id}`); - await updatePayoutStatus(payout); - })()); + await updatePayoutStatus(payout); + })(), + ); }); } }); diff --git a/functions/src/circlepay/payouts/database/addPayout.ts b/functions/src/circlepay/payouts/database/addPayout.ts index e307bf79..693ff2a2 100644 --- a/functions/src/circlepay/payouts/database/addPayout.ts +++ b/functions/src/circlepay/payouts/database/addPayout.ts @@ -1,11 +1,11 @@ -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { BaseEntityType, SharedOmit } from '../../../util/types'; +import {BaseEntityType, SharedOmit} from '../../../util/types'; -import { IPayoutEntity } from '../types'; -import { PayoutsCollection } from './index'; +import {IPayoutEntity} from '../types'; +import {PayoutsCollection} from './index'; /** * Prepares the passed payout for saving and saves it. Please note that @@ -13,23 +13,23 @@ import { PayoutsCollection } from './index'; * * @param payout - the payout to be saved */ -export const addPayout = async (payout: SharedOmit): Promise => { +export const addPayout = async ( + payout: SharedOmit, +): Promise => { const payoutDoc: IPayoutEntity = { id: v4(), createdAt: Timestamp.now(), updatedAt: Timestamp.now(), - ...payout + ...payout, }; if (process.env.NODE_ENV === 'test') { payoutDoc['testCreated'] = true; } - await PayoutsCollection - .doc(payoutDoc.id) - .set(payoutDoc); + await PayoutsCollection.doc(payoutDoc.id).set(payoutDoc); return payoutDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/database/getPayout.ts b/functions/src/circlepay/payouts/database/getPayout.ts index 9a019a86..97df70ac 100644 --- a/functions/src/circlepay/payouts/database/getPayout.ts +++ b/functions/src/circlepay/payouts/database/getPayout.ts @@ -1,7 +1,7 @@ -import { ArgumentError, NotFoundError } from '../../../util/errors'; +import {ArgumentError, NotFoundError} from '../../../util/errors'; -import { IPayoutEntity } from '../types'; -import { PayoutsCollection } from './index'; +import {IPayoutEntity} from '../types'; +import {PayoutsCollection} from './index'; /** * Gets payout by id @@ -20,13 +20,11 @@ export const getPayout = async (payoutId: string): Promise => { throw new ArgumentError('payoutId', payoutId); } - const payout = (await PayoutsCollection - .doc(payoutId) - .get()).data(); + const payout = (await PayoutsCollection.doc(payoutId).get()).data(); if (!payout) { throw new NotFoundError(payoutId, 'payout'); } return payout; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/database/getPayouts.ts b/functions/src/circlepay/payouts/database/getPayouts.ts index ddfb6f4e..9cf6082f 100644 --- a/functions/src/circlepay/payouts/database/getPayouts.ts +++ b/functions/src/circlepay/payouts/database/getPayouts.ts @@ -1,5 +1,5 @@ -import { PayoutsCollection } from './index'; -import { IPayoutEntity, PayoutStatus } from '../types'; +import {PayoutsCollection} from './index'; +import {IPayoutEntity, PayoutStatus} from '../types'; interface IGetPayoutsOptions { proposalId?: string; @@ -11,7 +11,9 @@ interface IGetPayoutsOptions { * * @param options - The options for which to retrieve payouts */ -export const getPayouts = async (options: IGetPayoutsOptions): Promise => { +export const getPayouts = async ( + options: IGetPayoutsOptions, +): Promise => { let payoutsQuery: any = PayoutsCollection; if (options.proposalId) { @@ -26,6 +28,5 @@ export const getPayouts = async (options: IGetPayoutsOptions): Promise x.data()); -}; \ No newline at end of file + return (await payoutsQuery.get()).docs.map((x) => x.data()); +}; diff --git a/functions/src/circlepay/payouts/database/index.ts b/functions/src/circlepay/payouts/database/index.ts index 56496188..40804fb4 100644 --- a/functions/src/circlepay/payouts/database/index.ts +++ b/functions/src/circlepay/payouts/database/index.ts @@ -1,24 +1,29 @@ -import { db } from '../../../util'; -import { Collections } from '../../../constants'; -import { IPayoutEntity } from '../types'; -import { addPayout } from './addPayout'; -import { getPayouts } from './getPayouts'; -import { getPayout } from './getPayout'; -import { updatePayout } from './updatePayout'; +import {db} from '../../../util'; +import {Collections} from '../../../constants'; +import {IPayoutEntity} from '../types'; +import {addPayout} from './addPayout'; +import {getPayouts} from './getPayouts'; +import {getPayout} from './getPayout'; +import {updatePayout} from './updatePayout'; -export const PayoutsCollection = db.collection(Collections.Payouts) +export const PayoutsCollection = db + .collection(Collections.Payouts) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IPayoutEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IPayoutEntity { return snapshot.data() as IPayoutEntity; }, - toFirestore(object: IPayoutEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: IPayoutEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); export const payoutDb = { add: addPayout, update: updatePayout, get: getPayout, - getMany: getPayouts -}; \ No newline at end of file + getMany: getPayouts, +}; diff --git a/functions/src/circlepay/payouts/database/updatePayout.ts b/functions/src/circlepay/payouts/database/updatePayout.ts index 45619ea2..3d83a18b 100644 --- a/functions/src/circlepay/payouts/database/updatePayout.ts +++ b/functions/src/circlepay/payouts/database/updatePayout.ts @@ -1,23 +1,23 @@ -import { firestore } from 'firebase-admin'; +import {firestore} from 'firebase-admin'; -import { IPayoutEntity } from './../types'; -import { PayoutsCollection } from './index'; +import {IPayoutEntity} from './../types'; +import {PayoutsCollection} from './index'; /** * Updates the payout in the backing store * * @param payout - The updated payout */ -export const updatePayout = async (payout: IPayoutEntity): Promise => { +export const updatePayout = async ( + payout: IPayoutEntity, +): Promise => { const payoutDoc = { ...payout, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; - await PayoutsCollection - .doc(payoutDoc.id) - .update(payoutDoc); + await PayoutsCollection.doc(payoutDoc.id).update(payoutDoc); return payoutDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/triggers/index.ts b/functions/src/circlepay/payouts/triggers/index.ts index 97e971fd..4b23f32f 100644 --- a/functions/src/circlepay/payouts/triggers/index.ts +++ b/functions/src/circlepay/payouts/triggers/index.ts @@ -1,9 +1,9 @@ import * as functions from 'firebase-functions'; -import { Collections } from '../../../constants'; -import { IEventEntity } from '../../../event/type'; -import { EVENT_TYPES } from '../../../event/event'; -import { onPayoutCreated } from './onPayoutCreated'; -import { onPayoutApproved } from './onPayoutApproved'; +import {Collections} from '../../../constants'; +import {IEventEntity} from '../../../event/type'; +import {EVENT_TYPES} from '../../../event/event'; +import {onPayoutCreated} from './onPayoutCreated'; +import {onPayoutApproved} from './onPayoutApproved'; export const payoutTriggers = functions.firestore .document(`/${Collections.Event}/{id}`) @@ -24,4 +24,4 @@ export const payoutTriggers = functions.firestore default: break; } - }); \ No newline at end of file + }); diff --git a/functions/src/circlepay/payouts/triggers/onPayoutApproved.ts b/functions/src/circlepay/payouts/triggers/onPayoutApproved.ts index 65183253..ca40f1b6 100644 --- a/functions/src/circlepay/payouts/triggers/onPayoutApproved.ts +++ b/functions/src/circlepay/payouts/triggers/onPayoutApproved.ts @@ -1,19 +1,22 @@ -import { IEventTrigger } from '../../../util/types'; -import { EVENT_TYPES } from '../../../event/event'; -import { CommonError } from '../../../util/errors'; -import { env } from '../../../constants'; -import { payoutDb } from '../database'; -import { executePayout } from '../business/executePayout'; +import {IEventTrigger} from '../../../util/types'; +import {EVENT_TYPES} from '../../../event/event'; +import {CommonError} from '../../../util/errors'; +import {env} from '../../../constants'; +import {payoutDb} from '../database'; +import {executePayout} from '../business/executePayout'; export const onPayoutApproved: IEventTrigger = async (eventObj) => { - if(eventObj.type !== EVENT_TYPES.PAYOUT_APPROVED) { + if (eventObj.type !== EVENT_TYPES.PAYOUT_APPROVED) { throw new CommonError(`onPayoutApproved was executed on ${eventObj.type}`); } const payout = await payoutDb.get(eventObj.objectId); // Check if there are enough approvals for execution (and execute it if there are) - if(payout.security.filter(x => x.redeemed).length >= env.payouts.neededApprovals) { + if ( + payout.security.filter((x) => x.redeemed).length >= + env.payouts.neededApprovals + ) { await executePayout(payout); } -} \ No newline at end of file +}; diff --git a/functions/src/circlepay/payouts/triggers/onPayoutCreated.ts b/functions/src/circlepay/payouts/triggers/onPayoutCreated.ts index e9ba14c0..0129f38a 100644 --- a/functions/src/circlepay/payouts/triggers/onPayoutCreated.ts +++ b/functions/src/circlepay/payouts/triggers/onPayoutCreated.ts @@ -1,34 +1,34 @@ -import { IEventTrigger } from '../../../util/types'; -import { EVENT_TYPES } from '../../../event/event'; -import { CommonError } from '../../../util/errors'; -import { env } from '../../../constants'; +import {IEventTrigger} from '../../../util/types'; +import {EVENT_TYPES} from '../../../event/event'; +import {CommonError} from '../../../util/errors'; +import {env} from '../../../constants'; import emailClient from './../../../notification/email'; -import { payoutDb } from '../database'; -import { bankAccountDb } from '../../backAccounts/database'; -import { proposalDb } from '../../../proposals/database'; -import { IProposalPayoutEntity } from '../types'; -import { commonDb } from '../../../common/database'; +import {payoutDb} from '../database'; +import {bankAccountDb} from '../../backAccounts/database'; +import {proposalDb} from '../../../proposals/database'; +import {IProposalPayoutEntity} from '../types'; +import {commonDb} from '../../../common/database'; export const onPayoutCreated: IEventTrigger = async (eventObj) => { - if(eventObj.type !== EVENT_TYPES.PAYOUT_CREATED) { + if (eventObj.type !== EVENT_TYPES.PAYOUT_CREATED) { throw new CommonError(`onPayoutCreated was executed on ${eventObj.type}`); } const payout = await payoutDb.get(eventObj.objectId); const wire = await bankAccountDb.get(payout.destination.id); - const proposal = payout.type === 'proposal' - ? await proposalDb.getProposal((payout as IProposalPayoutEntity).proposalId) - : null; + const proposal = + payout.type === 'proposal' + ? await proposalDb.getProposal( + (payout as IProposalPayoutEntity).proposalId, + ) + : null; - const common = proposal - ? await commonDb.get(proposal.commonId) - : null; + const common = proposal ? await commonDb.get(proposal.commonId) : null; - env.payouts.approvers.map((async (approver, index) => { - const urlBase = process.env.NODE_ENV === 'dev' - ? env.local - : env.endpoints.base; + env.payouts.approvers.map(async (approver, index) => { + const urlBase = + process.env.NODE_ENV === 'dev' ? env.local : env.endpoints.base; await emailClient.sendTemplatedEmail({ templateKey: 'approvePayout', @@ -40,17 +40,19 @@ export const onPayoutCreated: IEventTrigger = async (eventObj) => { ? `${(proposal.description as any).title} (${proposal.id})` : 'Independent Payout', - common: common - ? `${common.name} (${common.id})` - : 'Independent Payout', + common: common ? `${common.name} (${common.id})` : 'Independent Payout', - bankDescription: wire.description || 'The bank account has no description', + bankDescription: + wire.description || 'The bank account has no description', bank: wire.bank.bankName, payoutId: payout.id, - amount: (payout.amount / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }), - url: `${urlBase}/circlepay/payouts/approve?payoutId=${payout.id}&index=${index}&token=${payout.security[index].token}` - } + amount: (payout.amount / 100).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }), + url: `${urlBase}/circlepay/payouts/approve?payoutId=${payout.id}&index=${index}&token=${payout.security[index].token}`, + }, }); - })); -} \ No newline at end of file + }); +}; diff --git a/functions/src/circlepay/payouts/types.ts b/functions/src/circlepay/payouts/types.ts index 2c3fe7b2..2ff21798 100644 --- a/functions/src/circlepay/payouts/types.ts +++ b/functions/src/circlepay/payouts/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../../util/types'; +import {IBaseEntity} from '../../util/types'; export type PayoutStatus = 'pending' | 'complete' | 'failed'; export type PayoutType = 'independent' | 'proposal'; @@ -54,7 +54,6 @@ interface IUnexecutedPayout extends IPayoutBaseEntity { type IExecutablePayoutEntity = IUnexecutedPayout | IExecutedPayout; - /** * Security details about the payout, needed for the * execution of it @@ -100,15 +99,15 @@ export interface IPayoutDestination { } export type IProposalPayoutEntity = IExecutablePayoutEntity & { - type: 'proposal' + type: 'proposal'; /** * The ID of the proposal, for witch the payout * is made */ proposalId: string; -} +}; type IIndependentPayoutEntity = IExecutablePayoutEntity; -export type IPayoutEntity = IProposalPayoutEntity | IIndependentPayoutEntity; \ No newline at end of file +export type IPayoutEntity = IProposalPayoutEntity | IIndependentPayoutEntity; diff --git a/functions/src/circlepay/types.ts b/functions/src/circlepay/types.ts index 330a7f79..e7fdcf41 100644 --- a/functions/src/circlepay/types.ts +++ b/functions/src/circlepay/types.ts @@ -1,7 +1,7 @@ // ---- Card related ---- // -import { CirclePaymentStatus } from '../util/types'; -import { CircleCvvCheck } from './client/getCardFromCircle'; +import {CirclePaymentStatus} from '../util/types'; +import {CircleCvvCheck} from './client/getCardFromCircle'; // ---- Shared @@ -22,7 +22,7 @@ export interface ICircleCreateCardResponse { metadata: { email: string; phoneNumber: string; - } + }; expMonth: number; expYear: number; @@ -33,8 +33,8 @@ export interface ICircleCreateCardResponse { verification: { cvv: CircleCvvCheck; - } - } + }; + }; } export interface ICircleCreateCardPayload { @@ -56,7 +56,7 @@ export interface ICircleCreateCardPayload { */ encryptedData: string; - billingDetails: ICircleBillingDetails + billingDetails: ICircleBillingDetails; /** * Two digit number representing the card's expiration month. @@ -68,7 +68,6 @@ export interface ICircleCreateCardPayload { */ expYear: number; - metadata: ICircleMetadata; } @@ -146,7 +145,6 @@ interface ICircleCreatePaymentBase { amount: IPaymentAmount; source: IPaymentSource; - idempotencyKey: string; } @@ -161,7 +159,9 @@ interface ICircleCreatePaymentNoVerification extends ICircleCreatePaymentBase { verification: 'none'; } -export type ICircleCreatePaymentPayload = ICircleCreatePaymentVerification | ICircleCreatePaymentNoVerification; +export type ICircleCreatePaymentPayload = + | ICircleCreatePaymentVerification + | ICircleCreatePaymentNoVerification; export type ICircleCreatePaymentResponse = ICirclePayment; @@ -207,7 +207,6 @@ interface ICircleMetadata { type IPayoutAmount = IAmount; type IPayoutStatus = 'pending' | 'failed' | 'complete'; - interface ICirclePayoutDestination { type: 'wire'; @@ -232,7 +231,7 @@ export interface ICircleCreatePayoutResponse { id: string; status: IPayoutStatus; destination: ICirclePayoutDestination; - } + }; } export interface ICircleGetPayoutResponse { @@ -241,5 +240,5 @@ export interface ICircleGetPayoutResponse { destination: ICirclePayoutDestination; amount: IPayoutAmount; status: IPayoutStatus; - } -} \ No newline at end of file + }; +} diff --git a/functions/src/common/business/addCommonMember.ts b/functions/src/common/business/addCommonMember.ts index 5c1ec5f1..8936a46c 100644 --- a/functions/src/common/business/addCommonMember.ts +++ b/functions/src/common/business/addCommonMember.ts @@ -1,9 +1,9 @@ -import { ICommonEntity } from '../types'; -import { commonDb } from '../database'; -import { proposalDb } from '../../proposals/database'; -import { CommonError } from '../../util/errors'; -import { createEvent } from '../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../event/event'; +import {ICommonEntity} from '../types'; +import {commonDb} from '../database'; +import {proposalDb} from '../../proposals/database'; +import {CommonError} from '../../util/errors'; +import {createEvent} from '../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../event/event'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; @@ -14,13 +14,18 @@ import Timestamp = admin.firestore.Timestamp; * * @throws { CommonError } - If the passed proposal is not approved */ -export const addCommonMemberByProposalId = async (proposalId: string): Promise => { +export const addCommonMemberByProposalId = async ( + proposalId: string, +): Promise => { const proposal = await proposalDb.getProposal(proposalId); - if(proposal.state !== 'passed') { - throw new CommonError('Cannot add user from proposal, for witch the proposal is not approved', { - proposalId - }); + if (proposal.state !== 'passed') { + throw new CommonError( + 'Cannot add user from proposal, for witch the proposal is not approved', + { + proposalId, + }, + ); } const common = await commonDb.get(proposal.commonId); @@ -28,7 +33,7 @@ export const addCommonMemberByProposalId = async (proposalId: string): Promise => { - if(!common.members.includes({ userId })) { +const addCommonMember = async ( + common: ICommonEntity, + userId: string, +): Promise => { + if (!common.members.includes({userId})) { common.members.push({ - userId, - joinedAt: Timestamp.now() + userId, + joinedAt: Timestamp.now(), }); await commonDb.update(common); @@ -54,9 +62,9 @@ const addCommonMember = async (common: ICommonEntity, userId: string): Promise => { // Find and delete all proposals for the common const proposals = await proposalDb.getProposals({ - commonId: common.id + commonId: common.id, }); const deleteProposalPromiseArr: Promise[] = []; - proposals.forEach(proposal => deleteProposalPromiseArr.push(deleteProposal(proposal))); + proposals.forEach((proposal) => + deleteProposalPromiseArr.push(deleteProposal(proposal)), + ); await Promise.all(deleteProposalPromiseArr); @@ -21,6 +23,6 @@ export const deleteCommon = async (common: ICommonEntity): Promise => { // Everything is deleted. Log success logger.info('Common and related entities successfully deleted!', { common, - proposals + proposals, }); -}; \ No newline at end of file +}; diff --git a/functions/src/common/business/index.ts b/functions/src/common/business/index.ts index c49b9ce2..bb314d04 100644 --- a/functions/src/common/business/index.ts +++ b/functions/src/common/business/index.ts @@ -1,3 +1,3 @@ -export { isCommonMember } from './isCommonMember'; -export { createCommon } from './createCommon'; -export { updateCommon } from './updateCommon'; \ No newline at end of file +export {isCommonMember} from './isCommonMember'; +export {createCommon} from './createCommon'; +export {updateCommon} from './updateCommon'; diff --git a/functions/src/common/business/isCommonMember.ts b/functions/src/common/business/isCommonMember.ts index 6fd14277..a28302bd 100644 --- a/functions/src/common/business/isCommonMember.ts +++ b/functions/src/common/business/isCommonMember.ts @@ -1,6 +1,6 @@ -import { CommonError } from '../../util/errors'; +import {CommonError} from '../../util/errors'; -import { ICommonEntity } from '../types'; +import {ICommonEntity} from '../types'; /** * Check if the user is part of the common @@ -12,10 +12,16 @@ import { ICommonEntity } from '../types'; * * @throws { CommonError } - If the user is not part of the common and the *throws* param is `true` */ -export const isCommonMember = (common: ICommonEntity, userId: string, throws = false): boolean => { - if (!common.members.find(member => member.userId === userId)) { +export const isCommonMember = ( + common: ICommonEntity, + userId: string, + throws = false, +): boolean => { + if (!common.members.find((member) => member.userId === userId)) { if (throws) { - throw new CommonError(`User (${userId}) is not part of common (${common})`); + throw new CommonError( + `User (${userId}) is not part of common (${common})`, + ); } else { return false; } diff --git a/functions/src/common/business/removeCommonMember.ts b/functions/src/common/business/removeCommonMember.ts index 22904f73..a376d7ab 100644 --- a/functions/src/common/business/removeCommonMember.ts +++ b/functions/src/common/business/removeCommonMember.ts @@ -1,7 +1,7 @@ -import { ICommonEntity } from '../types'; -import { commonDb } from '../database'; -import { EVENT_TYPES } from '../../event/event'; -import { createEvent } from '../../util/db/eventDbService'; +import {ICommonEntity} from '../types'; +import {commonDb} from '../database'; +import {EVENT_TYPES} from '../../event/event'; +import {createEvent} from '../../util/db/eventDbService'; /** * Removes user from the common @@ -10,11 +10,14 @@ import { createEvent } from '../../util/db/eventDbService'; * @param memberId - The id of the member to remove * */ -export const removeCommonMember = async (common: ICommonEntity, memberId: string): Promise => { - if (!common.members.some(x => x.userId === memberId)) { +export const removeCommonMember = async ( + common: ICommonEntity, + memberId: string, +): Promise => { + if (!common.members.some((x) => x.userId === memberId)) { logger.error('Trying to remove non member from common', { common, - memberId + memberId, }); return; @@ -22,13 +25,13 @@ export const removeCommonMember = async (common: ICommonEntity, memberId: string logger.info('Removing common member', { common, - member: memberId + member: memberId, }); // Remove the member common.members.splice( - common.members.findIndex(x => x.userId === memberId), - 1 + common.members.findIndex((x) => x.userId === memberId), + 1, ); // Persist the changes @@ -38,6 +41,6 @@ export const removeCommonMember = async (common: ICommonEntity, memberId: string await createEvent({ userId: memberId, objectId: common.id, - type: EVENT_TYPES.COMMON_MEMBER_REMOVED + type: EVENT_TYPES.COMMON_MEMBER_REMOVED, }); }; diff --git a/functions/src/common/business/updateCommon.ts b/functions/src/common/business/updateCommon.ts index 691d75c5..652e4b1d 100644 --- a/functions/src/common/business/updateCommon.ts +++ b/functions/src/common/business/updateCommon.ts @@ -1,10 +1,11 @@ +import {commonDb} from '../database'; +import {ICommonEntity} from '../types'; +import {createEvent} from '../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../event/event'; -import { commonDb } from '../database'; -import { ICommonEntity } from '../types'; -import { createEvent } from '../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../event/event'; - -export const updateCommon = async (common: ICommonEntity) : Promise => { +export const updateCommon = async ( + common: ICommonEntity, +): Promise => { // should we validate here like in createCommon? // check if user is owner of common const updatedCommon = await commonDb.update(common); @@ -17,8 +18,8 @@ export const updateCommon = async (common: ICommonEntity) : Promise): Promise => { +export const addCommon = async ( + common: Omit, +): Promise => { const commonDoc: ICommonEntity = { id: v4(), @@ -25,16 +26,14 @@ export const addCommon = async (common: Omit): Promise => { +export const deleteCommonFromDatabase = async ( + commonId: Nullable, +): Promise => { if (!commonId) { throw new ArgumentError('commonId'); } logger.notice(`Deleting common with ID ${commonId}`); - return (await CommonCollection - .doc(commonId) - .delete()); -}; \ No newline at end of file + return await CommonCollection.doc(commonId).delete(); +}; diff --git a/functions/src/common/database/getCommon.ts b/functions/src/common/database/getCommon.ts index 478c58b3..f8297811 100644 --- a/functions/src/common/database/getCommon.ts +++ b/functions/src/common/database/getCommon.ts @@ -1,8 +1,8 @@ -import { ICommonEntity } from '../types'; -import { ArgumentError } from '../../util/errors/ArgumentError'; -import { commonCollection } from './index'; -import { Nullable } from '../../util/types'; -import { NotFoundError } from '../../util/errors/NotFoundError'; +import {ICommonEntity} from '../types'; +import {ArgumentError} from '../../util/errors/ArgumentError'; +import {commonCollection} from './index'; +import {Nullable} from '../../util/types'; +import {NotFoundError} from '../../util/errors/NotFoundError'; /** * Gets common by id @@ -15,17 +15,17 @@ import { NotFoundError } from '../../util/errors/NotFoundError'; * @returns - The found common */ export const getCommon = async (commonId: string): Promise => { - if(!commonId) { + if (!commonId) { throw new ArgumentError('commonId', commonId); } - const common = (await commonCollection - .doc(commonId) - .get()).data() as Nullable; + const common = ( + await commonCollection.doc(commonId).get() + ).data() as Nullable; - if(!common) { + if (!common) { throw new NotFoundError(commonId, 'common'); } return common; -} \ No newline at end of file +}; diff --git a/functions/src/common/database/index.ts b/functions/src/common/database/index.ts index 7e78d727..f03124e1 100644 --- a/functions/src/common/database/index.ts +++ b/functions/src/common/database/index.ts @@ -1,10 +1,10 @@ -import { db } from '../../util'; -import { Collections } from '../../constants'; +import {db} from '../../util'; +import {Collections} from '../../constants'; -import { addCommon } from './addCommon'; -import { getCommon } from './getCommon'; -import { updateCommon } from './updateCommon'; -import { deleteCommonFromDatabase } from './deleteCommon'; +import {addCommon} from './addCommon'; +import {getCommon} from './getCommon'; +import {updateCommon} from './updateCommon'; +import {deleteCommonFromDatabase} from './deleteCommon'; export const commonCollection = db.collection(Collections.Commons); @@ -12,5 +12,5 @@ export const commonDb = { add: addCommon, get: getCommon, update: updateCommon, - delete: deleteCommonFromDatabase + delete: deleteCommonFromDatabase, }; diff --git a/functions/src/common/database/updateCommon.ts b/functions/src/common/database/updateCommon.ts index 285d4918..868cf7c6 100644 --- a/functions/src/common/database/updateCommon.ts +++ b/functions/src/common/database/updateCommon.ts @@ -1,23 +1,23 @@ -import { firestore } from 'firebase-admin'; +import {firestore} from 'firebase-admin'; -import { ICommonEntity } from '../types'; -import { commonCollection } from './index'; +import {ICommonEntity} from '../types'; +import {commonCollection} from './index'; /** * Updates the common in the backing store * * @param common - The updated common */ -export const updateCommon = async (common: ICommonEntity): Promise => { +export const updateCommon = async ( + common: ICommonEntity, +): Promise => { const commonEntity = { ...common, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; - await commonCollection - .doc(commonEntity.id) - .update(commonEntity); + await commonCollection.doc(commonEntity.id).update(commonEntity); return commonEntity; -} \ No newline at end of file +}; diff --git a/functions/src/common/index.ts b/functions/src/common/index.ts index 6fe938bc..5cc613e5 100644 --- a/functions/src/common/index.ts +++ b/functions/src/common/index.ts @@ -1,14 +1,14 @@ import * as functions from 'firebase-functions'; -import { env } from '../constants'; -import { commonApp, commonRouter } from '../util'; -import { runtimeOptions } from '../constants'; -import { responseExecutor } from '../util/responseExecutor'; +import {env} from '../constants'; +import {commonApp, commonRouter} from '../util'; +import {runtimeOptions} from '../constants'; +import {responseExecutor} from '../util/responseExecutor'; -import { createCommon, updateCommon } from './business'; +import {createCommon, updateCommon} from './business'; import * as triggers from './triggers'; -import { commonDb } from './database'; -import { deleteCommon } from './business/deleteCommon'; +import {commonDb} from './database'; +import {deleteCommon} from './business/deleteCommon'; const router = commonRouter(); @@ -17,48 +17,58 @@ router.post('/create', async (req, res, next) => { async () => { return await createCommon({ ...req.body, - userId: req.user.uid + userId: req.user.uid, }); - }, { + }, + { req, res, next, - successMessage: 'Common created successfully' - }); + successMessage: 'Common created successfully', + }, + ); }); if (env.environment === 'staging' || env.environment === 'dev') { router.delete('/delete', async (req, res, next) => { await responseExecutor( async () => { - logger.notice(`User ${req.user.uid} is trying to delete common with ID ${req.query.commonId}`); + logger.notice( + `User ${req.user.uid} is trying to delete common with ID ${req.query.commonId}`, + ); const common = await commonDb.get(req.query.commonId as string); - return deleteCommon(common) - }, { + return deleteCommon(common); + }, + { req, res, next, - successMessage: 'Common deleted successfully' - }); + successMessage: 'Common deleted successfully', + }, + ); }); } -router.post('/update', async (req, res, next) => ( - await responseExecutor( - async () => { - return await updateCommon(req.body); - }, { - req, - res, - next, - successMessage: 'Common updated successfully' - }) -)); +router.post( + '/update', + async (req, res, next) => + await responseExecutor( + async () => { + return await updateCommon(req.body); + }, + { + req, + res, + next, + successMessage: 'Common updated successfully', + }, + ), +); export const commonsApp = functions .runWith(runtimeOptions) .https.onRequest(commonApp(router)); -export const commonTriggers = triggers; \ No newline at end of file +export const commonTriggers = triggers; diff --git a/functions/src/common/triggers/index.ts b/functions/src/common/triggers/index.ts index 9ad97589..eef6cb3f 100644 --- a/functions/src/common/triggers/index.ts +++ b/functions/src/common/triggers/index.ts @@ -1 +1 @@ -export { onCommonWhitelisted } from './onCommonWhitelisted'; \ No newline at end of file +export {onCommonWhitelisted} from './onCommonWhitelisted'; diff --git a/functions/src/common/triggers/onCommonWhitelisted.ts b/functions/src/common/triggers/onCommonWhitelisted.ts index 77f73774..66ff4599 100644 --- a/functions/src/common/triggers/onCommonWhitelisted.ts +++ b/functions/src/common/triggers/onCommonWhitelisted.ts @@ -1,8 +1,8 @@ import * as functions from 'firebase-functions'; -import { Collections } from '../../constants'; -import { ICommonEntity } from '../../common/types'; -import { EVENT_TYPES } from '../../event/event'; -import { createEvent } from '../../util/db/eventDbService'; +import {Collections} from '../../constants'; +import {ICommonEntity} from '../../common/types'; +import {EVENT_TYPES} from '../../event/event'; +import {createEvent} from '../../util/db/eventDbService'; export const onCommonWhitelisted = functions.firestore .document(`/${Collections.Commons}/{id}`) @@ -10,13 +10,14 @@ export const onCommonWhitelisted = functions.firestore const prevCommon = update.before.data() as ICommonEntity; const currCommon = update.after.data() as ICommonEntity; - if (currCommon.register === 'registered' && - currCommon.register !== prevCommon.register) { - - await createEvent({ - userId: currCommon.metadata.founderId, - objectId: currCommon.id, - type: EVENT_TYPES.COMMON_WHITELISTED - }); + if ( + currCommon.register === 'registered' && + currCommon.register !== prevCommon.register + ) { + await createEvent({ + userId: currCommon.metadata.founderId, + objectId: currCommon.id, + type: EVENT_TYPES.COMMON_WHITELISTED, + }); } - }); \ No newline at end of file + }); diff --git a/functions/src/common/types.ts b/functions/src/common/types.ts index 147e7cc0..75d7093e 100644 --- a/functions/src/common/types.ts +++ b/functions/src/common/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../util/types'; +import {IBaseEntity} from '../util/types'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; @@ -120,4 +120,4 @@ export type ContributionType = 'one-time' | 'monthly'; * na - The common is not whitelisted and thus visible only to members * registered - The common is whitelisted and part of the featured list */ -export type CommonRegister = 'na' | 'registered'; \ No newline at end of file +export type CommonRegister = 'na' | 'registered'; diff --git a/functions/src/constants/collections.ts b/functions/src/constants/collections.ts index cd8d4455..7b273341 100644 --- a/functions/src/constants/collections.ts +++ b/functions/src/constants/collections.ts @@ -12,5 +12,5 @@ export const Collections = { Cards: 'cards', Users: 'users', Discussion: 'discussion', - Deleted: 'deleted' -}; \ No newline at end of file + Deleted: 'deleted', +}; diff --git a/functions/src/constants/index.ts b/functions/src/constants/index.ts index d4d69ee6..3eed0a21 100644 --- a/functions/src/constants/index.ts +++ b/functions/src/constants/index.ts @@ -15,7 +15,7 @@ interface Env { app: { currentVersion: string; oldestSupportedVersion: string; - } + }; }; mail: { @@ -41,12 +41,12 @@ interface Env { countdownPeriod: number; quietEndingPeriod: number; }; - } + }; endpoints: { base: string; notifications: string; - } + }; secretManagerProject: string; local: string; @@ -54,11 +54,11 @@ interface Env { payouts: { approvers: string[]; neededApprovals: number; - } + }; backoffice: { sheetUrl: string; - } + }; } export const env = merge(envConfig, envSecrets) as Env; @@ -72,7 +72,7 @@ export const StatusCodes = { NotFound: 404, UnprocessableEntity: 422, - Ok: 200 + Ok: 200, }; export const ErrorCodes = { @@ -88,26 +88,21 @@ export const ErrorCodes = { CvvVerificationFail: 'CvvVerificationFail', // ---- External providers errors - CirclePayError: 'External.CirclePayError' + CirclePayError: 'External.CirclePayError', }; export const ProposalTypes = { Join: 'join', - Funding: 'fundingRequest' + Funding: 'fundingRequest', }; -export const ProposalActiveStates = [ - 'countdown' -]; +export const ProposalActiveStates = ['countdown']; -export const ProposalFinalStates = [ - 'passed', - 'failed' -]; +export const ProposalFinalStates = ['passed', 'failed']; // ---- Reexports -export { runtimeOptions } from './runtimeOptions'; -export { Collections } from './collections'; -export { Notifications } from './notifications'; +export {runtimeOptions} from './runtimeOptions'; +export {Collections} from './collections'; +export {Notifications} from './notifications'; -export { adminKeys }; +export {adminKeys}; diff --git a/functions/src/constants/notifications.ts b/functions/src/constants/notifications.ts index 04100026..c29dd929 100644 --- a/functions/src/constants/notifications.ts +++ b/functions/src/constants/notifications.ts @@ -1,4 +1,4 @@ export const Notifications = { - messageLimit: 10, - maxNotifications: 5 -} \ No newline at end of file + messageLimit: 10, + maxNotifications: 5, +}; diff --git a/functions/src/constants/runtimeOptions.ts b/functions/src/constants/runtimeOptions.ts index 6dea0a5b..bbf3a61d 100644 --- a/functions/src/constants/runtimeOptions.ts +++ b/functions/src/constants/runtimeOptions.ts @@ -1,3 +1,3 @@ export const runtimeOptions = { - timeoutSeconds: 540 -}; \ No newline at end of file + timeoutSeconds: 540, +}; diff --git a/functions/src/crons/index.ts b/functions/src/crons/index.ts index f36aa905..1f261a95 100644 --- a/functions/src/crons/index.ts +++ b/functions/src/crons/index.ts @@ -1 +1 @@ -export { backup } from './backupCron'; \ No newline at end of file +export {backup} from './backupCron'; diff --git a/functions/src/custom.d.ts b/functions/src/custom.d.ts index c62f5502..b63fa5f1 100644 --- a/functions/src/custom.d.ts +++ b/functions/src/custom.d.ts @@ -1,17 +1,16 @@ -import { ILogger } from './util/logger'; +import {ILogger} from './util/logger'; declare global { declare namespace Express { export interface Request { user: { uid: string; - } + }; requestId: string; } } - declare namespace NodeJS { export interface ProcessEnv { NODE_ENV: 'dev' | 'production' | 'staging' | 'test'; @@ -23,4 +22,4 @@ declare global { } declare const logger: ILogger; -} \ No newline at end of file +} diff --git a/functions/src/discussion/database/getDiscussion.ts b/functions/src/discussion/database/getDiscussion.ts index 48f01bac..7ad27ee3 100644 --- a/functions/src/discussion/database/getDiscussion.ts +++ b/functions/src/discussion/database/getDiscussion.ts @@ -1,9 +1,9 @@ -import { ArgumentError } from '../../util/errors'; -import { Nullable } from '../../util/types'; -import { IDiscussionEntity } from '../types'; -import { IProposalEntity } from '../../proposals/proposalTypes'; -import { NotFoundError } from '../../util/errors'; -import { discussionCollection } from './index'; +import {ArgumentError} from '../../util/errors'; +import {Nullable} from '../../util/types'; +import {IDiscussionEntity} from '../types'; +import {IProposalEntity} from '../../proposals/proposalTypes'; +import {NotFoundError} from '../../util/errors'; +import {discussionCollection} from './index'; interface IGetDiscussionOptions { /** @@ -13,28 +13,30 @@ interface IGetDiscussionOptions { } const defaultDiscussionOptions: IGetDiscussionOptions = { - throwOnFailure: true -} + throwOnFailure: true, +}; // discussion can be an Discussion doc or a discussion from a Proposal doc -export const getDiscussion = async (discussionId: string, customOptions?: Partial) : Promise => { - if(!discussionId) { +export const getDiscussion = async ( + discussionId: string, + customOptions?: Partial, +): Promise => { + if (!discussionId) { throw new ArgumentError('discussionId', discussionId); } const options = { ...defaultDiscussionOptions, - ...customOptions + ...customOptions, }; - const discussion = (await discussionCollection - .doc(discussionId) - .get()).data() as Nullable + const discussion = ( + await discussionCollection.doc(discussionId).get() + ).data() as Nullable; if (!discussion && options.throwOnFailure) { throw new NotFoundError(discussionId, 'discussion'); } return discussion; - -} +}; diff --git a/functions/src/discussion/database/index.ts b/functions/src/discussion/database/index.ts index 918510d4..ded13f9b 100644 --- a/functions/src/discussion/database/index.ts +++ b/functions/src/discussion/database/index.ts @@ -1,9 +1,9 @@ -import { db } from '../../util'; -import { Collections } from '../../constants'; -import { getDiscussion } from './getDiscussion'; +import {db} from '../../util'; +import {Collections} from '../../constants'; +import {getDiscussion} from './getDiscussion'; export const discussionCollection = db.collection(Collections.Discussion); export const discussionDb = { - getDiscussion -} \ No newline at end of file + getDiscussion, +}; diff --git a/functions/src/discussion/types.ts b/functions/src/discussion/types.ts index bc59b96c..4069cf3d 100644 --- a/functions/src/discussion/types.ts +++ b/functions/src/discussion/types.ts @@ -1,6 +1,6 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { IBaseEntity } from '../util/types'; +import {IBaseEntity} from '../util/types'; export interface IDiscussionEntity extends IBaseEntity { /** @@ -47,5 +47,4 @@ export interface IDiscussionEntity extends IBaseEntity { * Users who follow this discussion */ followers: string[]; - -} \ No newline at end of file +} diff --git a/functions/src/discussionMessage/database/getDiscussionMessages.ts b/functions/src/discussionMessage/database/getDiscussionMessages.ts index 5aca76b4..837bbec4 100644 --- a/functions/src/discussionMessage/database/getDiscussionMessages.ts +++ b/functions/src/discussionMessage/database/getDiscussionMessages.ts @@ -1,27 +1,37 @@ import admin from 'firebase-admin'; -import { discussionMessageCollection } from './index'; -import { IDiscussionMessage } from '../../discussionMessage/types'; +import {discussionMessageCollection} from './index'; +import {IDiscussionMessage} from '../../discussionMessage/types'; import QuerySnapshot = admin.firestore.QuerySnapshot; -export const getDiscussionMessages = async (discussionId: string, limit = 1, startDoc = null) : Promise => { - const discussionMessagesSnapshot = await getDiscussionMessagsSnapshot(discussionId, limit, startDoc); - return discussionMessagesSnapshot - .map(message => message.data() as IDiscussionMessage ); +export const getDiscussionMessages = async ( + discussionId: string, + limit = 1, + startDoc = null, +): Promise => { + const discussionMessagesSnapshot = await getDiscussionMessagsSnapshot( + discussionId, + limit, + startDoc, + ); + return discussionMessagesSnapshot.map( + (message) => message.data() as IDiscussionMessage, + ); }; -export const getDiscussionMessagsSnapshot = async (discussionId: string, limit = 1, startDoc = null) : Promise => { +export const getDiscussionMessagsSnapshot = async ( + discussionId: string, + limit = 1, + startDoc = null, +): Promise => { let discussionMessageQuery = discussionMessageCollection - .where('discussionId', '==', discussionId) - .orderBy('createTime', 'desc'); - + .where('discussionId', '==', discussionId) + .orderBy('createTime', 'desc'); - if (startDoc) { - discussionMessageQuery = discussionMessageQuery.startAfter(startDoc) - } - - return (await discussionMessageQuery - .limit(limit) - .get() as QuerySnapshot) - .docs; -} + if (startDoc) { + discussionMessageQuery = discussionMessageQuery.startAfter(startDoc); + } + return ((await discussionMessageQuery + .limit(limit) + .get()) as QuerySnapshot).docs; +}; diff --git a/functions/src/discussionMessage/database/index.ts b/functions/src/discussionMessage/database/index.ts index 10fe1854..6ffff467 100644 --- a/functions/src/discussionMessage/database/index.ts +++ b/functions/src/discussionMessage/database/index.ts @@ -1,10 +1,15 @@ -import { db } from '../../util'; -import { Collections } from '../../constants'; -import { getDiscussionMessages, getDiscussionMessagsSnapshot } from './getDiscussionMessages'; +import {db} from '../../util'; +import {Collections} from '../../constants'; +import { + getDiscussionMessages, + getDiscussionMessagsSnapshot, +} from './getDiscussionMessages'; -export const discussionMessageCollection = db.collection(Collections.DiscussionMessage); +export const discussionMessageCollection = db.collection( + Collections.DiscussionMessage, +); export const discussionMessageDb = { - getDiscussionMessages, - getDiscussionMessagsSnapshot, -}; \ No newline at end of file + getDiscussionMessages, + getDiscussionMessagsSnapshot, +}; diff --git a/functions/src/discussionMessage/triggers.ts b/functions/src/discussionMessage/triggers.ts index 5f464f77..d80be4d3 100644 --- a/functions/src/discussionMessage/triggers.ts +++ b/functions/src/discussionMessage/triggers.ts @@ -1,7 +1,7 @@ import * as functions from 'firebase-functions'; -import { createEvent } from '../util/db/eventDbService'; -import { EVENT_TYPES } from '../event/event'; +import {createEvent} from '../util/db/eventDbService'; +import {EVENT_TYPES} from '../event/event'; exports.watchForNewMessages = functions.firestore .document('/discussionMessage/{id}') @@ -11,6 +11,6 @@ exports.watchForNewMessages = functions.firestore await createEvent({ userId: discussionMessage.ownerId, objectId: snap.id, - type: EVENT_TYPES.MESSAGE_CREATED + type: EVENT_TYPES.MESSAGE_CREATED, }); - }); \ No newline at end of file + }); diff --git a/functions/src/discussionMessage/types.ts b/functions/src/discussionMessage/types.ts index 0976e461..39af39e4 100644 --- a/functions/src/discussionMessage/types.ts +++ b/functions/src/discussionMessage/types.ts @@ -2,35 +2,33 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; export interface IDiscussionMessage { - /** - * ID of the parent discussion of this message, could be a Discussion ID, or a Proposal ID + * ID of the parent discussion of this message, could be a Discussion ID, or a Proposal ID */ - discussionId: string; + discussionId: string; /** * The ID of the creator of the message */ - ownerId: string; + ownerId: string; /** * The name of the creator of the message */ - ownerName: string; + ownerName: string; /** * The content of the message */ - text: string; + text: string; /** * Time of creation */ - createTime: Timestamp; + createTime: Timestamp; /** * Image URLs of the user's avatar */ - ownerAvatar: string; - -} \ No newline at end of file + ownerAvatar: string; +} diff --git a/functions/src/event/event.ts b/functions/src/event/event.ts index 88addda6..325d70c4 100644 --- a/functions/src/event/event.ts +++ b/functions/src/event/event.ts @@ -1,14 +1,14 @@ -import { getDiscussionMessageById } from '../util/db/discussionMessagesDb'; -import { proposalDb } from '../proposals/database'; -import { commonDb } from '../common/database'; -import { getAllUsers } from '../util/db/userDbService'; -import { subscriptionDb } from '../subscriptions/database'; -import { paymentDb } from '../circlepay/payments/database'; -import { discussionDb } from '../discussion/database'; -import { discussionMessageDb } from '../discussionMessage/database'; -import { IDiscussionMessage } from '../discussionMessage/types'; -import { Notifications } from '../constants'; -import { IPaymentEntity } from '../circlepay/payments/types'; +import {getDiscussionMessageById} from '../util/db/discussionMessagesDb'; +import {proposalDb} from '../proposals/database'; +import {commonDb} from '../common/database'; +import {getAllUsers} from '../util/db/userDbService'; +import {subscriptionDb} from '../subscriptions/database'; +import {paymentDb} from '../circlepay/payments/database'; +import {discussionDb} from '../discussion/database'; +import {discussionMessageDb} from '../discussionMessage/database'; +import {IDiscussionMessage} from '../discussionMessage/types'; +import {Notifications} from '../constants'; +import {IPaymentEntity} from '../circlepay/payments/types'; interface IEventData { eventObject: (eventObjId: string) => any; @@ -19,30 +19,44 @@ interface IEventData { * [Notification limiting; users would stop * recieving comment notifications after 5 notifications were already sent * when the user comments, the counter is 'reset' and starting counting 5 notifications again - * + * * @param discussionOwner - owner of the discussion/proposal * @param discussionId - id of the discussion/proposal * @return userFilter - array of users that should be notified about this comment */ -const limitRecipients = async (discussionOwner: string, discussionId: string, discussionMessageOwner: string) : Promise => { - let users = [], lastDoc = null, didBreak = false; - const userFilter = []; +const limitRecipients = async ( + discussionOwner: string, + discussionId: string, + discussionMessageOwner: string, +): Promise => { + let users = [], + lastDoc = null, + didBreak = false; + const userFilter = []; - do { - // eslint-disable-next-line no-await-in-loop - const messages = await discussionMessageDb.getDiscussionMessagsSnapshot(discussionId, Notifications.messageLimit, lastDoc); - // the last doc from which to start counting the next batch of messages - lastDoc = messages[messages.length - 1]; - users = messages.map(message => (message.data() as IDiscussionMessage).ownerId); - // when this is the first comment, users will be empty, discussionOwner should get this notification, if we get here after a few baches, it's fine - messages.length < Notifications.messageLimit && users.push(discussionOwner); - didBreak = handleUserFilter(users, userFilter); - } while (userFilter.length <= Notifications.maxNotifications - && users.length >= Notifications.messageLimit - && !didBreak); + do { + // eslint-disable-next-line no-await-in-loop + const messages = await discussionMessageDb.getDiscussionMessagsSnapshot( + discussionId, + Notifications.messageLimit, + lastDoc, + ); + // the last doc from which to start counting the next batch of messages + lastDoc = messages[messages.length - 1]; + users = messages.map( + (message) => (message.data() as IDiscussionMessage).ownerId, + ); + // when this is the first comment, users will be empty, discussionOwner should get this notification, if we get here after a few baches, it's fine + messages.length < Notifications.messageLimit && users.push(discussionOwner); + didBreak = handleUserFilter(users, userFilter); + } while ( + userFilter.length <= Notifications.maxNotifications && + users.length >= Notifications.messageLimit && + !didBreak + ); - return excludeOwner(userFilter, discussionMessageOwner); -} + return excludeOwner(userFilter, discussionMessageOwner); +}; /** * [handleUserFilter description] @@ -51,29 +65,34 @@ const limitRecipients = async (discussionOwner: string, discussionId: string, di * @return - true: the loop got to break; in this scenario we want to stop the loop in 'limitRecipients' as well * false: when we didn't hit 'break' and we need 'limitRecipients' to keep running */ -const handleUserFilter = (userIDs: string[], userFilter: string[]) : boolean => { - const discussionMessageOwner = userIDs[0]; - for (let i = 1, limitCounter = userFilter.length; i < userIDs.length && limitCounter <= Notifications.maxNotifications; i++) { - if (discussionMessageOwner === userIDs[1] && limitCounter === 0) { - // don't notify any users, this is a consecutive comment of the same user - return true; - } - if (!userFilter.includes(userIDs[i]) - && userIDs[i] !== discussionMessageOwner) { - userFilter.push(userIDs[i]); - } - // increment counter for each messageOwner in users, including duplicates, but excluding consecutive duplicates - if (userIDs[i] !== userIDs[i - 1]) { - limitCounter ++; - } +const handleUserFilter = (userIDs: string[], userFilter: string[]): boolean => { + const discussionMessageOwner = userIDs[0]; + for ( + let i = 1, limitCounter = userFilter.length; + i < userIDs.length && limitCounter <= Notifications.maxNotifications; + i++ + ) { + if (discussionMessageOwner === userIDs[1] && limitCounter === 0) { + // don't notify any users, this is a consecutive comment of the same user + return true; + } + if ( + !userFilter.includes(userIDs[i]) && + userIDs[i] !== discussionMessageOwner + ) { + userFilter.push(userIDs[i]); + } + // increment counter for each messageOwner in users, including duplicates, but excluding consecutive duplicates + if (userIDs[i] !== userIDs[i - 1]) { + limitCounter++; + } } return false; -} +}; // excluding event owner (message creator, etc) from userFilter so she wouldn't get notified -const excludeOwner = (membersId: string[], ownerId: string): string[] => ( - membersId.filter((memberId) => memberId !== ownerId) -); +const excludeOwner = (membersId: string[], ownerId: string): string[] => + membersId.filter((memberId) => memberId !== ownerId); export enum EVENT_TYPES { // Common related events @@ -84,14 +103,12 @@ export enum EVENT_TYPES { COMMON_MEMBER_REMOVED = 'commonMemberRemoved', COMMON_UPDATED = 'commonUpdated', - // Request to join related events REQUEST_TO_JOIN_CREATED = 'requestToJoinCreated', REQUEST_TO_JOIN_ACCEPTED = 'requestToJoinAccepted', REQUEST_TO_JOIN_REJECTED = 'requestToJoinRejected', REQUEST_TO_JOIN_EXECUTED = 'requestToJoinExecuted', - // Funding request related event FUNDING_REQUEST_CREATED = 'fundingRequestCreated', FUNDING_REQUEST_REJECTED = 'fundingRequestRejected', @@ -99,11 +116,9 @@ export enum EVENT_TYPES { FUNDING_REQUEST_ACCEPTED = 'fundingRequestAccepted', FUNDING_REQUEST_ACCEPTED_INSUFFICIENT_FUNDS = 'fundingRequestAcceptedInsufficientFunds', - // Voting related events VOTE_CREATED = 'voteCreated', - // Payment related events PAYMENT_CREATED = 'paymentCreated', PAYMENT_CONFIRMED = 'paymentConfirmed', @@ -136,128 +151,146 @@ export enum EVENT_TYPES { SUBSCRIPTION_CANCELED_BY_PAYMENT_FAILURE = 'subscriptionCanceledByPaymentFailure', // Membership - MEMBERSHIP_REVOKED = 'membershipRevoked' + MEMBERSHIP_REVOKED = 'membershipRevoked', } export const eventData: Record = { [EVENT_TYPES.COMMON_CREATED]: { - eventObject: async (commonId: string): Promise => (await commonDb.get(commonId)), + eventObject: async (commonId: string): Promise => + await commonDb.get(commonId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: (common: any): string[] => { return [common.members[0].userId]; - } + }, }, [EVENT_TYPES.COMMON_CREATION_FAILED]: { - eventObject: async (commonId: string): Promise => (await commonDb.get(commonId)), + eventObject: async (commonId: string): Promise => + await commonDb.get(commonId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: (common: any): string[] => { return [common.members[0].userId]; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_CREATED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { - const proposalDao = (await commonDb.get(proposal.commonId)); - const userFilter = proposalDao.members.map(member => { + const proposalDao = await commonDb.get(proposal.commonId); + const userFilter = proposalDao.members.map((member) => { return member.userId; }); return excludeOwner(userFilter, proposal.proposerId); - } + }, }, [EVENT_TYPES.REQUEST_TO_JOIN_CREATED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { - return [ - proposal.proposerId - ]; - } + return [proposal.proposerId]; + }, }, [EVENT_TYPES.MESSAGE_CREATED]: { - eventObject: async (discussionMessageId: string): Promise => (await getDiscussionMessageById(discussionMessageId)).data(), + eventObject: async (discussionMessageId: string): Promise => + (await getDiscussionMessageById(discussionMessageId)).data(), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (discussionMessage: any): Promise => { - // message can be attached to a discussion or to a proposal (in proposal chat) - const discussionId = discussionMessage.discussionId; - const discussion = (await discussionDb.getDiscussion(discussionId, { throwOnFailure: false })) - || (await proposalDb.getProposal(discussionId)); + // message can be attached to a discussion or to a proposal (in proposal chat) + const discussionId = discussionMessage.discussionId; + const discussion = + (await discussionDb.getDiscussion(discussionId, { + throwOnFailure: false, + })) || (await proposalDb.getProposal(discussionId)); - const discussionOwner = discussion.proposerId || discussion.ownerId; - return await limitRecipients(discussionOwner, discussionId, discussionMessage.ownerId); - } + const discussionOwner = discussion.proposerId || discussion.ownerId; + return await limitRecipients( + discussionOwner, + discussionId, + discussionMessage.ownerId, + ); + }, }, [EVENT_TYPES.COMMON_WHITELISTED]: { - eventObject: async (commonId: string): Promise => (await commonDb.get(commonId)), + eventObject: async (commonId: string): Promise => + await commonDb.get(commonId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (dao: any): Promise => { const allUsers = await getAllUsers(); const usersId = allUsers.map((user) => user.uid); return excludeOwner(usersId, dao.members[0].userId); - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_ACCEPTED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_ACCEPTED_INSUFFICIENT_FUNDS]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_REJECTED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, [EVENT_TYPES.REQUEST_TO_JOIN_REJECTED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, [EVENT_TYPES.PAYMENT_FAILED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, [EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_PAYMENT_FAILURE]: { - eventObject: async (subscriptionId: string): Promise => (await subscriptionDb.get(subscriptionId)), + eventObject: async (subscriptionId: string): Promise => + await subscriptionDb.get(subscriptionId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (subscription: any): Promise => { return [subscription.userId]; - } + }, }, [EVENT_TYPES.SUBSCRIPTION_PAYMENT_CONFIRMED]: { - eventObject: async (paymentId: string): Promise => (await paymentDb.get(paymentId)), + eventObject: async (paymentId: string): Promise => + await paymentDb.get(paymentId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (payment: IPaymentEntity): Promise => { return [payment.userId]; - } + }, }, [EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_USER]: { - eventObject: async (subscriptionId: string): Promise => (await subscriptionDb.get(subscriptionId)), + eventObject: async (subscriptionId: string): Promise => + await subscriptionDb.get(subscriptionId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (subscription: any): Promise => { return [subscription.userId]; - } + }, }, [EVENT_TYPES.REQUEST_TO_JOIN_EXECUTED]: { - eventObject: async (proposalId: string): Promise => (await proposalDb.getProposal(proposalId)), + eventObject: async (proposalId: string): Promise => + await proposalDb.getProposal(proposalId), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types notifyUserFilter: async (proposal: any): Promise => { return [proposal.proposerId]; - } + }, }, - }; diff --git a/functions/src/event/index.ts b/functions/src/event/index.ts index 60041966..c66f0f47 100644 --- a/functions/src/event/index.ts +++ b/functions/src/event/index.ts @@ -1,14 +1,13 @@ import * as functions from 'firebase-functions'; -import { notifyData } from '../notification/notification' -import { createNotification } from '../util/db/notificationDbService'; -import { eventData } from './event' - +import {notifyData} from '../notification/notification'; +import {createNotification} from '../util/db/notificationDbService'; +import {eventData} from './event'; export interface IEventModel { - id: string, - objectId: string, - type: string, - createdAt: string, + id: string; + objectId: string; + type: string; + createdAt: string; } const processEvent = async (event: IEventModel) => { @@ -26,12 +25,11 @@ const processEvent = async (event: IEventModel) => { userFilter, createdAt: new Date(), }); - } -} + } +}; -exports.commonEventListeners = functions - .firestore +exports.commonEventListeners = functions.firestore .document('/event/{id}') .onCreate(async (snap) => { - await processEvent(snap.data() as IEventModel) - }) \ No newline at end of file + await processEvent(snap.data() as IEventModel); + }); diff --git a/functions/src/event/type.ts b/functions/src/event/type.ts index 19dc15f0..ce59ab17 100644 --- a/functions/src/event/type.ts +++ b/functions/src/event/type.ts @@ -1,9 +1,9 @@ -import { IBaseEntity } from '../util/types'; -import { EVENT_TYPES } from './event'; +import {IBaseEntity} from '../util/types'; +import {EVENT_TYPES} from './event'; export interface IEventEntity extends IBaseEntity { userId?: string; objectId?: string; - type: EVENT_TYPES -} \ No newline at end of file + type: EVENT_TYPES; +} diff --git a/functions/src/index.ts b/functions/src/index.ts index d7ad727d..973e749e 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,13 +6,13 @@ import * as notification from './notification'; import * as messageTriggers from './discussionMessage/triggers'; import * as commonTriggers from './common/triggers'; -import { circlePayApp, circlePayCrons } from './circlepay'; -import { commonsApp } from './common'; -import { proposalCrons, proposalTriggers, proposalsApp } from './proposals'; -import { subscriptionsApp } from './subscriptions'; -import { payoutTriggers } from './circlepay/payouts/triggers'; -import { backofficeApp } from './backoffice'; -import { metadataApp } from './metadata'; +import {circlePayApp, circlePayCrons} from './circlepay'; +import {commonsApp} from './common'; +import {proposalCrons, proposalTriggers, proposalsApp} from './proposals'; +import {subscriptionsApp} from './subscriptions'; +import {payoutTriggers} from './circlepay/payouts/triggers'; +import {backofficeApp} from './backoffice'; +import {metadataApp} from './metadata'; // --- Express apps export const commons = commonsApp; diff --git a/functions/src/metadata/index.ts b/functions/src/metadata/index.ts index 87a1b65f..b670a549 100644 --- a/functions/src/metadata/index.ts +++ b/functions/src/metadata/index.ts @@ -1,25 +1,23 @@ import * as functions from 'firebase-functions'; -import { env } from '../constants'; -import { commonApp, commonRouter } from '../util'; +import {env} from '../constants'; +import {commonApp, commonRouter} from '../util'; const metadataRouter = commonRouter(); metadataRouter.get('/app', (req, res) => { - res - .status(200) - .send({ - currentVersion: env.metadata.app.currentVersion || '0.1', // Add really small version if something goes bad so the app does not lock out users - oldestSupportedVersion: env.metadata.app.currentVersion || '0.1' - }); + res.status(200).send({ + currentVersion: env.metadata.app.currentVersion || '0.1', // Add really small version if something goes bad so the app does not lock out users + oldestSupportedVersion: env.metadata.app.currentVersion || '0.1', + }); }); export const metadataApp = functions .runWith({ - timeoutSeconds: 540 + timeoutSeconds: 540, }) - .https.onRequest(commonApp(metadataRouter, { - unauthenticatedRoutes: [ - '/app' - ] - })); + .https.onRequest( + commonApp(metadataRouter, { + unauthenticatedRoutes: ['/app'], + }), + ); diff --git a/functions/src/notification/email/index.ts b/functions/src/notification/email/index.ts index bdfea1fc..9696e1f5 100644 --- a/functions/src/notification/email/index.ts +++ b/functions/src/notification/email/index.ts @@ -1,26 +1,26 @@ -import { sendMail } from './mailer'; -import { env } from '../../constants'; -import { CommonError } from '../../util/errors'; - -import { approvePayout } from './templates/approvePayout'; -import { userCommonCreated } from './templates/userCommonCreated'; -import { userJoinedSuccess } from './templates/userJoinedSuccess'; -import { adminPayInSuccess } from './templates/adminPayInSuccess'; -import { adminCommonCreated } from './templates/adminCommonCreated'; -import { userCommonFeatured } from './templates/userCommonFeatured'; -import { requestToJoinSubmitted } from './templates/requestToJoinSubmitted'; -import { userFundingRequestAcceptedUnknown } from './templates/userFundingRequestAcceptedUnknown'; -import { userFundingRequestAcceptedIsraeli } from './templates/userFundingRequestAcceptedIsraeli'; -import { userFundingRequestAcceptedForeign } from './templates/userFundingRequestAcceptedForeign'; -import { userFundingRequestAcceptedZeroAmount } from './templates/userFundingRequestAcceptedZeroAmount'; -import { userJoinedButFailedPayment } from './templates/userJoinedButFailedPayment'; -import { adminFundingRequestAccepted } from './templates/adminFundingRequestAccepted'; -import { adminPreauthorizationFailed } from './templates/adminPreauthorizationFailed'; -import { adminJoinedButPaymentFailed } from './templates/adminJoinedButFailedPayment'; -import { subscriptionCanceled } from './templates/subscriptionCanceled'; -import { subscriptionChargeFailed } from './templates/subscriptionChargeFailed'; -import { subscriptionCharged } from './templates/subscriptionCharged'; -import { userFundingRequestAcceptedInsufficientFunds } from './templates/userFundingRequestAcceptedInsufficientFunds'; +import {sendMail} from './mailer'; +import {env} from '../../constants'; +import {CommonError} from '../../util/errors'; + +import {approvePayout} from './templates/approvePayout'; +import {userCommonCreated} from './templates/userCommonCreated'; +import {userJoinedSuccess} from './templates/userJoinedSuccess'; +import {adminPayInSuccess} from './templates/adminPayInSuccess'; +import {adminCommonCreated} from './templates/adminCommonCreated'; +import {userCommonFeatured} from './templates/userCommonFeatured'; +import {requestToJoinSubmitted} from './templates/requestToJoinSubmitted'; +import {userFundingRequestAcceptedUnknown} from './templates/userFundingRequestAcceptedUnknown'; +import {userFundingRequestAcceptedIsraeli} from './templates/userFundingRequestAcceptedIsraeli'; +import {userFundingRequestAcceptedForeign} from './templates/userFundingRequestAcceptedForeign'; +import {userFundingRequestAcceptedZeroAmount} from './templates/userFundingRequestAcceptedZeroAmount'; +import {userJoinedButFailedPayment} from './templates/userJoinedButFailedPayment'; +import {adminFundingRequestAccepted} from './templates/adminFundingRequestAccepted'; +import {adminPreauthorizationFailed} from './templates/adminPreauthorizationFailed'; +import {adminJoinedButPaymentFailed} from './templates/adminJoinedButFailedPayment'; +import {subscriptionCanceled} from './templates/subscriptionCanceled'; +import {subscriptionChargeFailed} from './templates/subscriptionChargeFailed'; +import {subscriptionCharged} from './templates/subscriptionCharged'; +import {userFundingRequestAcceptedInsufficientFunds} from './templates/userFundingRequestAcceptedInsufficientFunds'; const templates = { requestToJoinSubmitted, @@ -41,19 +41,18 @@ const templates = { subscriptionCanceled, subscriptionCharged, subscriptionChargeFailed, - userFundingRequestAcceptedInsufficientFunds + userFundingRequestAcceptedInsufficientFunds, }; const globalDefaultStubs = { - supportChatLink: 'https://common.io/help' + supportChatLink: 'https://common.io/help', }; const replaceAll = (string, search, replace) => { return string.split(search).join(replace); }; -const isNullOrUndefined = (val) => - val === null || val === undefined; +const isNullOrUndefined = (val) => val === null || val === undefined; interface IStub { required: boolean; @@ -80,19 +79,23 @@ interface ITemplatedEmail { // @todo Make the payload type based on the templateKey // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const getTemplatedEmail = (templateKey: keyof typeof templates, payload: any): ITemplatedEmail => { - let { template, subject } = templates[templateKey]; +export const getTemplatedEmail = ( + templateKey: keyof typeof templates, + payload: any, +): ITemplatedEmail => { + let {template, subject} = templates[templateKey]; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const { emailStubs, subjectStubs } = templates[templateKey]; - + const {emailStubs, subjectStubs} = templates[templateKey]; // @todo Logger is not definded here because the file is JS. Move it to TS // eslint-disable-next-line no-console console.debug('Email templating started'); if (isNullOrUndefined(template)) { - throw new CommonError(`The requested template (${templateKey}) cannot be found`); + throw new CommonError( + `The requested template (${templateKey}) cannot be found`, + ); } // Validate and add default values for the email template @@ -100,19 +103,26 @@ export const getTemplatedEmail = (templateKey: keyof typeof templates, payload: // Check if the stub is required. If it is check is there is value either in the // global stubs, the default stubs or the user provided ones if ( - emailStubs[stub].required && ( - isNullOrUndefined(payload.emailStubs[stub]) && - isNullOrUndefined(emailStubs[stub].default) && - isNullOrUndefined(globalDefaultStubs[stub]) - ) + emailStubs[stub].required && + isNullOrUndefined(payload.emailStubs[stub]) && + isNullOrUndefined(emailStubs[stub].default) && + isNullOrUndefined(globalDefaultStubs[stub]) ) { - throw new CommonError(`Required stub ${stub} was not provided for ${templateKey} template`); + throw new CommonError( + `Required stub ${stub} was not provided for ${templateKey} template`, + ); } // If there is a default value for the stub and has not been replaced add it here - if (!isNullOrUndefined(emailStubs[stub].default) && isNullOrUndefined(payload.emailStubs[stub])) { + if ( + !isNullOrUndefined(emailStubs[stub].default) && + isNullOrUndefined(payload.emailStubs[stub]) + ) { template = template.replace(`{{${stub}}}`, emailStubs[stub]); - } else if (!isNullOrUndefined(globalDefaultStubs[stub]) && isNullOrUndefined(payload.emailStubs[stub])) { + } else if ( + !isNullOrUndefined(globalDefaultStubs[stub]) && + isNullOrUndefined(payload.emailStubs[stub]) + ) { template = template.replace(`{{${stub}}}`, globalDefaultStubs[stub]); } } @@ -124,8 +134,13 @@ export const getTemplatedEmail = (templateKey: keyof typeof templates, payload: // Validate the email subject for (const stub in subjectStubs) { - if (subjectStubs[stub].required && isNullOrUndefined(payload.subjectStubs[stub])) { - throw new CommonError(`Required stub ${stub} was not provided for subject template`); + if ( + subjectStubs[stub].required && + isNullOrUndefined(payload.subjectStubs[stub]) + ) { + throw new CommonError( + `Required stub ${stub} was not provided for subject template`, + ); } } @@ -138,25 +153,35 @@ export const getTemplatedEmail = (templateKey: keyof typeof templates, payload: return { body: template, - subject + subject, }; }; export interface ISendTemplatedEmailData { - templateKey: keyof typeof templates, - emailStubs?: any, - subjectStubs?: any, - to: string | string[], - from?: string, - bcc?: string, + templateKey: keyof typeof templates; + emailStubs?: any; + subjectStubs?: any; + to: string | string[]; + from?: string; + bcc?: string; } type SendTemplatedEmail = (data: ISendTemplatedEmailData) => Promise; -export const sendTemplatedEmail: SendTemplatedEmail = async ({ templateKey, emailStubs, subjectStubs, to, from, bcc }) => { +export const sendTemplatedEmail: SendTemplatedEmail = async ({ + templateKey, + emailStubs, + subjectStubs, + to, + from, + bcc, +}) => { to === 'admin' && (to = env.mail.adminMail); - const { body, subject } = getTemplatedEmail(templateKey, { emailStubs, subjectStubs }); + const {body, subject} = getTemplatedEmail(templateKey, { + emailStubs, + subjectStubs, + }); if (Array.isArray(to)) { const emailPromises = []; @@ -165,13 +190,7 @@ export const sendTemplatedEmail: SendTemplatedEmail = async ({ templateKey, emai // eslint-disable-next-line no-console console.log(`Sending ${templateKey} to ${emailTo}.`); - emailPromises.push(sendMail( - emailTo, - subject, - body, - from, - bcc - )); + emailPromises.push(sendMail(emailTo, subject, body, from, bcc)); }); await Promise.all(emailPromises); @@ -179,21 +198,14 @@ export const sendTemplatedEmail: SendTemplatedEmail = async ({ templateKey, emai // eslint-disable-next-line no-console console.log(`Sending ${templateKey} to ${to}.`); - await sendMail( - to, - subject, - body, - from, - bcc - ); + await sendMail(to, subject, body, from, bcc); } - // eslint-disable-next-line no-console console.log('Templated email send successfully'); }; export default { getTemplatedEmail, - sendTemplatedEmail + sendTemplatedEmail, }; diff --git a/functions/src/notification/email/mailer.ts b/functions/src/notification/email/mailer.ts index b0818479..eb309232 100644 --- a/functions/src/notification/email/mailer.ts +++ b/functions/src/notification/email/mailer.ts @@ -1,16 +1,22 @@ import sendgrid from '@sendgrid/mail'; -import { env } from '../../constants'; -import { getSecret } from '../../settings'; +import {env} from '../../constants'; +import {getSecret} from '../../settings'; const SENDGRID_APIKEY = 'SENDGRID_APIKEY'; const setApiKey = async () => { const apiKey = await getSecret(SENDGRID_APIKEY); sendgrid.setApiKey(apiKey); -} +}; -export const sendMail = async (dest: string, subject: string, message: string, from = env.mail.sender, bcc = null): Promise => { +export const sendMail = async ( + dest: string, + subject: string, + message: string, + from = env.mail.sender, + bcc = null, +): Promise => { // @question Moore, why are we awaiting this on every single mail we send? await setApiKey(); @@ -20,6 +26,6 @@ export const sendMail = async (dest: string, subject: string, message: string, f bcc, subject, text: message, - html: message + html: message, }); -}; \ No newline at end of file +}; diff --git a/functions/src/notification/email/templates/adminCommonCreated.ts b/functions/src/notification/email/templates/adminCommonCreated.ts index 114e2e18..12be669d 100644 --- a/functions/src/notification/email/templates/adminCommonCreated.ts +++ b/functions/src/notification/email/templates/adminCommonCreated.ts @@ -30,45 +30,45 @@ const template = ` const emailStubs = { commonLink: { - required: true + required: true, }, userName: { - required: true + required: true, }, userId: { - required: true + required: true, }, userEmail: { - required: true + required: true, }, commonCreatedOn: { - required: true + required: true, }, log: { - required: true + required: true, }, commonId: { - required: true + required: true, }, commonName: { - required: true + required: true, }, tagline: { - required: true + required: true, }, about: { - required: true + required: true, }, paymentType: { - required: true + required: true, }, minContribution: { - required: true - } + required: true, + }, }; export const adminCommonCreated = { subject: 'New Common pending approval', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/adminFundingRequestAccepted.ts b/functions/src/notification/email/templates/adminFundingRequestAccepted.ts index 180b7b2c..c26a38d5 100644 --- a/functions/src/notification/email/templates/adminFundingRequestAccepted.ts +++ b/functions/src/notification/email/templates/adminFundingRequestAccepted.ts @@ -22,45 +22,45 @@ Log/info: const emailStubs = { commonName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonBalance: { - required: true + required: true, }, commonId: { - required: true + required: true, }, proposalId: { - required: true + required: true, }, userName: { - required: true + required: true, }, userEmail: { - required: true + required: true, }, userId: { - required: true + required: true, }, fundingAmount: { - required: true + required: true, }, submittedOn: { - required: true + required: true, }, passedOn: { - require: true + require: true, }, log: { - require: true - } + require: true, + }, }; export const adminFundingRequestAccepted = { subject: 'Funding proposal was approved', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/adminJoinedButFailedPayment.ts b/functions/src/notification/email/templates/adminJoinedButFailedPayment.ts index 26ce4eec..c5d6ad37 100644 --- a/functions/src/notification/email/templates/adminJoinedButFailedPayment.ts +++ b/functions/src/notification/email/templates/adminJoinedButFailedPayment.ts @@ -17,33 +17,33 @@ Log/info: const emailStubs = { commonId: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true + required: true, }, proposalId: { - required: true + required: true, }, userName: { - required: true + required: true, }, paymentAmount: { - required: true + required: true, }, submittedOn: { - required: true + required: true, }, log: { - required: true - } + required: true, + }, }; export const adminJoinedButPaymentFailed = { subject: 'Payment could not be processed', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/adminPayInSuccess.ts b/functions/src/notification/email/templates/adminPayInSuccess.ts index 402979cc..2c6c22f3 100644 --- a/functions/src/notification/email/templates/adminPayInSuccess.ts +++ b/functions/src/notification/email/templates/adminPayInSuccess.ts @@ -4,12 +4,12 @@ const template = ` const emailStubs = { proposalId: { - required: true - } + required: true, + }, }; export const adminPayInSuccess = { subject: 'Successful pay-in', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/adminPreauthorizationFailed.ts b/functions/src/notification/email/templates/adminPreauthorizationFailed.ts index 35ff8a2d..a86e2297 100644 --- a/functions/src/notification/email/templates/adminPreauthorizationFailed.ts +++ b/functions/src/notification/email/templates/adminPreauthorizationFailed.ts @@ -15,33 +15,33 @@ const template = ` const emailStubs = { commonName: { - required: true + required: true, }, membershipRequestId: { - required: true + required: true, }, userId: { - required: true + required: true, }, userEmail: { - required: true + required: true, }, userFullName: { - required: true + required: true, }, paymentAmount: { - required: true + required: true, }, submittedOn: { - required: true + required: true, }, failureReason: { - required: true - } + required: true, + }, }; export const adminPreauthorizationFailed = { subject: 'Payment pre-authorization failed', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/approvePayout.ts b/functions/src/notification/email/templates/approvePayout.ts index 47db09fd..3b1cd9db 100644 --- a/functions/src/notification/email/templates/approvePayout.ts +++ b/functions/src/notification/email/templates/approvePayout.ts @@ -27,33 +27,33 @@ const template = ` const emailStubs = { payoutId: { - required: true + required: true, }, amount: { - required: true + required: true, }, url: { - required: true + required: true, }, beneficiary: { - required: true + required: true, }, proposal: { - required: true + required: true, }, common: { - required: true + required: true, }, bankDescription: { - required: true + required: true, }, bank: { - required: true - } + required: true, + }, }; export const approvePayout = { subject: 'Approve payout', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/requestToJoinSubmitted.ts b/functions/src/notification/email/templates/requestToJoinSubmitted.ts index 65703039..8df1de31 100644 --- a/functions/src/notification/email/templates/requestToJoinSubmitted.ts +++ b/functions/src/notification/email/templates/requestToJoinSubmitted.ts @@ -13,21 +13,21 @@ const template = ` const emailStubs = { userName: { - required: true + required: true, }, link: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const requestToJoinSubmitted = { subject: 'Request to join submitted', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/subscriptionCanceled.ts b/functions/src/notification/email/templates/subscriptionCanceled.ts index 7f424c68..2b2294fc 100644 --- a/functions/src/notification/email/templates/subscriptionCanceled.ts +++ b/functions/src/notification/email/templates/subscriptionCanceled.ts @@ -1,4 +1,4 @@ -import { IEmailTemplate } from '../index'; +import {IEmailTemplate} from '../index'; const template = `
@@ -23,16 +23,16 @@ export const subscriptionCanceled: IEmailTemplate = { template: template, emailStubs: { firstName: { - required: true + required: true, }, dueDate: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true - } - } -}; \ No newline at end of file + required: true, + }, + }, +}; diff --git a/functions/src/notification/email/templates/subscriptionChargeFailed.ts b/functions/src/notification/email/templates/subscriptionChargeFailed.ts index 52f75359..809f186f 100644 --- a/functions/src/notification/email/templates/subscriptionChargeFailed.ts +++ b/functions/src/notification/email/templates/subscriptionChargeFailed.ts @@ -1,4 +1,4 @@ -import { IEmailTemplate } from ".."; +import {IEmailTemplate} from '..'; const template = `
@@ -12,20 +12,20 @@ const template = ` Common,
Collaborative Social Action.
-` +`; export const subscriptionChargeFailed: IEmailTemplate = { subject: 'Payment could not be processed', template: template, emailStubs: { firstName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true - } - } -} \ No newline at end of file + required: true, + }, + }, +}; diff --git a/functions/src/notification/email/templates/subscriptionCharged.ts b/functions/src/notification/email/templates/subscriptionCharged.ts index dc695733..f6d5d25d 100644 --- a/functions/src/notification/email/templates/subscriptionCharged.ts +++ b/functions/src/notification/email/templates/subscriptionCharged.ts @@ -1,4 +1,4 @@ -import { IEmailTemplate } from '../index'; +import {IEmailTemplate} from '../index'; import moment from 'moment'; const template = ` @@ -17,36 +17,35 @@ const template = `
`; - export const subscriptionCharged: IEmailTemplate = { subject: 'Payment confirmation - your monthly contribution to {{commonName}}', template: template, subjectStubs: { commonName: { - required: true - } + required: true, + }, }, emailStubs: { firstName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true + required: true, }, lastDigits: { - required: true + required: true, }, chargeDate: { required: false, - default: moment(new Date()).format('MMMM D, YYYY') + default: moment(new Date()).format('MMMM D, YYYY'), }, chargeAmount: { - required: true - } - } -}; \ No newline at end of file + required: true, + }, + }, +}; diff --git a/functions/src/notification/email/templates/userCommonCreated.ts b/functions/src/notification/email/templates/userCommonCreated.ts index e7392378..83006d93 100644 --- a/functions/src/notification/email/templates/userCommonCreated.ts +++ b/functions/src/notification/email/templates/userCommonCreated.ts @@ -19,21 +19,21 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, commonName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userCommonCreated = { subject: 'Common successfully created', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userCommonFeatured.ts b/functions/src/notification/email/templates/userCommonFeatured.ts index 78086932..8f30dcf3 100644 --- a/functions/src/notification/email/templates/userCommonFeatured.ts +++ b/functions/src/notification/email/templates/userCommonFeatured.ts @@ -13,21 +13,21 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, commonName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userCommonFeatured = { subject: 'Your Common is now featured ', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userFundingRequestAcceptedForeign.ts b/functions/src/notification/email/templates/userFundingRequestAcceptedForeign.ts index e7c5871c..51e277bf 100644 --- a/functions/src/notification/email/templates/userFundingRequestAcceptedForeign.ts +++ b/functions/src/notification/email/templates/userFundingRequestAcceptedForeign.ts @@ -22,24 +22,24 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, proposal: { - required: true + required: true, }, fundingAmount: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userFundingRequestAcceptedForeign = { subject: 'Your funding proposal was approved', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userFundingRequestAcceptedInsufficientFunds.ts b/functions/src/notification/email/templates/userFundingRequestAcceptedInsufficientFunds.ts index 5f45b76b..4a092483 100644 --- a/functions/src/notification/email/templates/userFundingRequestAcceptedInsufficientFunds.ts +++ b/functions/src/notification/email/templates/userFundingRequestAcceptedInsufficientFunds.ts @@ -1,4 +1,4 @@ -import { IEmailTemplate } from '..'; +import {IEmailTemplate} from '..'; const template = `
@@ -33,19 +33,19 @@ export const userFundingRequestAcceptedInsufficientFunds: IEmailTemplate = { template: template, emailStubs: { firstName: { - required: true + required: true, }, commonName: { - required: true + required: true, }, proposalName: { - required: true + required: true, }, amountRequested: { - required: true + required: true, }, commonBalance: { - required: true - } - } -}; \ No newline at end of file + required: true, + }, + }, +}; diff --git a/functions/src/notification/email/templates/userFundingRequestAcceptedIsraeli.ts b/functions/src/notification/email/templates/userFundingRequestAcceptedIsraeli.ts index 8a641bae..df1f3791 100644 --- a/functions/src/notification/email/templates/userFundingRequestAcceptedIsraeli.ts +++ b/functions/src/notification/email/templates/userFundingRequestAcceptedIsraeli.ts @@ -27,24 +27,24 @@ const template = `
const emailStubs = { userName: { - required: true + required: true, }, proposal: { - required: true + required: true, }, fundingAmount: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userFundingRequestAcceptedIsraeli = { subject: 'ההצעה שלך ב- Common אושרה!', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userFundingRequestAcceptedUnknown.ts b/functions/src/notification/email/templates/userFundingRequestAcceptedUnknown.ts index e130a4c0..2a100d64 100644 --- a/functions/src/notification/email/templates/userFundingRequestAcceptedUnknown.ts +++ b/functions/src/notification/email/templates/userFundingRequestAcceptedUnknown.ts @@ -20,24 +20,24 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, proposal: { - required: true + required: true, }, fundingAmount: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userFundingRequestAcceptedUnknown = { subject: 'Proposal approved - Missing information', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userFundingRequestAcceptedZeroAmount.ts b/functions/src/notification/email/templates/userFundingRequestAcceptedZeroAmount.ts index b75e1a1b..993e955b 100644 --- a/functions/src/notification/email/templates/userFundingRequestAcceptedZeroAmount.ts +++ b/functions/src/notification/email/templates/userFundingRequestAcceptedZeroAmount.ts @@ -14,21 +14,21 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, proposal: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userFundingRequestAcceptedZeroAmount = { subject: 'Your proposal was approved!', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userJoinedButFailedPayment.ts b/functions/src/notification/email/templates/userJoinedButFailedPayment.ts index 3920e49e..69854bab 100644 --- a/functions/src/notification/email/templates/userJoinedButFailedPayment.ts +++ b/functions/src/notification/email/templates/userJoinedButFailedPayment.ts @@ -15,21 +15,21 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userJoinedButFailedPayment = { subject: 'Payment could not be processed', emailStubs, - template + template, }; diff --git a/functions/src/notification/email/templates/userJoinedSuccess.ts b/functions/src/notification/email/templates/userJoinedSuccess.ts index 089b61aa..95f08e54 100644 --- a/functions/src/notification/email/templates/userJoinedSuccess.ts +++ b/functions/src/notification/email/templates/userJoinedSuccess.ts @@ -12,21 +12,21 @@ Collaborative Social Action. const emailStubs = { userName: { - required: true + required: true, }, commonLink: { - required: true + required: true, }, commonName: { - required: true + required: true, }, supportChatLink: { - required: true - } + required: true, + }, }; export const userJoinedSuccess = { subject: 'You are in!', emailStubs, - template + template, }; diff --git a/functions/src/notification/helpers/index.ts b/functions/src/notification/helpers/index.ts index ac0f2a35..d25838c4 100644 --- a/functions/src/notification/helpers/index.ts +++ b/functions/src/notification/helpers/index.ts @@ -1,10 +1,13 @@ -export const getFundingRequestAcceptedTemplate = (country: string, amount: number): string => { +export const getFundingRequestAcceptedTemplate = ( + country: string, + amount: number, +): string => { if (amount) { return !country ? 'userFundingRequestAcceptedUnknown' - : (country === 'IL' - ? 'userFundingRequestAcceptedIsraeli' - : 'userFundingRequestAcceptedForeign'); + : country === 'IL' + ? 'userFundingRequestAcceptedIsraeli' + : 'userFundingRequestAcceptedForeign'; } return 'userFundingRequestAcceptedZeroAmount'; -}; \ No newline at end of file +}; diff --git a/functions/src/notification/index.ts b/functions/src/notification/index.ts index 3e079365..6b018c2f 100644 --- a/functions/src/notification/index.ts +++ b/functions/src/notification/index.ts @@ -1,15 +1,15 @@ import * as functions from 'firebase-functions'; -import { getUserById } from '../util/db/userDbService'; +import {getUserById} from '../util/db/userDbService'; import Notification from './notification'; import emailClient from './email'; -import { notifyData } from './notification'; +import {notifyData} from './notification'; export interface INotificationModel { - userFilter: string[], - createdAt: string, - eventObjectId: string, - eventType: string, - eventId: string, + userFilter: string[]; + createdAt: string; + eventObjectId: string; + eventType: string; + eventId: string; } /** @@ -19,44 +19,48 @@ export interface INotificationModel { * @param notification - notification object with data of what and to whom to send it */ const processNotification = async (notification: INotificationModel) => { + const currNotifyObj = notifyData[notification.eventType]; - const currNotifyObj = notifyData[notification.eventType]; + if (!currNotifyObj.data) { + throw Error( + `Not found data method for notification on event type "${notification.eventType}".`, + ); + } - if (!currNotifyObj.data) { - throw Error(`Not found data method for notification on event type "${notification.eventType}".`); - } + const eventNotifyData = await currNotifyObj.data(notification.eventObjectId); - const eventNotifyData = await currNotifyObj.data(notification.eventObjectId); - - if (notification.userFilter) { - notification.userFilter.forEach( async filterUserId => { - const userData: any = (await getUserById(filterUserId)).data(); + if (notification.userFilter) { + notification.userFilter.forEach(async (filterUserId) => { + const userData: any = (await getUserById(filterUserId)).data(); - if (currNotifyObj.notification) { - try { - const {title, body, image, path} = await currNotifyObj.notification(eventNotifyData); - await Notification.send(userData.tokens, title, body, image, path); - } catch(err) { - logger.error('Notification send err -> ', err) - throw err - } - } - }); - } - // handling email sending separately from notifications because we don't want to send as many emails as userFilter.length - if (currNotifyObj.email) { - const emailTemplate = currNotifyObj.email(eventNotifyData); - const emailTemplateArr = Array.isArray(emailTemplate) ? emailTemplate : [emailTemplate]; - emailTemplateArr.forEach( async (currEmailTemplate) => { - const template = currEmailTemplate; - await emailClient.sendTemplatedEmail(template); - }); - } -} + if (currNotifyObj.notification) { + try { + const {title, body, image, path} = await currNotifyObj.notification( + eventNotifyData, + ); + await Notification.send(userData.tokens, title, body, image, path); + } catch (err) { + logger.error('Notification send err -> ', err); + throw err; + } + } + }); + } + // handling email sending separately from notifications because we don't want to send as many emails as userFilter.length + if (currNotifyObj.email) { + const emailTemplate = currNotifyObj.email(eventNotifyData); + const emailTemplateArr = Array.isArray(emailTemplate) + ? emailTemplate + : [emailTemplate]; + emailTemplateArr.forEach(async (currEmailTemplate) => { + const template = currEmailTemplate; + await emailClient.sendTemplatedEmail(template); + }); + } +}; -exports.commonNotificationListener = functions - .firestore +exports.commonNotificationListener = functions.firestore .document('/notification/{id}') .onCreate(async (snap) => { return processNotification(snap.data() as INotificationModel); - }) + }); diff --git a/functions/src/notification/notification.ts b/functions/src/notification/notification.ts index 0d4e76e8..287b1579 100644 --- a/functions/src/notification/notification.ts +++ b/functions/src/notification/notification.ts @@ -1,21 +1,21 @@ import admin from 'firebase-admin'; -import { EVENT_TYPES } from '../event/event'; -import { env } from '../constants'; -import { getUserById } from '../util/db/userDbService'; -import { Utils } from '../util/util'; -import { getDiscussionMessageById } from '../util/db/discussionMessagesDb'; -import { proposalDb } from '../proposals/database'; -import { commonDb } from '../common/database'; -import { subscriptionDb } from '../subscriptions/database'; -import { ISendTemplatedEmailData } from './email'; -import { ISubscriptionEntity } from '../subscriptions/types'; -import { IUserEntity } from '../users/types'; -import { userDb } from '../users/database'; -import { paymentDb } from '../circlepay/payments/database'; -import { cardDb } from '../circlepay/cards/database'; -import { ICardEntity } from '../circlepay/cards/types'; +import {EVENT_TYPES} from '../event/event'; +import {env} from '../constants'; +import {getUserById} from '../util/db/userDbService'; +import {Utils} from '../util/util'; +import {getDiscussionMessageById} from '../util/db/discussionMessagesDb'; +import {proposalDb} from '../proposals/database'; +import {commonDb} from '../common/database'; +import {subscriptionDb} from '../subscriptions/database'; +import {ISendTemplatedEmailData} from './email'; +import {ISubscriptionEntity} from '../subscriptions/types'; +import {IUserEntity} from '../users/types'; +import {userDb} from '../users/database'; +import {paymentDb} from '../circlepay/payments/database'; +import {cardDb} from '../circlepay/cards/database'; +import {ICardEntity} from '../circlepay/cards/types'; import moment from 'moment'; -import { getFundingRequestAcceptedTemplate } from './helpers'; +import {getFundingRequestAcceptedTemplate} from './helpers'; const messaging = admin.messaging(); @@ -27,17 +27,16 @@ const getNameString = (userData) => { }; export interface INotification { - send: any + send: any; } const memberAddedNotification = (commonData) => ({ title: 'Congrats!', body: `Your request to join "${commonData.name}" was accepted, you are now a member!`, image: commonData.image || '', - path: `CommonProfile/${commonData.id}` + path: `CommonProfile/${commonData.id}`, }); - interface IEventData { data: (eventObj: string) => any; email?: (notifyData: any) => any; @@ -48,14 +47,14 @@ export const notifyData: Record = { [EVENT_TYPES.COMMON_CREATED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (objectId: string) => { - const commonData = (await commonDb.get(objectId)); + const commonData = await commonDb.get(objectId); return { commonData, - userData: (await getUserById(commonData.members[0].userId)).data() + userData: (await getUserById(commonData.members[0].userId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ commonData, userData }): ISendTemplatedEmailData[] => { + email: ({commonData, userData}): ISendTemplatedEmailData[] => { return [ { to: userData.email, @@ -63,8 +62,8 @@ export const notifyData: Record = { emailStubs: { userName: getNameString(userData), commonName: commonData.name, - commonLink: Utils.getCommonLink(commonData.id) - } + commonLink: Utils.getCommonLink(commonData.id), + }, }, { to: env.mail.adminMail, @@ -81,137 +80,156 @@ export const notifyData: Record = { tagline: commonData.metadata.byline, about: commonData.metadata.description, paymentType: 'one-time', - minContribution: (commonData.metadata.minFeeToJoin / 100) - .toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }) - } - } + minContribution: ( + commonData.metadata.minFeeToJoin / 100 + ).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }), + }, + }, ]; - } + }, }, [EVENT_TYPES.REQUEST_TO_JOIN_CREATED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (proposalId: string) => { - const proposalData = (await proposalDb.getProposal(proposalId)); + const proposalData = await proposalDb.getProposal(proposalId); return { - commonData: (await commonDb.get(proposalData.commonId)), - userData: (await getUserById(proposalData.proposerId)).data() + commonData: await commonDb.get(proposalData.commonId), + userData: (await getUserById(proposalData.proposerId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ commonData, userData }): ISendTemplatedEmailData => { + email: ({commonData, userData}): ISendTemplatedEmailData => { return { to: userData.email, templateKey: 'requestToJoinSubmitted', emailStubs: { userName: getNameString(userData), link: Utils.getCommonLink(commonData.id), - commonName: commonData.name - } + commonName: commonData.name, + }, }; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_CREATED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (objectId: string) => { - const proposalData = (await proposalDb.getProposal(objectId)); + const proposalData = await proposalDb.getProposal(objectId); return { proposalData, - commonData: (await commonDb.get(proposalData.commonId)), - userData: (await getUserById(proposalData.proposerId)).data() + commonData: await commonDb.get(proposalData.commonId), + userData: (await getUserById(proposalData.proposerId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ proposalData, commonData, userData }) => { + notification: async ({proposalData, commonData, userData}) => { return { title: 'A new funding proposal in your Common!', - body: `Your fellow member ${getNameString(userData)} is asking for $${proposalData.fundingRequest.amount / 100} for their proposal in "${commonData.name}". See the proposal and vote.`, + body: `Your fellow member ${getNameString(userData)} is asking for $${ + proposalData.fundingRequest.amount / 100 + } for their proposal in "${ + commonData.name + }". See the proposal and vote.`, image: commonData.image || '', - path: `ProposalScreen/${commonData.id}/${proposalData.id}` + path: `ProposalScreen/${commonData.id}/${proposalData.id}`, }; - } - + }, }, [EVENT_TYPES.COMMON_WHITELISTED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (commonId: string) => { - const commonData = (await commonDb.get(commonId)); + const commonData = await commonDb.get(commonId); return { commonData, - userData: (await getUserById(commonData.metadata.founderId)).data() + userData: (await getUserById(commonData.metadata.founderId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ commonData }) => { + notification: async ({commonData}) => { return { title: 'A new Common was just featured!', body: `A new Common was just featured: "${commonData.name}". You might want to check it out.`, image: commonData.image || '', - path: `CommonProfile/${commonData.id}` + path: `CommonProfile/${commonData.id}`, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ commonData, userData }): ISendTemplatedEmailData => { + email: ({commonData, userData}): ISendTemplatedEmailData => { return { to: userData.email, templateKey: 'userCommonFeatured', emailStubs: { commonName: commonData.name, commonLink: Utils.getCommonLink(commonData.id), - userName: getNameString(userData) - } + userName: getNameString(userData), + }, }; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_ACCEPTED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (objectId: string) => { - const proposalData = (await proposalDb.getProposal(objectId)); + const proposalData = await proposalDb.getProposal(objectId); const cards = await cardDb.getMany({ ownerId: proposalData.proposerId, sort: { orderByDesc: 'updatedAt', - limit: 1 - } + limit: 1, + }, }); return { proposalData, - commonData: (await commonDb.get(proposalData.commonId)), + commonData: await commonDb.get(proposalData.commonId), userData: (await getUserById(proposalData.proposerId)).data(), - cardMetadata: cards[0]?.metadata + cardMetadata: cards[0]?.metadata, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ proposalData, commonData }) => { + notification: async ({proposalData, commonData}) => { return { title: 'Your funding proposal was approved!', - body: `A funding proposal for $${proposalData.fundingRequest.amount / 100} was approved by "${commonData.name}".`, + body: `A funding proposal for $${ + proposalData.fundingRequest.amount / 100 + } was approved by "${commonData.name}".`, image: commonData.image || '', - path: `ProposalScreen/${commonData.id}/${proposalData.id}` + path: `ProposalScreen/${commonData.id}/${proposalData.id}`, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ userData, proposalData, commonData, cardMetadata }): ISendTemplatedEmailData[] => { - const userTemplate = getFundingRequestAcceptedTemplate(cardMetadata?.billingDetails?.country, proposalData.fundingRequest.amount); + email: ({ + userData, + proposalData, + commonData, + cardMetadata, + }): ISendTemplatedEmailData[] => { + const userTemplate = getFundingRequestAcceptedTemplate( + cardMetadata?.billingDetails?.country, + proposalData.fundingRequest.amount, + ); return [ { to: userData.email, - from: proposalData.fundingRequest.amount === 0 ? env.mail.sender : env.mail.payoutEmail, + from: + proposalData.fundingRequest.amount === 0 + ? env.mail.sender + : env.mail.payoutEmail, bcc: env.mail.payoutEmail, - templateKey: (userTemplate as any), + templateKey: userTemplate as any, emailStubs: { userName: getNameString(userData), proposal: proposalData.description.title, - fundingAmount: (proposalData.fundingRequest.amount / 100).toLocaleString('en-US', { + fundingAmount: ( + proposalData.fundingRequest.amount / 100 + ).toLocaleString('en-US', { style: 'currency', - currency: 'USD' + currency: 'USD', }), - commonName: commonData.name - } + commonName: commonData.name, + }, }, { to: env.mail.adminMail, @@ -219,23 +237,28 @@ export const notifyData: Record = { emailStubs: { commonName: commonData.name, commonLink: Utils.getCommonLink(commonData.id), - commonBalance: (commonData.balance / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }), + commonBalance: (commonData.balance / 100).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }), commonId: commonData.id, proposalId: proposalData.id, userName: getNameString(userData), userEmail: userData.email, userId: userData.uid, - fundingAmount: (proposalData.fundingRequest.amount / 100).toLocaleString('en-US', { + fundingAmount: ( + proposalData.fundingRequest.amount / 100 + ).toLocaleString('en-US', { style: 'currency', - currency: 'USD' + currency: 'USD', }), submittedOn: proposalData.createdAt.toDate(), passedOn: new Date(), - log: 'Funding request accepted' - } - } + log: 'Funding request accepted', + }, + }, ]; - } + }, }, [EVENT_TYPES.FUNDING_REQUEST_ACCEPTED_INSUFFICIENT_FUNDS]: { data: async (objectId: string): Promise => { @@ -246,73 +269,80 @@ export const notifyData: Record = { return { user, common, - proposal + proposal, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ user, proposal, common }): ISendTemplatedEmailData => ({ + email: ({user, proposal, common}): ISendTemplatedEmailData => ({ to: user.email, templateKey: 'userFundingRequestAcceptedInsufficientFunds', emailStubs: { firstName: user.firstName, commonName: common.name, proposalName: proposal.description.title, - amountRequested: (proposal.fundingRequest.amount / 100) - .toLocaleString('en-US', { style: 'currency', currency: 'USD' }), - commonBalance: (common.balance / 100) - .toLocaleString('en-US', { style: 'currency', currency: 'USD' }) - } - }) + amountRequested: ( + proposal.fundingRequest.amount / 100 + ).toLocaleString('en-US', {style: 'currency', currency: 'USD'}), + commonBalance: (common.balance / 100).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }), + }, + }), }, [EVENT_TYPES.REQUEST_TO_JOIN_EXECUTED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (proposalId: string) => { - const proposalData = (await proposalDb.getProposal(proposalId)); + const proposalData = await proposalDb.getProposal(proposalId); return { - commonData: (await commonDb.get(proposalData.commonId)), - userData: (await getUserById(proposalData.proposerId)).data() + commonData: await commonDb.get(proposalData.commonId), + userData: (await getUserById(proposalData.proposerId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ commonData }) => memberAddedNotification(commonData), + notification: async ({commonData}) => memberAddedNotification(commonData), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ commonData, userData }): ISendTemplatedEmailData => { + email: ({commonData, userData}): ISendTemplatedEmailData => { return { to: userData.email, templateKey: 'userJoinedSuccess', emailStubs: { userName: getNameString(userData), commonLink: Utils.getCommonLink(commonData.id), - commonName: commonData.name - } + commonName: commonData.name, + }, }; - } + }, }, [EVENT_TYPES.REQUEST_TO_JOIN_REJECTED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (objectId: string) => { - const proposalData = (await proposalDb.getProposal(objectId)); + const proposalData = await proposalDb.getProposal(objectId); return { - commonData: (await commonDb.get(proposalData.commonId)) + commonData: await commonDb.get(proposalData.commonId), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ commonData }) => { + notification: async ({commonData}) => { return { title: `Bad news, your request to join "${commonData.name}" was rejected.`, body: `Don't give up, there are plenty of other Commons you can join.`, image: commonData.image || '', - path: `CommonProfile/${commonData.id}` + path: `CommonProfile/${commonData.id}`, }; - } + }, }, [EVENT_TYPES.MESSAGE_CREATED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (messageId: string) => { - const discussionMessage = (await getDiscussionMessageById(messageId)).data(); - const commonId = discussionMessage.commonId - || (await proposalDb.getProposal(discussionMessage.discussionId))?.commonId; + const discussionMessage = ( + await getDiscussionMessageById(messageId) + ).data(); + const commonId = + discussionMessage.commonId || + (await proposalDb.getProposal(discussionMessage.discussionId)) + ?.commonId; const path = discussionMessage.commonId ? `Discussions/${commonId}/${discussionMessage.discussionId}` @@ -320,41 +350,41 @@ export const notifyData: Record = { return { sender: (await getUserById(discussionMessage.ownerId)).data(), - commonData: (await commonDb.get(commonId)), - path + commonData: await commonDb.get(commonId), + path, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ sender, commonData, path }) => ( - { - title: `New comment!`, - body: `The member ${getNameString(sender)} commented in "${commonData.name}"`, - image: commonData.image || '', - path - } - ) + notification: async ({sender, commonData, path}) => ({ + title: `New comment!`, + body: `The member ${getNameString(sender)} commented in "${ + commonData.name + }"`, + image: commonData.image || '', + path, + }), }, [EVENT_TYPES.PAYMENT_FAILED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types data: async (proposalId: string) => { - const proposalData = (await proposalDb.getProposal(proposalId)); + const proposalData = await proposalDb.getProposal(proposalId); return { - commonData: (await commonDb.get(proposalData.commonId)), - userData: (await getUserById(proposalData.proposerId)).data() + commonData: await commonDb.get(proposalData.commonId), + userData: (await getUserById(proposalData.proposerId)).data(), }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - email: ({ commonData, userData }): ISendTemplatedEmailData => { + email: ({commonData, userData}): ISendTemplatedEmailData => { return { to: userData.email, templateKey: 'userJoinedButFailedPayment', emailStubs: { userName: getNameString(userData), commonLink: Utils.getCommonLink(commonData.id), - commonName: commonData.name - } + commonName: commonData.name, + }, }; - } + }, }, [EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_PAYMENT_FAILURE]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -362,25 +392,27 @@ export const notifyData: Record = { const subscription = await subscriptionDb.get(subscriptionId); const user = await userDb.get(subscription.userId); - return { subscription, - user + user, }; }, - email: ({ subscription, user }: { - subscription: ISubscriptionEntity, - user: IUserEntity + email: ({ + subscription, + user, + }: { + subscription: ISubscriptionEntity; + user: IUserEntity; }): ISendTemplatedEmailData => ({ to: user.email, templateKey: 'subscriptionChargeFailed', emailStubs: { firstName: user.firstName, commonName: subscription.metadata.common.name, - commonLink: Utils.getCommonLink(subscription.metadata.common.id) - } - }) + commonLink: Utils.getCommonLink(subscription.metadata.common.id), + }, + }), }, [EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_USER]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -388,16 +420,18 @@ export const notifyData: Record = { const subscription = await subscriptionDb.get(subscriptionId); const user = await userDb.get(subscription.userId); - return { subscription, - user + user, }; }, - email: ({ subscription, user }: { - subscription: ISubscriptionEntity, - user: IUserEntity + email: ({ + subscription, + user, + }: { + subscription: ISubscriptionEntity; + user: IUserEntity; }): ISendTemplatedEmailData => ({ to: user.email, templateKey: 'subscriptionCanceled', @@ -405,9 +439,9 @@ export const notifyData: Record = { firstName: user.firstName, dueDate: moment(subscription.dueDate.toDate()).format('MMMM D, YYYY'), commonName: subscription.metadata.common.name, - commonLink: Utils.getCommonLink(subscription.metadata.common.id) - } - }) + commonLink: Utils.getCommonLink(subscription.metadata.common.id), + }, + }), }, [EVENT_TYPES.SUBSCRIPTION_PAYMENT_CONFIRMED]: { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -422,24 +456,25 @@ export const notifyData: Record = { subscription, user, card, - commonData + commonData, }; }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - notification: async ({ commonData, subscription }) => ( - subscription.charges === 1 - ? memberAddedNotification(commonData) - : null - ), - email: ({ subscription, user, card }: { - subscription: ISubscriptionEntity, - user: IUserEntity, - card: ICardEntity + notification: async ({commonData, subscription}) => + subscription.charges === 1 ? memberAddedNotification(commonData) : null, + email: ({ + subscription, + user, + card, + }: { + subscription: ISubscriptionEntity; + user: IUserEntity; + card: ICardEntity; }): ISendTemplatedEmailData => ({ to: user.email, templateKey: 'subscriptionCharged', subjectStubs: { - commonName: subscription.metadata.common.name + commonName: subscription.metadata.common.name, }, emailStubs: { firstName: user.firstName, @@ -447,34 +482,46 @@ export const notifyData: Record = { commonLink: Utils.getCommonLink(subscription.metadata.common.id), chargeDate: moment(new Date()).format('MMMM D, YYYY'), lastDigits: card.metadata.digits, - chargeAmount: (subscription.amount / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }) - } - }) - } + chargeAmount: (subscription.amount / 100).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }), + }, + }), + }, }; -export default new class Notification implements INotification { - async send(tokens = [], title, body, image = '', path, options = { - contentAvailable: true, - mutable_content: true, - priority: 'high' - }) { +export default new (class Notification implements INotification { + async send( + tokens = [], + title, + body, + image = '', + path, + options = { + contentAvailable: true, + mutable_content: true, + priority: 'high', + }, + ) { const payload = { data: { - path + path, }, notification: { title, body, - image - } + image, + }, }; // @question Ask about this rule "promise/always-return". It is kinda useless so we may disable it globally? // eslint-disable-next-line promise/always-return // sendToDevice cannot have an empty tokens array - const messageSent: admin.messaging.MessagingDevicesResponse = tokens.length > 0 && await messaging.sendToDevice(tokens, payload, options); + const messageSent: admin.messaging.MessagingDevicesResponse = + tokens.length > 0 && + (await messaging.sendToDevice(tokens, payload, options)); logger.debug('Send Success', messageSent); } -}; +})(); diff --git a/functions/src/proposals/business/countVotes.ts b/functions/src/proposals/business/countVotes.ts index 8c7926ef..e5e615e5 100644 --- a/functions/src/proposals/business/countVotes.ts +++ b/functions/src/proposals/business/countVotes.ts @@ -1,7 +1,7 @@ -import { ArgumentError } from '../../util/errors'; +import {ArgumentError} from '../../util/errors'; -import { IProposalEntity } from '../proposalTypes'; -import { VoteOutcome } from '../voteTypes'; +import {IProposalEntity} from '../proposalTypes'; +import {VoteOutcome} from '../voteTypes'; export interface ICalculatedVotes { votesFor: number; @@ -20,27 +20,28 @@ export interface ICalculatedVotes { * @returns The counted votes and the voting outcome */ export const countVotes = (proposal: IProposalEntity): ICalculatedVotes => { - if(!proposal) { + if (!proposal) { throw new ArgumentError('proposal', proposal); } const votes: ICalculatedVotes = { votesAgainst: 0, votesFor: 0, - outcome: null + outcome: null, }; proposal.votes.forEach((vote) => { - if(vote.voteOutcome === 'approved') { + if (vote.voteOutcome === 'approved') { votes.votesFor += 1; } else { votes.votesAgainst += 1; } }); - votes.outcome = votes.votesFor > votes.votesAgainst && votes.votesFor > 0 - ? 'approved' - : 'rejected'; + votes.outcome = + votes.votesFor > votes.votesAgainst && votes.votesFor > 0 + ? 'approved' + : 'rejected'; return votes; -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/business/createFundingRequest.ts b/functions/src/proposals/business/createFundingRequest.ts index 3ce92909..deea3bee 100644 --- a/functions/src/proposals/business/createFundingRequest.ts +++ b/functions/src/proposals/business/createFundingRequest.ts @@ -1,68 +1,74 @@ import * as yup from 'yup'; -import { fileValidationSchema, imageValidationSchema, linkValidationSchema } from '../../util/schemas'; -import { CommonError } from '../../util/errors'; -import { validate } from '../../util/validate'; -import { Nullable } from '../../util/types'; -import { env, StatusCodes } from '../../constants'; -import { isCommonMember } from '../../common/business'; -import { commonDb } from '../../common/database'; - -import { proposalDb } from '../database'; -import { IFundingRequestProposal, IProposalFile, IProposalImage, IProposalLink } from '../proposalTypes'; -import { createEvent } from '../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../event/event'; +import { + fileValidationSchema, + imageValidationSchema, + linkValidationSchema, +} from '../../util/schemas'; +import {CommonError} from '../../util/errors'; +import {validate} from '../../util/validate'; +import {Nullable} from '../../util/types'; +import {env, StatusCodes} from '../../constants'; +import {isCommonMember} from '../../common/business'; +import {commonDb} from '../../common/database'; + +import {proposalDb} from '../database'; +import { + IFundingRequestProposal, + IProposalFile, + IProposalImage, + IProposalLink, +} from '../proposalTypes'; +import {createEvent} from '../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../event/event'; const createFundingProposalValidationSchema = yup.object({ - commonId: yup - .string() - .uuid() - .required(), + commonId: yup.string().uuid().required(), - proposerId: yup - .string() - .required(), + proposerId: yup.string().required(), - title: yup - .string() - .required(), + title: yup.string().required(), - description: yup - .string() - .required(), + description: yup.string().required(), - amount: yup - .number() - .required(), + amount: yup.number().required(), - links: yup.array(linkValidationSchema) - .optional(), + links: yup.array(linkValidationSchema).optional(), - files: yup.array(fileValidationSchema) - .optional(), + files: yup.array(fileValidationSchema).optional(), - images: yup.array(imageValidationSchema) - .optional() + images: yup.array(imageValidationSchema).optional(), }); -type CreateFundingProposalPayload = yup.InferType; +type CreateFundingProposalPayload = yup.InferType< + typeof createFundingProposalValidationSchema +>; -export const createFundingRequest = async (payload: CreateFundingProposalPayload): Promise => { - await validate(payload, createFundingProposalValidationSchema); +export const createFundingRequest = async ( + payload: CreateFundingProposalPayload, +): Promise => { + await validate( + payload, + createFundingProposalValidationSchema, + ); // Acquire the necessary data const common = await commonDb.get(payload.commonId); // Check if user is member of the common if (!isCommonMember(common, payload.proposerId)) { - throw new CommonError('User tried to create funding request in common, that he is not part of', { - statusCode: StatusCodes.Forbidden, - - userMessage: 'You can only create funding requests in commons, that you are part of', - - commonId: common.id, - userId: payload.proposerId - }); + throw new CommonError( + 'User tried to create funding request in common, that he is not part of', + { + statusCode: StatusCodes.Forbidden, + + userMessage: + 'You can only create funding requests in commons, that you are part of', + + commonId: common.id, + userId: payload.proposerId, + }, + ); } // @question @@ -71,7 +77,6 @@ export const createFundingRequest = async (payload: CreateFundingProposalPayload // @todo Check if the common has enough funds - // Create the funding proposal const fundingProposal = await proposalDb.addProposal({ proposerId: payload.proposerId, @@ -82,9 +87,9 @@ export const createFundingRequest = async (payload: CreateFundingProposalPayload description: { title: payload.title, description: payload.description, - links: payload.links as Nullable || [], - images: payload.images as Nullable || [], - files: payload.files as Nullable || [] + links: (payload.links as Nullable) || [], + images: (payload.images as Nullable) || [], + files: (payload.files as Nullable) || [], }, fundingRequest: { @@ -93,16 +98,16 @@ export const createFundingRequest = async (payload: CreateFundingProposalPayload }, countdownPeriod: env.durations.funding.countdownPeriod, - quietEndingPeriod: env.durations.funding.quietEndingPeriod + quietEndingPeriod: env.durations.funding.quietEndingPeriod, }); // Emit funding request created event await createEvent({ userId: payload.proposerId, objectId: fundingProposal.id, - type: EVENT_TYPES.FUNDING_REQUEST_CREATED - }) + type: EVENT_TYPES.FUNDING_REQUEST_CREATED, + }); // Return the payload return fundingProposal as IFundingRequestProposal; -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/business/deleteProposal.ts b/functions/src/proposals/business/deleteProposal.ts index a20a811b..0d2eda74 100644 --- a/functions/src/proposals/business/deleteProposal.ts +++ b/functions/src/proposals/business/deleteProposal.ts @@ -1,17 +1,21 @@ -import { IProposalEntity } from '../proposalTypes'; -import { proposalDb, voteDb } from '../database'; +import {IProposalEntity} from '../proposalTypes'; +import {proposalDb, voteDb} from '../database'; /** * Tries to delete the proposal from the database and all it's related entities * * @param proposal - The proposal to delete */ -export const deleteProposal = async (proposal: IProposalEntity): Promise => { +export const deleteProposal = async ( + proposal: IProposalEntity, +): Promise => { // Find and delete all votes that this proposal owns const deleteProposalVotesPromiseArr: Promise[] = []; const votes = await voteDb.getAllProposalVotes(proposal.id); - votes.forEach(vote => deleteProposalVotesPromiseArr.push(voteDb.delete(vote.id))); + votes.forEach((vote) => + deleteProposalVotesPromiseArr.push(voteDb.delete(vote.id)), + ); await Promise.all(deleteProposalVotesPromiseArr); @@ -20,6 +24,6 @@ export const deleteProposal = async (proposal: IProposalEntity): Promise = logger.info('Successfully deleted proposal and related votes', { proposal, - votes + votes, }); -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/business/finalizeProposal.ts b/functions/src/proposals/business/finalizeProposal.ts index a2dd91e5..285ba363 100644 --- a/functions/src/proposals/business/finalizeProposal.ts +++ b/functions/src/proposals/business/finalizeProposal.ts @@ -1,14 +1,14 @@ -import { ArgumentError, CommonError } from '../../util/errors'; -import { createEvent } from '../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../event/event'; +import {ArgumentError, CommonError} from '../../util/errors'; +import {createEvent} from '../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../event/event'; -import { updateProposal } from '../database/updateProposal'; -import { IProposalEntity } from '../proposalTypes'; +import {updateProposal} from '../database/updateProposal'; +import {IProposalEntity} from '../proposalTypes'; -import { countVotes } from './countVotes'; -import { hasAbsoluteMajority } from './hasAbsoluteMajority'; -import { isExpired } from './isExpired'; -import { commonDb } from '../../common/database'; +import {countVotes} from './countVotes'; +import {hasAbsoluteMajority} from './hasAbsoluteMajority'; +import {isExpired} from './isExpired'; +import {commonDb} from '../../common/database'; /** * Finalizes (counts votes, changes status and more) the passed proposal @@ -19,15 +19,17 @@ import { commonDb } from '../../common/database'; * * @returns - The finalized proposal */ -export const finalizeProposal = async (proposal: IProposalEntity): Promise => { +export const finalizeProposal = async ( + proposal: IProposalEntity, +): Promise => { if (!proposal) { throw new ArgumentError('proposal', proposal); } // If the proposal does not have a majority and is not expired we should not finalize it - if (!await isExpired(proposal) && !(await hasAbsoluteMajority(proposal))) { + if (!(await isExpired(proposal)) && !(await hasAbsoluteMajority(proposal))) { throw new CommonError('Trying to finalize non expired proposal', { - proposal + proposal, }); } @@ -35,8 +37,7 @@ export const finalizeProposal = async (proposal: IProposalEntity): Promise votes.votesAgainst && - votes.votesFor > 0 + votes.votesFor > votes.votesAgainst && votes.votesFor > 0 ? 'passed' : 'failed'; @@ -47,7 +48,7 @@ export const finalizeProposal = async (proposal: IProposalEntity): Promise => { } if (common.balance < proposal.fundingRequest.amount) { - logger.warn(`Proposal with id ${proposal.id} cannot be funded, because the common does not have enough balance!`); + logger.warn( + `Proposal with id ${proposal.id} cannot be funded, because the common does not have enough balance!`, + ); - throw new CommonError(`Proposal with id ${proposal.id} cannot be funded, because the common does not have enough balance!`); + throw new CommonError( + `Proposal with id ${proposal.id} cannot be funded, because the common does not have enough balance!`, + ); return; } // Change the commons balance and // update the funding proposal - common.balance = FieldValue.increment(proposal.fundingRequest.amount * -1) as any; + common.balance = FieldValue.increment( + proposal.fundingRequest.amount * -1, + ) as any; proposal.fundingRequest.funded = true; // Persist the changes asynchronously - await Promise.all([ - commonDb.update(common), - proposalDb.update(proposal) - ]); -}; \ No newline at end of file + await Promise.all([commonDb.update(common), proposalDb.update(proposal)]); +}; diff --git a/functions/src/proposals/business/hasAbsoluteMajority.ts b/functions/src/proposals/business/hasAbsoluteMajority.ts index 14c74f87..0f4fa512 100644 --- a/functions/src/proposals/business/hasAbsoluteMajority.ts +++ b/functions/src/proposals/business/hasAbsoluteMajority.ts @@ -1,10 +1,10 @@ -import { ArgumentError } from '../../util/errors'; +import {ArgumentError} from '../../util/errors'; -import { ICommonEntity } from '../../common/types'; -import { commonDb } from '../../common/database'; +import {ICommonEntity} from '../../common/types'; +import {commonDb} from '../../common/database'; -import { IProposalEntity } from '../proposalTypes'; -import { countVotes } from './countVotes'; +import {IProposalEntity} from '../proposalTypes'; +import {countVotes} from './countVotes'; /** * Checks if proposal has majority for any of the vote options @@ -16,18 +16,23 @@ import { countVotes } from './countVotes'; * * @returns - Boolean specifying if the proposal has majority in either of the votes */ -export const hasAbsoluteMajority = async (proposal: IProposalEntity, common?: ICommonEntity): Promise => { - if(!proposal) { +export const hasAbsoluteMajority = async ( + proposal: IProposalEntity, + common?: ICommonEntity, +): Promise => { + if (!proposal) { throw new ArgumentError('proposal', proposal); } - if(!common) { + if (!common) { common = await commonDb.get(proposal.commonId); } const votes = countVotes(proposal); const votesNeededForMajority = Math.floor(common.members.length / 2) + 1; - return votes.votesAgainst >= votesNeededForMajority - || votes.votesFor >= votesNeededForMajority; -} \ No newline at end of file + return ( + votes.votesAgainst >= votesNeededForMajority || + votes.votesFor >= votesNeededForMajority + ); +}; diff --git a/functions/src/proposals/business/isExpired.ts b/functions/src/proposals/business/isExpired.ts index f4845280..ccbdfd49 100644 --- a/functions/src/proposals/business/isExpired.ts +++ b/functions/src/proposals/business/isExpired.ts @@ -1,5 +1,5 @@ -import { IProposalEntity } from '../proposalTypes'; -import { ArgumentError } from '../../util/errors'; +import {IProposalEntity} from '../proposalTypes'; +import {ArgumentError} from '../../util/errors'; /** * Returns whether the passed proposal has expired or @@ -12,7 +12,9 @@ import { ArgumentError } from '../../util/errors'; * * @returns - Promise resolved in boolean, the result */ -export const isExpired = async (proposal: IProposalEntity): Promise => { +export const isExpired = async ( + proposal: IProposalEntity, +): Promise => { if (!proposal) { throw new ArgumentError('proposal', proposal); } @@ -22,8 +24,10 @@ export const isExpired = async (proposal: IProposalEntity): Promise => } const now = new Date(); - const expiration = new Date(proposal.createdAt.toDate().getTime() + (proposal.countdownPeriod * 1000)); + const expiration = new Date( + proposal.createdAt.toDate().getTime() + proposal.countdownPeriod * 1000, + ); // If the expiration is in the past it is therefore expired return expiration < now; -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/business/isInQuietEnding.ts b/functions/src/proposals/business/isInQuietEnding.ts index 2b5c86af..a4147bc5 100644 --- a/functions/src/proposals/business/isInQuietEnding.ts +++ b/functions/src/proposals/business/isInQuietEnding.ts @@ -1,4 +1,4 @@ -import { IProposalEntity } from '../proposalTypes'; +import {IProposalEntity} from '../proposalTypes'; /** * Checks if the current proposal is in it's quiet ending period @@ -7,8 +7,10 @@ import { IProposalEntity } from '../proposalTypes'; */ export const isInQuietEnding = (proposal: IProposalEntity): boolean => { const now = new Date(); - const expiration = new Date(now.getTime() + (proposal.countdownPeriod * 1000)); - const quietEndingStart = new Date(expiration.getTime() - (proposal.quietEndingPeriod * 1000)); + const expiration = new Date(now.getTime() + proposal.countdownPeriod * 1000); + const quietEndingStart = new Date( + expiration.getTime() - proposal.quietEndingPeriod * 1000, + ); return now < expiration && now > quietEndingStart; -} \ No newline at end of file +}; diff --git a/functions/src/proposals/business/votes/createVote.ts b/functions/src/proposals/business/votes/createVote.ts index f29712be..9905abc1 100644 --- a/functions/src/proposals/business/votes/createVote.ts +++ b/functions/src/proposals/business/votes/createVote.ts @@ -1,30 +1,25 @@ import * as yup from 'yup'; -import { IVoteEntity, VoteOutcome } from '../../voteTypes'; -import { commonDb } from '../../../common/database'; -import { isCommonMember } from '../../../common/business'; -import { proposalDb, voteDb } from '../../database'; -import { ProposalFinalStates, StatusCodes } from '../../../constants'; -import { validate } from '../../../util/validate'; -import { CommonError } from '../../../util/errors'; -import { hasVoted } from './hasVoted'; -import { isExpired } from '../isExpired'; -import { createEvent } from '../../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../../event/event'; -import { processVote } from './processVotes'; -import { finalizeProposal } from '../finalizeProposal'; +import {IVoteEntity, VoteOutcome} from '../../voteTypes'; +import {commonDb} from '../../../common/database'; +import {isCommonMember} from '../../../common/business'; +import {proposalDb, voteDb} from '../../database'; +import {ProposalFinalStates, StatusCodes} from '../../../constants'; +import {validate} from '../../../util/validate'; +import {CommonError} from '../../../util/errors'; +import {hasVoted} from './hasVoted'; +import {isExpired} from '../isExpired'; +import {createEvent} from '../../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../../event/event'; +import {processVote} from './processVotes'; +import {finalizeProposal} from '../finalizeProposal'; const createVoteValidationScheme = yup.object({ - voterId: yup.string() - .required(), + voterId: yup.string().required(), - proposalId: yup.string() - .required() - .uuid(), + proposalId: yup.string().required().uuid(), - outcome: yup.string() - .required() - .oneOf(['rejected', 'approved']) + outcome: yup.string().required().oneOf(['rejected', 'approved']), }); type CreateVotePayload = yup.InferType; @@ -42,7 +37,9 @@ type CreateVotePayload = yup.InferType; * * @returns - The created vote entity as is in the *Votes* collection */ -export const createVote = async (payload: CreateVotePayload): Promise => { +export const createVote = async ( + payload: CreateVotePayload, +): Promise => { // Validate the data await validate(payload, createVoteValidationScheme); @@ -52,30 +49,36 @@ export const createVote = async (payload: CreateVotePayload): Promise => - proposal.votes.some(x => x.voterId === voterId); +export const hasVoted = async ( + proposal: IProposalEntity, + voterId: string, +): Promise => proposal.votes.some((x) => x.voterId === voterId); diff --git a/functions/src/proposals/business/votes/processVotes.ts b/functions/src/proposals/business/votes/processVotes.ts index 6564a567..856a8ec4 100644 --- a/functions/src/proposals/business/votes/processVotes.ts +++ b/functions/src/proposals/business/votes/processVotes.ts @@ -1,11 +1,11 @@ -import { IVoteEntity } from '../../voteTypes'; -import { proposalDb, voteDb } from '../../database'; -import { hasAbsoluteMajority } from '../hasAbsoluteMajority'; -import { finalizeProposal } from '../finalizeProposal'; -import { commonDb } from '../../../common/database'; -import { countVotes } from '../countVotes'; -import { isInQuietEnding } from '../isInQuietEnding'; -import { updateProposal } from '../../database/updateProposal'; +import {IVoteEntity} from '../../voteTypes'; +import {proposalDb, voteDb} from '../../database'; +import {hasAbsoluteMajority} from '../hasAbsoluteMajority'; +import {finalizeProposal} from '../finalizeProposal'; +import {commonDb} from '../../../common/database'; +import {countVotes} from '../countVotes'; +import {isInQuietEnding} from '../isInQuietEnding'; +import {updateProposal} from '../../database/updateProposal'; export const processVote = async (vote: IVoteEntity): Promise => { const proposal = await proposalDb.getProposal(vote.proposalId); @@ -17,12 +17,14 @@ export const processVote = async (vote: IVoteEntity): Promise => { proposal.votes.push({ voteId: vote.id, voterId: vote.voterId, - voteOutcome: vote.outcome + voteOutcome: vote.outcome, }); // Check for majority and update the proposal state if (await hasAbsoluteMajority(proposal, common)) { - logger.info(`After vote (${vote.id}) proposal (${proposal.id}) has majority. Finalizing.`); + logger.info( + `After vote (${vote.id}) proposal (${proposal.id}) has majority. Finalizing.`, + ); await finalizeProposal(proposal); } @@ -37,7 +39,7 @@ export const processVote = async (vote: IVoteEntity): Promise => { proposal.votes.push({ voteId: voteData.id, voterId: voteData.voterId, - voteOutcome: voteData.outcome + voteOutcome: voteData.outcome, }); }); } @@ -66,4 +68,4 @@ export const processVote = async (vote: IVoteEntity): Promise => { // Save all changes to the database await updateProposal(proposal); -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/crons/finalizeProposalsCron.ts b/functions/src/proposals/crons/finalizeProposalsCron.ts index 91441335..1e4e7d2d 100644 --- a/functions/src/proposals/crons/finalizeProposalsCron.ts +++ b/functions/src/proposals/crons/finalizeProposalsCron.ts @@ -1,28 +1,29 @@ import * as functions from 'firebase-functions'; -import { proposalDb } from '../database'; -import { isExpired } from '../business/isExpired'; -import { finalizeProposal } from '../business/finalizeProposal'; - +import {proposalDb} from '../database'; +import {isExpired} from '../business/isExpired'; +import {finalizeProposal} from '../business/finalizeProposal'; export const finalizeProposals = functions.pubsub .schedule('*/5 * * * *') // At every 5th minute .onRun(async () => { - const proposals = await proposalDb.getProposals({ state: 'countdown' }); + const proposals = await proposalDb.getProposals({state: 'countdown'}); const promiseArray: Promise[] = []; for (const proposal of proposals) { // eslint-disable-next-line no-loop-func - promiseArray.push((async () => { - if (await isExpired(proposal)) { - logger.info(`Finalizing expired proposal with id ${proposal.id}`, { - proposal - }); + promiseArray.push( + (async () => { + if (await isExpired(proposal)) { + logger.info(`Finalizing expired proposal with id ${proposal.id}`, { + proposal, + }); - await finalizeProposal(proposal); - } - })()); + await finalizeProposal(proposal); + } + })(), + ); } await Promise.all(promiseArray); diff --git a/functions/src/proposals/crons/index.ts b/functions/src/proposals/crons/index.ts index c5b9a587..d5ca074c 100644 --- a/functions/src/proposals/crons/index.ts +++ b/functions/src/proposals/crons/index.ts @@ -1 +1 @@ -export { finalizeProposals } from './finalizeProposalsCron'; \ No newline at end of file +export {finalizeProposals} from './finalizeProposalsCron'; diff --git a/functions/src/proposals/database/addProposal.ts b/functions/src/proposals/database/addProposal.ts index 755e898e..fe2ddece 100644 --- a/functions/src/proposals/database/addProposal.ts +++ b/functions/src/proposals/database/addProposal.ts @@ -1,14 +1,18 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; -import { BaseEntityType, SharedOmit } from '../../util/types'; +import {BaseEntityType, SharedOmit} from '../../util/types'; -import { IProposalEntity } from '../proposalTypes'; -import { ProposalsCollection } from './index'; +import {IProposalEntity} from '../proposalTypes'; +import {ProposalsCollection} from './index'; - -type OmittedProperties = 'votes' | 'state' | 'paymentState' | 'votesFor' | 'votesAgainst'; +type OmittedProperties = + | 'votes' + | 'state' + | 'paymentState' + | 'votesFor' + | 'votesAgainst'; /** * Prepares the passed proposal for saving and saves it. Please note that @@ -16,7 +20,9 @@ type OmittedProperties = 'votes' | 'state' | 'paymentState' | 'votesFor' | 'vote * * @param proposal - the proposal to be saved */ -export const addProposal = async (proposal: SharedOmit): Promise => { +export const addProposal = async ( + proposal: SharedOmit, +): Promise => { const proposalDoc: IProposalEntity = { id: v4(), @@ -32,16 +38,14 @@ export const addProposal = async (proposal: SharedOmit => { +export const deleteProposalFromDatabase = async ( + proposalId: string, +): Promise => { if (!proposalId) { throw new ArgumentError('proposalId'); } logger.notice(`Deleting proposal with ID ${proposalId}`); - return (await ProposalsCollection - .doc(proposalId) - .delete()); -}; \ No newline at end of file + return await ProposalsCollection.doc(proposalId).delete(); +}; diff --git a/functions/src/proposals/database/getFundingRequest.ts b/functions/src/proposals/database/getFundingRequest.ts index 86c4a0d3..cbcc56a4 100644 --- a/functions/src/proposals/database/getFundingRequest.ts +++ b/functions/src/proposals/database/getFundingRequest.ts @@ -1,9 +1,9 @@ -import { ArgumentError, CommonError } from '../../util/errors'; -import { NotFoundError } from '../../util/errors'; -import { Nullable } from '../../util/types'; +import {ArgumentError, CommonError} from '../../util/errors'; +import {NotFoundError} from '../../util/errors'; +import {Nullable} from '../../util/types'; -import { ProposalsCollection } from './index'; -import { IFundingRequestProposal, IProposalEntity } from '../proposalTypes'; +import {ProposalsCollection} from './index'; +import {IFundingRequestProposal, IProposalEntity} from '../proposalTypes'; /** * Gets funding request by id @@ -16,14 +16,16 @@ import { IFundingRequestProposal, IProposalEntity } from '../proposalTypes'; * * @returns - The found proposal */ -export const getFundingRequest = async (proposalId: string): Promise => { +export const getFundingRequest = async ( + proposalId: string, +): Promise => { if (!proposalId) { throw new ArgumentError('proposalId', proposalId); } - const proposal = (await ProposalsCollection - .doc(proposalId) - .get()).data() as Nullable; + const proposal = ( + await ProposalsCollection.doc(proposalId).get() + ).data() as Nullable; if (!proposal) { throw new NotFoundError(proposalId, 'proposal'); @@ -31,9 +33,9 @@ export const getFundingRequest = async (proposalId: string): Promise => { +export const getJoinRequest = async ( + proposalId: string, +): Promise => { if (!proposalId) { throw new ArgumentError('proposalId', proposalId); } - const proposal = (await ProposalsCollection - .doc(proposalId) - .get()).data() as Nullable; + const proposal = ( + await ProposalsCollection.doc(proposalId).get() + ).data() as Nullable; if (!proposal) { throw new NotFoundError(proposalId, 'proposal'); @@ -31,9 +33,9 @@ export const getJoinRequest = async (proposalId: string): Promise => { - if(!proposalId) { +export const getProposal = async ( + proposalId: string, + throwErr = true, +): Promise => { + if (!proposalId) { throw new ArgumentError('proposalId', proposalId); } - const proposal = (await ProposalsCollection - .doc(proposalId) - .get()).data() as Nullable; + const proposal = ( + await ProposalsCollection.doc(proposalId).get() + ).data() as Nullable; - if(!proposal && throwErr) { + if (!proposal && throwErr) { throw new NotFoundError(proposalId, 'proposal'); } return proposal; -} \ No newline at end of file +}; diff --git a/functions/src/proposals/database/getProposals.ts b/functions/src/proposals/database/getProposals.ts index 6fa8cda2..44ececf9 100644 --- a/functions/src/proposals/database/getProposals.ts +++ b/functions/src/proposals/database/getProposals.ts @@ -1,11 +1,20 @@ import admin from 'firebase-admin'; -import { ProposalsCollection } from './index'; -import { FundingRequestState, IProposalEntity, ProposalType, RequestToJoinState } from '../proposalTypes'; +import {ProposalsCollection} from './index'; +import { + FundingRequestState, + IProposalEntity, + ProposalType, + RequestToJoinState, +} from '../proposalTypes'; import QuerySnapshot = admin.firestore.QuerySnapshot; interface IGetProposalsOptions { - state?: RequestToJoinState | RequestToJoinState[] | FundingRequestState | FundingRequestState[]; + state?: + | RequestToJoinState + | RequestToJoinState[] + | FundingRequestState + | FundingRequestState[]; type?: ProposalType; proposerId?: string; commonId?: string; @@ -16,7 +25,9 @@ interface IGetProposalsOptions { * * @param options - List of params that all of the returned proposal must match */ -export const getProposals = async (options: IGetProposalsOptions): Promise => { +export const getProposals = async ( + options: IGetProposalsOptions, +): Promise => { let proposalsQuery: any = ProposalsCollection; if (options.state) { @@ -30,13 +41,18 @@ export const getProposals = async (options: IGetProposalsOptions): Promise) - .docs.map(x => x.data()); -}; \ No newline at end of file + return ((await proposalsQuery.get()) as QuerySnapshot).docs.map( + (x) => x.data(), + ); +}; diff --git a/functions/src/proposals/database/index.ts b/functions/src/proposals/database/index.ts index 55592aa2..4c68b60c 100644 --- a/functions/src/proposals/database/index.ts +++ b/functions/src/proposals/database/index.ts @@ -1,18 +1,18 @@ -import { db } from '../../util'; -import { Collections } from '../../constants'; +import {db} from '../../util'; +import {Collections} from '../../constants'; -import { addProposal } from './addProposal'; -import { getProposal } from './getProposal'; -import { getProposals } from './getProposals'; -import { updateProposal } from './updateProposal'; -import { deleteProposalFromDatabase } from './deleteProposal'; +import {addProposal} from './addProposal'; +import {getProposal} from './getProposal'; +import {getProposals} from './getProposals'; +import {updateProposal} from './updateProposal'; +import {deleteProposalFromDatabase} from './deleteProposal'; -import { addVote } from './votes/addVote'; -import { getVote } from './votes/getVote'; -import { getAllProposalVotes } from './votes/getAllProposalVotes'; -import { getFundingRequest } from './getFundingRequest'; -import { getJoinRequest } from './getJoinRequest'; -import { deleteVoteFromDatabase } from './votes/deleteVote'; +import {addVote} from './votes/addVote'; +import {getVote} from './votes/getVote'; +import {getAllProposalVotes} from './votes/getAllProposalVotes'; +import {getFundingRequest} from './getFundingRequest'; +import {getJoinRequest} from './getJoinRequest'; +import {deleteVoteFromDatabase} from './votes/deleteVote'; export const VotesCollection = db.collection(Collections.Votes); export const ProposalsCollection = db.collection(Collections.Proposals); @@ -25,7 +25,7 @@ export const proposalDb = { getProposals, update: updateProposal, - delete: deleteProposalFromDatabase + delete: deleteProposalFromDatabase, }; export const voteDb = { @@ -33,5 +33,5 @@ export const voteDb = { getVote, getAllProposalVotes, - delete: deleteVoteFromDatabase -} \ No newline at end of file + delete: deleteVoteFromDatabase, +}; diff --git a/functions/src/proposals/database/updateProposal.ts b/functions/src/proposals/database/updateProposal.ts index 10615f95..91971e6b 100644 --- a/functions/src/proposals/database/updateProposal.ts +++ b/functions/src/proposals/database/updateProposal.ts @@ -1,7 +1,7 @@ -import { firestore } from 'firebase-admin'; +import {firestore} from 'firebase-admin'; -import { IProposalEntity } from '../proposalTypes'; -import { ProposalsCollection } from './index'; +import {IProposalEntity} from '../proposalTypes'; +import {ProposalsCollection} from './index'; type WithRequired = Pick & Partial>; @@ -10,16 +10,16 @@ type WithRequired = Pick & Partial>; * * @param proposal - The updated proposal */ -export const updateProposal = async (proposal: WithRequired | IProposalEntity): Promise> => { +export const updateProposal = async ( + proposal: WithRequired | IProposalEntity, +): Promise> => { const proposalDoc = { ...proposal, - updatedAt: firestore.Timestamp.now() + updatedAt: firestore.Timestamp.now(), }; - await ProposalsCollection - .doc(proposalDoc.id) - .update(proposalDoc); + await ProposalsCollection.doc(proposalDoc.id).update(proposalDoc); return proposalDoc; -} \ No newline at end of file +}; diff --git a/functions/src/proposals/database/votes/addVote.ts b/functions/src/proposals/database/votes/addVote.ts index 72c16079..ba4034b7 100644 --- a/functions/src/proposals/database/votes/addVote.ts +++ b/functions/src/proposals/database/votes/addVote.ts @@ -1,33 +1,33 @@ -import { v4 } from 'uuid'; -import { firestore } from 'firebase-admin'; +import {v4} from 'uuid'; +import {firestore} from 'firebase-admin'; -import { IVoteEntity } from '../../voteTypes'; -import { BaseEntityType } from '../../../util/types'; +import {IVoteEntity} from '../../voteTypes'; +import {BaseEntityType} from '../../../util/types'; -import { VotesCollection } from '../index'; +import {VotesCollection} from '../index'; /** * Creates a vote document and saves it in the database * * @param vote - the vote object, from witch the document will be constructed */ -export const addVote = async (vote: Omit): Promise => { +export const addVote = async ( + vote: Omit, +): Promise => { const voteDoc: IVoteEntity = { id: v4(), createdAt: firestore.Timestamp.fromDate(new Date()), updatedAt: firestore.Timestamp.fromDate(new Date()), - ...vote + ...vote, }; - if(process.env.NODE_ENV === 'test') { + if (process.env.NODE_ENV === 'test') { voteDoc['testCreated'] = true; } - await VotesCollection - .doc(voteDoc.id) - .set(voteDoc); + await VotesCollection.doc(voteDoc.id).set(voteDoc); return voteDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/proposals/database/votes/deleteVote.ts b/functions/src/proposals/database/votes/deleteVote.ts index c47514ce..4c4e6da6 100644 --- a/functions/src/proposals/database/votes/deleteVote.ts +++ b/functions/src/proposals/database/votes/deleteVote.ts @@ -2,10 +2,9 @@ import admin from 'firebase-admin'; import WriteResult = admin.firestore.WriteResult; -import { ArgumentError } from '../../../util/errors'; - -import { VotesCollection } from '../index'; +import {ArgumentError} from '../../../util/errors'; +import {VotesCollection} from '../index'; /** * Deletes vote. Use carefully. If you want to cleanly delete the @@ -16,14 +15,14 @@ import { VotesCollection } from '../index'; * * @throws { ArgumentError } - If the vote ID is not provided */ -export const deleteVoteFromDatabase = async (voteId: string): Promise => { +export const deleteVoteFromDatabase = async ( + voteId: string, +): Promise => { if (!voteId) { throw new ArgumentError('voteId'); } logger.notice(`Deleting vote with ID ${voteId}`); - return (await VotesCollection - .doc(voteId) - .delete()); -}; \ No newline at end of file + return await VotesCollection.doc(voteId).delete(); +}; diff --git a/functions/src/proposals/database/votes/getAllProposalVotes.ts b/functions/src/proposals/database/votes/getAllProposalVotes.ts index afe926ee..83eef700 100644 --- a/functions/src/proposals/database/votes/getAllProposalVotes.ts +++ b/functions/src/proposals/database/votes/getAllProposalVotes.ts @@ -1,20 +1,23 @@ import admin from 'firebase-admin'; -import { IVoteEntity } from '../../voteTypes'; -import { VotesCollection } from '../index'; +import {IVoteEntity} from '../../voteTypes'; +import {VotesCollection} from '../index'; import QuerySnapshot = admin.firestore.QuerySnapshot; - /** * Returns array of all votes casted to the proposal * * @param proposalId - The ID of the proposal for witch we want to retrieve the proposals */ -export const getAllProposalVotes = async (proposalId: string): Promise => { - const votes = await VotesCollection - .where('proposalId', '==', proposalId) - .get() as QuerySnapshot; +export const getAllProposalVotes = async ( + proposalId: string, +): Promise => { + const votes = (await VotesCollection.where( + 'proposalId', + '==', + proposalId, + ).get()) as QuerySnapshot; - return votes.docs.map(x => x.data()); -}; \ No newline at end of file + return votes.docs.map((x) => x.data()); +}; diff --git a/functions/src/proposals/database/votes/getVote.ts b/functions/src/proposals/database/votes/getVote.ts index e505ca71..3d8071cc 100644 --- a/functions/src/proposals/database/votes/getVote.ts +++ b/functions/src/proposals/database/votes/getVote.ts @@ -1,9 +1,9 @@ -import { ArgumentError } from '../../../util/errors'; -import { NotFoundError } from '../../../util/errors'; -import { Nullable } from '../../../util/types'; +import {ArgumentError} from '../../../util/errors'; +import {NotFoundError} from '../../../util/errors'; +import {Nullable} from '../../../util/types'; -import { VotesCollection } from '../index'; -import { IVoteEntity } from '../../voteTypes'; +import {VotesCollection} from '../index'; +import {IVoteEntity} from '../../voteTypes'; /** * Gets vote by id @@ -16,17 +16,17 @@ import { IVoteEntity } from '../../voteTypes'; * @returns - The found vote */ export const getVote = async (voteId: string): Promise => { - if(!voteId) { + if (!voteId) { throw new ArgumentError('voteId', voteId); } - const vote = (await VotesCollection - .doc(voteId) - .get()).data() as Nullable; + const vote = ( + await VotesCollection.doc(voteId).get() + ).data() as Nullable; - if(!vote) { + if (!vote) { throw new NotFoundError(voteId, 'vote'); } return vote; -} \ No newline at end of file +}; diff --git a/functions/src/proposals/index.ts b/functions/src/proposals/index.ts index c1e0bc10..9ce33583 100644 --- a/functions/src/proposals/index.ts +++ b/functions/src/proposals/index.ts @@ -1,12 +1,12 @@ import * as functions from 'firebase-functions'; -import { commonApp, commonRouter } from '../util'; -import { runtimeOptions } from '../constants'; -import { responseExecutor } from '../util/responseExecutor'; +import {commonApp, commonRouter} from '../util'; +import {runtimeOptions} from '../constants'; +import {responseExecutor} from '../util/responseExecutor'; -import { createVote } from './business/votes/createVote'; -import { createJoinRequest } from './business/createJoinRequest'; -import { createFundingRequest } from './business/createFundingRequest'; +import {createVote} from './business/votes/createVote'; +import {createJoinRequest} from './business/createJoinRequest'; +import {createFundingRequest} from './business/createFundingRequest'; import * as crons from './crons'; import * as triggers from './triggers'; @@ -18,43 +18,51 @@ router.post('/create/join', async (req, res, next) => { async () => { return await createJoinRequest({ ...req.body, - proposerId: req.user.uid + proposerId: req.user.uid, }); - }, { + }, + { req, res, next, - successMessage: 'Join request successfully created!' - }); + successMessage: 'Join request successfully created!', + }, + ); }); router.post('/create/funding', async (req, res, next) => { - await responseExecutor(async () => { - return createFundingRequest({ - ...req.body, - proposerId: req.user.uid - }); - }, { - req, - res, - next, - successMessage: 'Funding request successfully created!' - }); + await responseExecutor( + async () => { + return createFundingRequest({ + ...req.body, + proposerId: req.user.uid, + }); + }, + { + req, + res, + next, + successMessage: 'Funding request successfully created!', + }, + ); }); router.post('/create/vote', async (req, res, next) => { - await responseExecutor(async () => { - return createVote({ - voterId: req.user.uid, - proposalId: req.body.proposalId, - outcome: req.body.outcome - }); - }, { - req, - res, - next, - successMessage: `Successfully ${req.body.outcome} proposal!` - }); + await responseExecutor( + async () => { + return createVote({ + voterId: req.user.uid, + proposalId: req.body.proposalId, + outcome: req.body.outcome, + }); + }, + { + req, + res, + next, + successMessage: `Successfully ${req.body.outcome} proposal!`, + }, + ); }); export const proposalsApp = functions @@ -62,4 +70,4 @@ export const proposalsApp = functions .https.onRequest(commonApp(router)); export const proposalCrons = crons; -export const proposalTriggers = triggers; \ No newline at end of file +export const proposalTriggers = triggers; diff --git a/functions/src/proposals/proposalTypes.ts b/functions/src/proposals/proposalTypes.ts index 72b77376..0a95a9a2 100644 --- a/functions/src/proposals/proposalTypes.ts +++ b/functions/src/proposals/proposalTypes.ts @@ -1,11 +1,20 @@ -import { IBaseEntity } from '../util/types'; -import { ContributionType } from '../common/types'; -import { VoteOutcome } from './voteTypes'; - -export type FundingRequestState = 'countdown' | 'passed' | 'failed' | 'passedInsufficientBalance'; +import {IBaseEntity} from '../util/types'; +import {ContributionType} from '../common/types'; +import {VoteOutcome} from './voteTypes'; + +export type FundingRequestState = + | 'countdown' + | 'passed' + | 'failed' + | 'passedInsufficientBalance'; export type RequestToJoinState = 'countdown' | 'passed' | 'failed'; -export type ProposalPaymentState = 'notAttempted' | 'pending' | 'failed' | 'confirmed' | 'notRelevant'; +export type ProposalPaymentState = + | 'notAttempted' + | 'pending' + | 'failed' + | 'confirmed' + | 'notRelevant'; /** * The base proposal fields, that will be available @@ -139,23 +148,24 @@ export interface IFundingRequestProposal extends IBaseProposalEntity { /** * Object with some description of the proposal */ - description: IProposalDescription | { - /** - * The proposal in short - */ - title: string; - - /** - * Collection of images supporting the request - */ - images: IProposalImage[]; - - /** - * Collection of files supporting the request - */ - files: IProposalFile[]; - }; - + description: + | IProposalDescription + | { + /** + * The proposal in short + */ + title: string; + + /** + * Collection of images supporting the request + */ + images: IProposalImage[]; + + /** + * Collection of files supporting the request + */ + files: IProposalFile[]; + }; fundingRequest: { /** @@ -219,7 +229,6 @@ export interface IJoinRequestProposal extends IBaseProposalEntity { }; } - export type ProposalType = 'join' | 'fundingRequest'; /** diff --git a/functions/src/proposals/triggers/index.ts b/functions/src/proposals/triggers/index.ts index 6da97433..d8c456b5 100644 --- a/functions/src/proposals/triggers/index.ts +++ b/functions/src/proposals/triggers/index.ts @@ -1 +1 @@ -export { onProposalApproved } from './onProposalApproved'; +export {onProposalApproved} from './onProposalApproved'; diff --git a/functions/src/proposals/triggers/onProposalApproved.ts b/functions/src/proposals/triggers/onProposalApproved.ts index d83678b2..79402369 100644 --- a/functions/src/proposals/triggers/onProposalApproved.ts +++ b/functions/src/proposals/triggers/onProposalApproved.ts @@ -1,63 +1,64 @@ import * as functions from 'firebase-functions'; -import { Collections } from '../../constants'; -import { IEventEntity } from '../../event/type'; -import { EVENT_TYPES } from '../../event/event'; -import { fundProposal } from '../business/fundProposal'; -import { createSubscription } from '../../subscriptions/business'; -import { commonDb } from '../../common/database'; -import { proposalDb } from '../database'; -import { createEvent } from '../../util/db/eventDbService'; -import { createProposalPayment } from '../../circlepay/payments/business/createProposalPayment'; -import { addCommonMemberByProposalId } from '../../common/business/addCommonMember'; - +import {Collections} from '../../constants'; +import {IEventEntity} from '../../event/type'; +import {EVENT_TYPES} from '../../event/event'; +import {fundProposal} from '../business/fundProposal'; +import {createSubscription} from '../../subscriptions/business'; +import {commonDb} from '../../common/database'; +import {proposalDb} from '../database'; +import {createEvent} from '../../util/db/eventDbService'; +import {createProposalPayment} from '../../circlepay/payments/business/createProposalPayment'; +import {addCommonMemberByProposalId} from '../../common/business/addCommonMember'; export const onProposalApproved = functions.firestore .document(`/${Collections.Event}/{id}`) .onCreate(async (eventSnap, context) => { - const event = eventSnap.data() as IEventEntity; + const event = eventSnap.data() as IEventEntity; - if (event.type === EVENT_TYPES.FUNDING_REQUEST_ACCEPTED) { - logger.info('Funding request was approved. Crunching some numbers'); + if (event.type === EVENT_TYPES.FUNDING_REQUEST_ACCEPTED) { + logger.info('Funding request was approved. Crunching some numbers'); - await fundProposal(event.objectId); + await fundProposal(event.objectId); - // Everything went fine so it is event time - await createEvent({ - userId: event.userId, - objectId: event.objectId, - type: EVENT_TYPES.FUNDING_REQUEST_EXECUTED - }); - } + // Everything went fine so it is event time + await createEvent({ + userId: event.userId, + objectId: event.objectId, + type: EVENT_TYPES.FUNDING_REQUEST_EXECUTED, + }); + } - // @refactor - if (event.type === EVENT_TYPES.REQUEST_TO_JOIN_ACCEPTED) { - logger.info('Join request was approved. Starting to process payment'); + // @refactor + if (event.type === EVENT_TYPES.REQUEST_TO_JOIN_ACCEPTED) { + logger.info('Join request was approved. Starting to process payment'); - const proposal = await proposalDb.getJoinRequest(event.objectId); + const proposal = await proposalDb.getJoinRequest(event.objectId); - // If the proposal is monthly create subscription. Otherwise charge - if (proposal.join.fundingType === 'monthly') { - await createSubscription(proposal); - } else { - // Create the payment - await createProposalPayment({ + // If the proposal is monthly create subscription. Otherwise charge + if (proposal.join.fundingType === 'monthly') { + await createSubscription(proposal); + } else { + // Create the payment + await createProposalPayment( + { proposalId: proposal.id, sessionId: context.eventId, - ipAddress: '127.0.0.1' // @todo Get ip, but what IP? - }, { throwOnFailure: true }); + ipAddress: '127.0.0.1', // @todo Get ip, but what IP? + }, + {throwOnFailure: true}, + ); - // Update common funding info - const common = await commonDb.get(proposal.commonId); + // Update common funding info + const common = await commonDb.get(proposal.commonId); - common.raised += proposal.join.funding; - common.balance += proposal.join.funding; + common.raised += proposal.join.funding; + common.balance += proposal.join.funding; - await commonDb.update(common); + await commonDb.update(common); - // Add the user as member - await addCommonMemberByProposalId(proposal.id); - } + // Add the user as member + await addCommonMemberByProposalId(proposal.id); } } - ); + }); diff --git a/functions/src/proposals/voteTypes.ts b/functions/src/proposals/voteTypes.ts index 78446560..7768a5b0 100644 --- a/functions/src/proposals/voteTypes.ts +++ b/functions/src/proposals/voteTypes.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../util/types'; +import {IBaseEntity} from '../util/types'; export interface IVoteEntity extends IBaseEntity { /** @@ -23,4 +23,4 @@ export interface IVoteEntity extends IBaseEntity { outcome: VoteOutcome; } -export type VoteOutcome = 'approved' | 'rejected'; \ No newline at end of file +export type VoteOutcome = 'approved' | 'rejected'; diff --git a/functions/src/subscriptions/business/cancelSubscription.ts b/functions/src/subscriptions/business/cancelSubscription.ts index edd57c13..80b3bc21 100644 --- a/functions/src/subscriptions/business/cancelSubscription.ts +++ b/functions/src/subscriptions/business/cancelSubscription.ts @@ -1,37 +1,40 @@ -import { EVENT_TYPES } from '../../event/event'; -import { ISubscriptionEntity } from '../types'; +import {EVENT_TYPES} from '../../event/event'; +import {ISubscriptionEntity} from '../types'; -import { updateSubscription } from '../database/updateSubscription'; -import { revokeMembership } from './revokeMembership'; -import { createEvent } from '../../util/db/eventDbService'; +import {updateSubscription} from '../database/updateSubscription'; +import {revokeMembership} from './revokeMembership'; +import {createEvent} from '../../util/db/eventDbService'; export enum CancellationReason { CanceledByUser = 'CanceledByUser', - CanceledByPaymentFailure = 'CanceledByPaymentFailure' + CanceledByPaymentFailure = 'CanceledByPaymentFailure', } - /** * Cancel recurring payment so the user is not charged again. Does not revoke memberships! * * @param subscriptionId - the id of the subscription * @param cancellationReason - whether the user canceled or the payment has failed multiple times */ -export const cancelSubscription = async (subscription: ISubscriptionEntity, cancellationReason: CancellationReason): Promise => { +export const cancelSubscription = async ( + subscription: ISubscriptionEntity, + cancellationReason: CancellationReason, +): Promise => { subscription.status = cancellationReason; await updateSubscription(subscription); // If the subscription is canceled by payment failure we shoukd - if(cancellationReason === CancellationReason.CanceledByPaymentFailure) { + if (cancellationReason === CancellationReason.CanceledByPaymentFailure) { await revokeMembership(subscription); } await createEvent({ userId: subscription.userId, objectId: subscription.id, - type: cancellationReason === CancellationReason.CanceledByPaymentFailure - ? EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_PAYMENT_FAILURE - : EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_USER + type: + cancellationReason === CancellationReason.CanceledByPaymentFailure + ? EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_PAYMENT_FAILURE + : EVENT_TYPES.SUBSCRIPTION_CANCELED_BY_USER, }); -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/chargeSubscription.ts b/functions/src/subscriptions/business/chargeSubscription.ts index ce31a7e1..8a20e002 100644 --- a/functions/src/subscriptions/business/chargeSubscription.ts +++ b/functions/src/subscriptions/business/chargeSubscription.ts @@ -1,12 +1,12 @@ import moment from 'moment'; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; -import { ISubscriptionEntity } from '../types'; +import {ISubscriptionEntity} from '../types'; -import { EVENT_TYPES } from '../../event/event'; -import { createEvent } from '../../util/db/eventDbService'; -import { subscriptionDb } from '../database'; -import { createSubscriptionPayment } from '../../circlepay/payments/business/createSubscriptionPayment'; +import {EVENT_TYPES} from '../../event/event'; +import {createEvent} from '../../util/db/eventDbService'; +import {subscriptionDb} from '../database'; +import {createSubscriptionPayment} from '../../circlepay/payments/business/createSubscriptionPayment'; /** * Charges one subscription (only if the due date is @@ -14,10 +14,10 @@ import { createSubscriptionPayment } from '../../circlepay/payments/business/cre * * @param subscriptionId - the id of the subscription, that we want to charge */ -export const chargeSubscriptionById = async (subscriptionId: string): Promise => { - await chargeSubscription( - await subscriptionDb.get(subscriptionId) - ); +export const chargeSubscriptionById = async ( + subscriptionId: string, +): Promise => { + await chargeSubscription(await subscriptionDb.get(subscriptionId)); }; /** @@ -26,12 +26,19 @@ export const chargeSubscriptionById = async (subscriptionId: string): Promise => { +export const chargeSubscription = async ( + subscription: ISubscriptionEntity, +): Promise => { // Check if the due date is in the past (only the date and not the time) - if (!moment(subscription.dueDate.toDate()).isSameOrBefore(new Date(), 'day')) { - logger.error(`Trying to charge subscription ${subscription.id}, but the due date is in the future!`, { - subscription - }); + if ( + !moment(subscription.dueDate.toDate()).isSameOrBefore(new Date(), 'day') + ) { + logger.error( + `Trying to charge subscription ${subscription.id}, but the due date is in the future!`, + { + subscription, + }, + ); return; } @@ -40,17 +47,17 @@ export const chargeSubscription = async (subscription: ISubscriptionEntity): Pro await createSubscriptionPayment({ subscriptionId: subscription.id, sessionId: v4(), - ipAddress: '127.0.0.1' + ipAddress: '127.0.0.1', }); } catch (e) { logger.error('Payment for subscription has failed!', { - subscriptionId: subscription.id + subscriptionId: subscription.id, }); await createEvent({ userId: subscription.userId, objectId: subscription.id, - type: EVENT_TYPES.SUBSCRIPTION_PAYMENT_FAILED + type: EVENT_TYPES.SUBSCRIPTION_PAYMENT_FAILED, }); } -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/chargeSubscriptions.ts b/functions/src/subscriptions/business/chargeSubscriptions.ts index e5dd2886..e766137f 100644 --- a/functions/src/subscriptions/business/chargeSubscriptions.ts +++ b/functions/src/subscriptions/business/chargeSubscriptions.ts @@ -1,11 +1,11 @@ import admin from 'firebase-admin'; -import { QuerySnapshot } from '@google-cloud/firestore'; +import {QuerySnapshot} from '@google-cloud/firestore'; -import { Collections } from '../../util/constants'; +import {Collections} from '../../util/constants'; -import { chargeSubscription } from './chargeSubscription'; -import { ISubscriptionEntity } from '../types'; +import {chargeSubscription} from './chargeSubscription'; +import {ISubscriptionEntity} from '../types'; import Timestamp = admin.firestore.Timestamp; const db = admin.firestore(); @@ -17,11 +17,16 @@ const db = admin.firestore(); export const chargeSubscriptions = async (): Promise => { logger.info(`Beginning subscription charging for ${new Date().getDate()}`); - const subscriptionsDueToday = await db.collection(Collections.Subscriptions) + const subscriptionsDueToday = (await db + .collection(Collections.Subscriptions) // .where('dueDate', '>=', new Date().setHours(0,0,0,0)) - .where('dueDate', '<=', Timestamp.fromMillis(new Date().setHours(23, 59, 59, 999))) + .where( + 'dueDate', + '<=', + Timestamp.fromMillis(new Date().setHours(23, 59, 59, 999)), + ) .where('status', 'in', ['Active', 'PaymentFailed']) - .get() as QuerySnapshot; + .get()) as QuerySnapshot; const promiseArr: Promise[] = []; @@ -39,40 +44,46 @@ export const chargeSubscriptions = async (): Promise => { } } - if (subscriptionEntity.status === 'Active' || subscriptionEntity.status === 'PaymentFailed') { + if ( + subscriptionEntity.status === 'Active' || + subscriptionEntity.status === 'PaymentFailed' + ) { // eslint-disable-next-line no-loop-func - promiseArr.push((async () => { - logger.info(`Charging subscription (${subscriptionEntity.id}) with $${subscriptionEntity.amount}`, { - subscription: subscriptionEntity, - date: new Date() - }); - - // Add try/catch so that if one charge fails - // the others won't be canceled because of it - try { - await chargeSubscription(subscriptionEntity); - - // logger.info(`Charged subscription (${subscriptionEntity.id}) with $${subscriptionEntity.amount}`, { - // subscription: subscriptionEntity, - // date: new Date() - // }); - } catch (e) { - logger.warn('Error occurred while trying to charge subscription', { - error: e - }); - } - })()); + promiseArr.push( + (async () => { + logger.info( + `Charging subscription (${subscriptionEntity.id}) with $${subscriptionEntity.amount}`, + { + subscription: subscriptionEntity, + date: new Date(), + }, + ); + + // Add try/catch so that if one charge fails + // the others won't be canceled because of it + try { + await chargeSubscription(subscriptionEntity); + + // logger.info(`Charged subscription (${subscriptionEntity.id}) with $${subscriptionEntity.amount}`, { + // subscription: subscriptionEntity, + // date: new Date() + // }); + } catch (e) { + logger.warn('Error occurred while trying to charge subscription', { + error: e, + }); + } + })(), + ); } else { logger.error(` Subscription (${subscriptionEntity.id}) with unsupported status (${subscriptionEntity.status}) was in the charge loop. `); } - } await Promise.all(promiseArr); logger.info(`Subscriptions charged successfully`); - -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/createSubscription.ts b/functions/src/subscriptions/business/createSubscription.ts index 9ea4ec74..1c16cc72 100644 --- a/functions/src/subscriptions/business/createSubscription.ts +++ b/functions/src/subscriptions/business/createSubscription.ts @@ -1,19 +1,18 @@ -import { v4 } from 'uuid'; +import {v4} from 'uuid'; import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { CommonError } from '../../util/errors'; -import { IProposalEntity } from '../../proposals/proposalTypes'; -import { ISubscriptionEntity } from '../types'; -import { commonDb } from '../../common/database'; -import { subscriptionDb } from '../database'; -import { createEvent } from '../../util/db/eventDbService'; -import { EVENT_TYPES } from '../../event/event'; -import { createSubscriptionPayment } from '../../circlepay/payments/business/createSubscriptionPayment'; -import { isSuccessful } from '../../circlepay/payments/helpers'; -import { addCommonMemberByProposalId } from '../../common/business/addCommonMember'; -import { cardDb } from '../../circlepay/cards/database'; - +import {CommonError} from '../../util/errors'; +import {IProposalEntity} from '../../proposals/proposalTypes'; +import {ISubscriptionEntity} from '../types'; +import {commonDb} from '../../common/database'; +import {subscriptionDb} from '../database'; +import {createEvent} from '../../util/db/eventDbService'; +import {EVENT_TYPES} from '../../event/event'; +import {createSubscriptionPayment} from '../../circlepay/payments/business/createSubscriptionPayment'; +import {isSuccessful} from '../../circlepay/payments/helpers'; +import {addCommonMemberByProposalId} from '../../common/business/addCommonMember'; +import {cardDb} from '../../circlepay/cards/database'; /** * Creates subscription based on proposal @@ -23,23 +22,30 @@ import { cardDb } from '../../circlepay/cards/database'; * @throws { CommonError } - If the proposal is not provided * @throws { CommonError } - If the card, assigned to the proposal, is not found */ -export const createSubscription = async (proposal: IProposalEntity): Promise => { +export const createSubscription = async ( + proposal: IProposalEntity, +): Promise => { if (!proposal || !proposal.id) { throw new CommonError('Cannot create subscription without proposal'); } if (proposal.type !== 'join') { - throw new CommonError('Cannot create subscription for proposals that are not join proposals', { - proposal - }); + throw new CommonError( + 'Cannot create subscription for proposals that are not join proposals', + { + proposal, + }, + ); } - // Check if there is already subscription created for the common - if (await subscriptionDb.exists({ proposalId: proposal.id })) { - throw new CommonError('There is already created subscription for this proposal', { - proposal - }); + if (await subscriptionDb.exists({proposalId: proposal.id})) { + throw new CommonError( + 'There is already created subscription for this proposal', + { + proposal, + }, + ); } // Acquire the required data @@ -66,26 +72,25 @@ export const createSubscription = async (proposal: IProposalEntity): Promise => { +export const handleFailedSubscriptionPayment = async ( + subscription: ISubscriptionEntity, + payment: IPaymentEntity, +): Promise => { const failedPayment = { paymentStatus: payment.status, - paymentId: payment.id + paymentId: payment.id, }; if (subscription.status === 'Active') { @@ -46,7 +49,7 @@ export const handleFailedSubscriptionPayment = async (subscription: ISubscriptio updateSubscription(subscription), await proposalDb.update({ id: subscription.proposalId, - paymentState: 'failed' - }) + paymentState: 'failed', + }), ]); -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/handleSuccesfulPayment.ts b/functions/src/subscriptions/business/handleSuccesfulPayment.ts index 2517f1c3..b2b4e2e1 100644 --- a/functions/src/subscriptions/business/handleSuccesfulPayment.ts +++ b/functions/src/subscriptions/business/handleSuccesfulPayment.ts @@ -4,14 +4,13 @@ import moment from 'moment'; import Timestamp = admin.firestore.Timestamp; import FieldValue = admin.firestore.FieldValue; -import { addMonth } from '../../util'; -import { CommonError } from '../../util/errors'; - -import { ISubscriptionEntity } from '../types'; -import { subscriptionDb } from '../database'; -import { proposalDb } from '../../proposals/database'; -import { commonDb } from '../../common/database'; +import {addMonth} from '../../util'; +import {CommonError} from '../../util/errors'; +import {ISubscriptionEntity} from '../types'; +import {subscriptionDb} from '../database'; +import {proposalDb} from '../../proposals/database'; +import {commonDb} from '../../common/database'; /** * Clears the state of the subscription and updates the due date on payment success @@ -21,7 +20,9 @@ import { commonDb } from '../../common/database'; * @throws { CommonError } - If there is no subscription passed (or is null/undefined) * @throws { CommonError } - If the due date for the subscription is in the future */ -export const handleSuccessfulSubscriptionPayment = async (subscription: ISubscriptionEntity): Promise => { +export const handleSuccessfulSubscriptionPayment = async ( + subscription: ISubscriptionEntity, +): Promise => { if (!subscription) { throw new CommonError(` Cannot handle successful payment without providing subscription object! @@ -42,9 +43,11 @@ export const handleSuccessfulSubscriptionPayment = async (subscription: ISubscri logger.error( `Trying to update due date that is in the future for subscription with id (${subscription.id})! - `, { - subscription - }); + `, + { + subscription, + }, + ); } // Update metadata about the subscription @@ -64,7 +67,7 @@ export const handleSuccessfulSubscriptionPayment = async (subscription: ISubscri subscriptionDb.update(subscription), proposalDb.update({ id: subscription.proposalId, - paymentState: 'confirmed' - }) + paymentState: 'confirmed', + }), ]); -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/index.ts b/functions/src/subscriptions/business/index.ts index 377695a2..c8f1d271 100644 --- a/functions/src/subscriptions/business/index.ts +++ b/functions/src/subscriptions/business/index.ts @@ -1,8 +1,8 @@ -export { chargeSubscription, chargeSubscriptionById } from './chargeSubscription'; -export { handleSuccessfulSubscriptionPayment } from './handleSuccesfulPayment'; -export { chargeSubscriptions } from './chargeSubscriptions'; -export { handleFailedSubscriptionPayment } from './handleFailedSubscriptionPayment'; -export { cancelSubscription } from './cancelSubscription'; -export { createSubscription } from './createSubscription'; -export { revokeMemberships } from './revokeMemberships'; -export { revokeMembership } from './revokeMembership'; +export {chargeSubscription, chargeSubscriptionById} from './chargeSubscription'; +export {handleSuccessfulSubscriptionPayment} from './handleSuccesfulPayment'; +export {chargeSubscriptions} from './chargeSubscriptions'; +export {handleFailedSubscriptionPayment} from './handleFailedSubscriptionPayment'; +export {cancelSubscription} from './cancelSubscription'; +export {createSubscription} from './createSubscription'; +export {revokeMemberships} from './revokeMemberships'; +export {revokeMembership} from './revokeMembership'; diff --git a/functions/src/subscriptions/business/revokeMembership.ts b/functions/src/subscriptions/business/revokeMembership.ts index c506e64b..df154c12 100644 --- a/functions/src/subscriptions/business/revokeMembership.ts +++ b/functions/src/subscriptions/business/revokeMembership.ts @@ -1,16 +1,17 @@ -import { ISubscriptionEntity } from '../types'; -import { EVENT_TYPES } from '../../event/event'; +import {ISubscriptionEntity} from '../types'; +import {EVENT_TYPES} from '../../event/event'; -import { updateSubscription } from '../database/updateSubscription'; -import { createEvent } from '../../util/db/eventDbService'; -import { commonDb } from '../../common/database'; -import { removeCommonMember } from '../../common/business/removeCommonMember'; +import {updateSubscription} from '../database/updateSubscription'; +import {createEvent} from '../../util/db/eventDbService'; +import {commonDb} from '../../common/database'; +import {removeCommonMember} from '../../common/business/removeCommonMember'; -export const revokeMembership = async (subscription: ISubscriptionEntity): Promise => { +export const revokeMembership = async ( + subscription: ISubscriptionEntity, +): Promise => { const common = await commonDb.get(subscription.metadata.common.id); - - await removeCommonMember(common, subscription.userId) + await removeCommonMember(common, subscription.userId); subscription.revoked = true; @@ -19,6 +20,6 @@ export const revokeMembership = async (subscription: ISubscriptionEntity): Promi await createEvent({ userId: subscription.userId, objectId: subscription.id, - type: EVENT_TYPES.MEMBERSHIP_REVOKED + type: EVENT_TYPES.MEMBERSHIP_REVOKED, }); -} \ No newline at end of file +}; diff --git a/functions/src/subscriptions/business/revokeMemberships.ts b/functions/src/subscriptions/business/revokeMemberships.ts index 2a69e8de..9a365155 100644 --- a/functions/src/subscriptions/business/revokeMemberships.ts +++ b/functions/src/subscriptions/business/revokeMemberships.ts @@ -1,11 +1,11 @@ import admin from 'firebase-admin'; -import { QuerySnapshot } from '@google-cloud/firestore'; +import {QuerySnapshot} from '@google-cloud/firestore'; -import { ISubscriptionEntity } from '../types'; -import { Collections } from '../../util/constants'; +import {ISubscriptionEntity} from '../types'; +import {Collections} from '../../util/constants'; -import { CancellationReason } from './cancelSubscription'; -import { revokeMembership } from './revokeMembership'; +import {CancellationReason} from './cancelSubscription'; +import {revokeMembership} from './revokeMembership'; const db = admin.firestore(); @@ -17,43 +17,54 @@ export const revokeMemberships = async (): Promise => { // Only get the subscription cancelled by user, because the subscriptions // canceled by payment failure should already be revoked - const subscriptions = await db.collection(Collections.Subscriptions) + const subscriptions = (await db + .collection(Collections.Subscriptions) .where('dueDate', '<=', new Date().setHours(23, 59, 59, 999)) .where('status', '==', CancellationReason.CanceledByUser) .where('revoked', '==', false) - .get() as QuerySnapshot; + .get()) as QuerySnapshot; const promiseArr: Promise[] = []; for (const subscriptionSnap of subscriptions.docs) { const subscription = subscriptionSnap.data() as ISubscriptionEntity; - if (subscription.status === 'Active' || subscription.status === 'PaymentFailed') { + if ( + subscription.status === 'Active' || + subscription.status === 'PaymentFailed' + ) { logger.warn( ` Trying to revoke subscription with status (${subscription.status}) from the cron - ` + `, ); } else { // eslint-disable-next-line no-loop-func - promiseArr.push((async () => { - // Add try/catch so that if one revoke fails - // the others won't be canceled because of it - try { - logger.info(`Revoking membership for subscription with id ${subscription.id}`); - - await revokeMembership(subscription); - - logger.info(`Revoked membership ${subscription.id}`); - } catch (e) { - logger.warn('Error occurred while trying to revoke subscription', e); - } - })()); + promiseArr.push( + (async () => { + // Add try/catch so that if one revoke fails + // the others won't be canceled because of it + try { + logger.info( + `Revoking membership for subscription with id ${subscription.id}`, + ); + + await revokeMembership(subscription); + + logger.info(`Revoked membership ${subscription.id}`); + } catch (e) { + logger.warn( + 'Error occurred while trying to revoke subscription', + e, + ); + } + })(), + ); } } await Promise.all(promiseArr); logger.info(`Memberships revoked successfully`); -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/cron/dailySubscriptionCron.ts b/functions/src/subscriptions/cron/dailySubscriptionCron.ts index 634f1863..64c220c8 100644 --- a/functions/src/subscriptions/cron/dailySubscriptionCron.ts +++ b/functions/src/subscriptions/cron/dailySubscriptionCron.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { chargeSubscriptions, revokeMemberships } from '../business'; +import {chargeSubscriptions, revokeMemberships} from '../business'; /** * Runs the daily crons job, responsible for charging the subscriptions @@ -9,8 +9,5 @@ exports.backup = functions.pubsub .schedule('17 5 * * *') // => every day at 05:17 AM .onRun(async () => { // Execute the subscription charging and revoking asynchronously - await Promise.all([ - chargeSubscriptions(), - revokeMemberships() - ]); - }); \ No newline at end of file + await Promise.all([chargeSubscriptions(), revokeMemberships()]); + }); diff --git a/functions/src/subscriptions/database/addSubscription.ts b/functions/src/subscriptions/database/addSubscription.ts index c13ab03e..160c3b98 100644 --- a/functions/src/subscriptions/database/addSubscription.ts +++ b/functions/src/subscriptions/database/addSubscription.ts @@ -1,12 +1,11 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; -import { BaseEntityType, SharedOmit } from '../../util/types'; - -import { ISubscriptionEntity } from '../types'; -import { SubscriptionsCollection } from './index'; +import {BaseEntityType, SharedOmit} from '../../util/types'; +import {ISubscriptionEntity} from '../types'; +import {SubscriptionsCollection} from './index'; /** * Prepares the passed subscription for saving and saves it. Please note that @@ -14,7 +13,9 @@ import { SubscriptionsCollection } from './index'; * * @param subscription - the subscription to be saves */ -export const addSubscription = async (subscription: SharedOmit): Promise => { +export const addSubscription = async ( + subscription: SharedOmit, +): Promise => { const subscriptionDoc: ISubscriptionEntity = { id: v4(), @@ -23,16 +24,14 @@ export const addSubscription = async (subscription: SharedOmit): Promise => { +export const deleteSubscription = async ( + subscriptionId: Nullable, +): Promise => { if (!subscriptionId) { throw new CommonError('Cannot get subscription without providing the id!'); } logger.warn(`Deleting subscription with id ${subscriptionId}`); - return (await db.collection(Collections.Subscriptions) + return await db + .collection(Collections.Subscriptions) .doc(subscriptionId) - .delete()); -}; \ No newline at end of file + .delete(); +}; diff --git a/functions/src/subscriptions/database/getSubscription.ts b/functions/src/subscriptions/database/getSubscription.ts index 30624ece..c216d6b4 100644 --- a/functions/src/subscriptions/database/getSubscription.ts +++ b/functions/src/subscriptions/database/getSubscription.ts @@ -1,10 +1,10 @@ import admin from 'firebase-admin'; -import { Nullable } from '../../util/types'; -import { Collections } from '../../util/constants'; -import { CommonError, NotFoundError } from '../../util/errors'; +import {Nullable} from '../../util/types'; +import {Collections} from '../../util/constants'; +import {CommonError, NotFoundError} from '../../util/errors'; -import { ISubscriptionEntity } from '../types'; +import {ISubscriptionEntity} from '../types'; const db = admin.firestore(); @@ -18,18 +18,21 @@ const db = admin.firestore(); * * @returns { ISubscriptionEntity } - The found subscription */ -export const getSubscription = async (subscriptionId: Nullable, throwErr = true): Promise => { +export const getSubscription = async ( + subscriptionId: Nullable, + throwErr = true, +): Promise => { if (!subscriptionId) { throw new CommonError('Cannot get subscription without providing the id!'); } - const subscription = (await db.collection(Collections.Subscriptions) - .doc(subscriptionId) - .get()).data() as Nullable; + const subscription = ( + await db.collection(Collections.Subscriptions).doc(subscriptionId).get() + ).data() as Nullable; if (!subscription && throwErr) { throw new NotFoundError(subscriptionId, 'subscription'); } return subscription; -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/database/getSubscriptions.ts b/functions/src/subscriptions/database/getSubscriptions.ts index be1663cc..7bf10613 100644 --- a/functions/src/subscriptions/database/getSubscriptions.ts +++ b/functions/src/subscriptions/database/getSubscriptions.ts @@ -23,4 +23,3 @@ // return (await subscriptionsQuery.get()).docs // .map(bankAccount => bankAccount.data()); // }; - diff --git a/functions/src/subscriptions/database/index.ts b/functions/src/subscriptions/database/index.ts index 8d30511c..dce1c018 100644 --- a/functions/src/subscriptions/database/index.ts +++ b/functions/src/subscriptions/database/index.ts @@ -1,10 +1,10 @@ -import { getSubscription } from './getSubscription'; -import { updateSubscription } from './updateSubscription'; -import { db } from '../../util'; -import { Collections } from '../../constants'; -import { addSubscription } from './addSubscription'; -import { subscriptionExists } from './subscriptionExists'; -import { deleteSubscription } from './deleteSubscription'; +import {getSubscription} from './getSubscription'; +import {updateSubscription} from './updateSubscription'; +import {db} from '../../util'; +import {Collections} from '../../constants'; +import {addSubscription} from './addSubscription'; +import {subscriptionExists} from './subscriptionExists'; +import {deleteSubscription} from './deleteSubscription'; export const SubscriptionsCollection = db.collection(Collections.Subscriptions); @@ -13,5 +13,5 @@ export const subscriptionDb = { get: getSubscription, update: updateSubscription, exists: subscriptionExists, - delete: deleteSubscription -}; \ No newline at end of file + delete: deleteSubscription, +}; diff --git a/functions/src/subscriptions/database/subscriptionExists.ts b/functions/src/subscriptions/database/subscriptionExists.ts index 9c6b5969..487380a8 100644 --- a/functions/src/subscriptions/database/subscriptionExists.ts +++ b/functions/src/subscriptions/database/subscriptionExists.ts @@ -1,7 +1,6 @@ import admin from 'firebase-admin'; -import { ISubscriptionEntity } from '../types'; -import { SubscriptionsCollection } from './index'; - +import {ISubscriptionEntity} from '../types'; +import {SubscriptionsCollection} from './index'; import DocumentSnapshot = admin.firestore.DocumentSnapshot; @@ -16,15 +15,23 @@ interface ISubscriptionExistsArgs { * * @param args - Arguments against we will check */ -export const subscriptionExists = async (args: ISubscriptionExistsArgs): Promise => { +export const subscriptionExists = async ( + args: ISubscriptionExistsArgs, +): Promise => { let subscription: DocumentSnapshot; if (args.id) { - subscription = (await SubscriptionsCollection.doc(args.id).get()) as DocumentSnapshot; + subscription = (await SubscriptionsCollection.doc( + args.id, + ).get()) as DocumentSnapshot; } if (args.proposalId) { - const where = await SubscriptionsCollection.where('proposalId', '==', args.proposalId).get(); + const where = await SubscriptionsCollection.where( + 'proposalId', + '==', + args.proposalId, + ).get(); if (where.empty) { return false; @@ -34,4 +41,4 @@ export const subscriptionExists = async (args: ISubscriptionExistsArgs): Promise } return subscription ? subscription.exists : false; -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/database/updateSubscription.ts b/functions/src/subscriptions/database/updateSubscription.ts index 9f3d4b69..05f5faba 100644 --- a/functions/src/subscriptions/database/updateSubscription.ts +++ b/functions/src/subscriptions/database/updateSubscription.ts @@ -1,10 +1,10 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { Collections } from '../../util/constants'; -import { ISubscriptionEntity } from '../types'; +import {Collections} from '../../util/constants'; +import {ISubscriptionEntity} from '../types'; -const db = admin.firestore() +const db = admin.firestore(); /** * Updates subscription in the firestore using .update() @@ -12,10 +12,14 @@ const db = admin.firestore() * @param subscription - The updated subscription * @param subscriptionId - **Optional** - The id of the subscription. If not provided the id from the subscription object will be used */ -export const updateSubscription = async (subscription: ISubscriptionEntity, subscriptionId?: string): Promise => { - subscription.updatedAt = Timestamp.now() +export const updateSubscription = async ( + subscription: ISubscriptionEntity, + subscriptionId?: string, +): Promise => { + subscription.updatedAt = Timestamp.now(); - await db.collection(Collections.Subscriptions) + await db + .collection(Collections.Subscriptions) .doc(subscriptionId || subscription.id) .update(subscription); -}; \ No newline at end of file +}; diff --git a/functions/src/subscriptions/index.ts b/functions/src/subscriptions/index.ts index a26ae725..d4b78514 100644 --- a/functions/src/subscriptions/index.ts +++ b/functions/src/subscriptions/index.ts @@ -1,40 +1,45 @@ import * as functions from 'firebase-functions'; -import { commonApp, commonRouter } from '../util'; -import { responseExecutor } from '../util/responseExecutor'; -import { runtimeOptions } from '../util/constants'; -import { CommonError } from '../util/errors'; -import { cancelSubscription } from './business'; -import { CancellationReason } from './business/cancelSubscription'; -import { subscriptionDb } from './database'; +import {commonApp, commonRouter} from '../util'; +import {responseExecutor} from '../util/responseExecutor'; +import {runtimeOptions} from '../util/constants'; +import {CommonError} from '../util/errors'; +import {cancelSubscription} from './business'; +import {CancellationReason} from './business/cancelSubscription'; +import {subscriptionDb} from './database'; const router = commonRouter(); router.post('/cancel', async (req, res, next) => { - await responseExecutor(async () => { - const {subscriptionId} = req.query; + await responseExecutor( + async () => { + const {subscriptionId} = req.query; - if (!subscriptionId) { - throw new CommonError('The subscription id is required, but not provided!'); - } + if (!subscriptionId) { + throw new CommonError( + 'The subscription id is required, but not provided!', + ); + } - const subscription = await subscriptionDb.get(subscriptionId as string); + const subscription = await subscriptionDb.get(subscriptionId as string); - if (subscription.userId !== req.user.uid) { - throw new CommonError(` + if (subscription.userId !== req.user.uid) { + throw new CommonError(` Cannot cancel subscription that is not yours `); - } - - await cancelSubscription(subscription, CancellationReason.CanceledByUser); - }, { - req, - res, - next, - successMessage: `Subscription successfully canceled` - }); + } + + await cancelSubscription(subscription, CancellationReason.CanceledByUser); + }, + { + req, + res, + next, + successMessage: `Subscription successfully canceled`, + }, + ); }); export const subscriptionsApp = functions .runWith(runtimeOptions) - .https.onRequest(commonApp(router)); \ No newline at end of file + .https.onRequest(commonApp(router)); diff --git a/functions/src/subscriptions/types.ts b/functions/src/subscriptions/types.ts index 09d87e71..ee9d789b 100644 --- a/functions/src/subscriptions/types.ts +++ b/functions/src/subscriptions/types.ts @@ -1,7 +1,7 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { CirclePaymentStatus, IBaseEntity } from '../util/types'; +import {CirclePaymentStatus, IBaseEntity} from '../util/types'; export interface ISubscriptionEntity extends IBaseEntity { /** @@ -93,7 +93,7 @@ export interface ISubscriptionMetadata { common: { name: string; id: string; - } + }; } /** @@ -108,4 +108,9 @@ export interface ISubscriptionMetadata { * CanceledByUser - The subscription is not active, because the user has canceled it. The membership may * still be active, but it will be revoked on the next due date */ -export type SubscriptionStatus = 'Pending' | 'Active' | 'CanceledByUser' | 'CanceledByPaymentFailure' | 'PaymentFailed'; \ No newline at end of file +export type SubscriptionStatus = + | 'Pending' + | 'Active' + | 'CanceledByUser' + | 'CanceledByPaymentFailure' + | 'PaymentFailed'; diff --git a/functions/src/users/database/getUser.ts b/functions/src/users/database/getUser.ts index dae8da75..7ea072cf 100644 --- a/functions/src/users/database/getUser.ts +++ b/functions/src/users/database/getUser.ts @@ -1,7 +1,7 @@ -import { ArgumentError, NotFoundError } from '../../util/errors'; +import {ArgumentError, NotFoundError} from '../../util/errors'; -import { IUserEntity } from '../types'; -import { UserCollection } from './index'; +import {IUserEntity} from '../types'; +import {UserCollection} from './index'; /** * Tries to find the user by provided user ID @@ -16,14 +16,11 @@ export const getUser = async (userId: string): Promise => { throw new ArgumentError('userId', userId); } - const user = await UserCollection - .doc(userId) - .get(); + const user = await UserCollection.doc(userId).get(); if (!user.exists) { throw new NotFoundError('user.userId', userId); } - return user.data(); -}; \ No newline at end of file +}; diff --git a/functions/src/users/database/getUserByEmail.ts b/functions/src/users/database/getUserByEmail.ts index 0817f5d4..0e30c4c6 100644 --- a/functions/src/users/database/getUserByEmail.ts +++ b/functions/src/users/database/getUserByEmail.ts @@ -1,6 +1,6 @@ -import { IUserEntity } from '../types'; -import { ArgumentError, CommonError, NotFoundError } from '../../util/errors'; -import { UserCollection } from './index'; +import {IUserEntity} from '../types'; +import {ArgumentError, CommonError, NotFoundError} from '../../util/errors'; +import {UserCollection} from './index'; /** * Tries to find the user by provided email address @@ -16,19 +16,20 @@ export const getUserByEmail = async (email: string): Promise => { throw new ArgumentError('email', email); } - const user = await UserCollection - .where('email', '==', email) - .get(); + const user = await UserCollection.where('email', '==', email).get(); if (!user.docs.length) { throw new NotFoundError('user.email', email); } if (user.docs.length > 1) { - throw new CommonError(`There are more than one user with the email ${email}`, { - users: user.docs.map(u => u.data()) - }); + throw new CommonError( + `There are more than one user with the email ${email}`, + { + users: user.docs.map((u) => u.data()), + }, + ); } return user.docs[0].data(); -}; \ No newline at end of file +}; diff --git a/functions/src/users/types.ts b/functions/src/users/types.ts index 174b9a1e..83b748ed 100644 --- a/functions/src/users/types.ts +++ b/functions/src/users/types.ts @@ -1,12 +1,12 @@ -import { IBaseEntity } from '../util/types'; +import {IBaseEntity} from '../util/types'; export interface IUserEntity extends IBaseEntity { - uid: string; + uid: string; - email: string; - photoURL: string; + email: string; + photoURL: string; - firstName: string; - lastName: string; - displayName: string; -} \ No newline at end of file + firstName: string; + lastName: string; + displayName: string; +} diff --git a/functions/src/util/addMonth.ts b/functions/src/util/addMonth.ts index cfe4bf2b..190acefb 100644 --- a/functions/src/util/addMonth.ts +++ b/functions/src/util/addMonth.ts @@ -4,9 +4,9 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; export const addMonth = (date: Date | Timestamp): Date => { - if(!(date instanceof Date)) { + if (!(date instanceof Date)) { date = date.toDate(); } return moment(date).add(1, 'months').toDate(); -}; \ No newline at end of file +}; diff --git a/functions/src/util/backup.js b/functions/src/util/backup.js index c2bda04f..ace3e55b 100644 --- a/functions/src/util/backup.js +++ b/functions/src/util/backup.js @@ -3,7 +3,7 @@ const dateformat = require('dateformat'); const client = new firestore.v1.FirestoreAdminClient(); -const { CommonError } = require('./errors'); +const {CommonError} = require('./errors'); /** * Util for creating a firestore backup for the current @@ -17,18 +17,21 @@ exports.backup = () => { const timestamp = dateformat(Date.now(), 'isoDateTime'); - const bucket = projectId === 'common-staging-50741' - ? `gs://common-staging-50741.appspot.com/backup/${timestamp}` - : projectId === 'common-daostack' - && `gs://common-daostack.appspot.com/backup/${timestamp}`; + const bucket = + projectId === 'common-staging-50741' + ? `gs://common-staging-50741.appspot.com/backup/${timestamp}` + : projectId === 'common-daostack' && + `gs://common-daostack.appspot.com/backup/${timestamp}`; if (!bucket) { - throw new CommonError('EnvironmentError: cannot find the current GCloud project!'); + throw new CommonError( + 'EnvironmentError: cannot find the current GCloud project!', + ); } return client.exportDocuments({ name: databaseName, outputUriPrefix: bucket, - collectionIds: [] + collectionIds: [], }); -}; \ No newline at end of file +}; diff --git a/functions/src/util/commonApp.ts b/functions/src/util/commonApp.ts index 15adfc68..5caf2a62 100644 --- a/functions/src/util/commonApp.ts +++ b/functions/src/util/commonApp.ts @@ -7,40 +7,51 @@ import { authenticate, errorHandling, routeBasedMiddleware, - requestLoggingMiddleware + requestLoggingMiddleware, } from './middleware'; export const commonRouter = express.Router; interface ICommonAppOptions { - unauthenticatedRoutes: string[] + unauthenticatedRoutes: string[]; } -export const commonApp = (router: express.Router, options?: ICommonAppOptions): express.Application => { +export const commonApp = ( + router: express.Router, + options?: ICommonAppOptions, +): express.Application => { const app = express(); app.use(sessions); app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ - extended: true - })); + app.use( + bodyParser.urlencoded({ + extended: true, + }), + ); app.use(express.json()); - app.use(express.urlencoded({ - extended: true - })); + app.use( + express.urlencoded({ + extended: true, + }), + ); app.use(requestLoggingMiddleware); - app.use(cors({ - origin: true - })); + app.use( + cors({ + origin: true, + }), + ); - app.use(routeBasedMiddleware(authenticate, { - exclude: options?.unauthenticatedRoutes || [] - })); + app.use( + routeBasedMiddleware(authenticate, { + exclude: options?.unauthenticatedRoutes || [], + }), + ); app.use(router); @@ -50,7 +61,7 @@ export const commonApp = (router: express.Router, options?: ICommonAppOptions): message: 'OK', healthy: true, uptime: process.uptime(), - timestamp: Date.now() + timestamp: Date.now(), }; try { @@ -66,4 +77,4 @@ export const commonApp = (router: express.Router, options?: ICommonAppOptions): app.use(errorHandling); return app; -}; \ No newline at end of file +}; diff --git a/functions/src/util/constants.ts b/functions/src/util/constants.ts index 2e172c84..e1d27df0 100644 --- a/functions/src/util/constants.ts +++ b/functions/src/util/constants.ts @@ -3,7 +3,7 @@ export const StatusCodes = { NotFound: 404, - Ok: 200 + Ok: 200, }; export const ErrorCodes = { @@ -13,7 +13,7 @@ export const ErrorCodes = { UncaughtError: 'UncaughtError', // ---- External providers errors - CirclePayError: 'External.CirclePayError' + CirclePayError: 'External.CirclePayError', }; export const Collections = { @@ -22,9 +22,9 @@ export const Collections = { Payments: 'payments', Commons: 'daos', Event: 'event', - Cards: 'cards' + Cards: 'cards', }; export const runtimeOptions = { - timeoutSeconds: 540 -}; \ No newline at end of file + timeoutSeconds: 540, +}; diff --git a/functions/src/util/createErrorResponse.ts b/functions/src/util/createErrorResponse.ts index f2ec9dd3..6d0a5b74 100644 --- a/functions/src/util/createErrorResponse.ts +++ b/functions/src/util/createErrorResponse.ts @@ -1,7 +1,7 @@ import express from 'express'; -import { ICommonError } from './errors/CommonError'; -import { StatusCodes } from '../constants'; +import {ICommonError} from './errors/CommonError'; +import {StatusCodes} from '../constants'; export interface IErrorResponse { error: string; @@ -13,7 +13,11 @@ export interface IErrorResponse { data?: any; } -export const createErrorResponse = (req: express.Request, res: express.Response, error: ICommonError): void => { +export const createErrorResponse = ( + req: express.Request, + res: express.Response, + error: ICommonError, +): void => { logger.info(`Error occurred at ${req.path}`); // Here `error instanceof CommonError` does not work for some @@ -30,14 +34,12 @@ export const createErrorResponse = (req: express.Request, res: express.Response, id: req.requestId, body: req.body, query: req.query, - headers: req.headers + headers: req.headers, }, - data: error.data + data: error.data, }; - const statusCode = - error.statusCode || - StatusCodes.InternalServerError; + const statusCode = error.statusCode || StatusCodes.InternalServerError; logger.info(` Creating error response with message '${error.message}' @@ -46,19 +48,19 @@ export const createErrorResponse = (req: express.Request, res: express.Response, `); logger.error('Error occurred', { - error + error, }); - res - .status(statusCode) - .json(errorResponse); + res.status(statusCode).json(errorResponse); } else { - logger.warn(` + logger.warn( + ` The error passed to createErrorResponse was not of CommonError type. This should never happen! - `, { - payload: error - } + `, + { + payload: error, + }, ); res diff --git a/functions/src/util/db/discussionMessagesDb.ts b/functions/src/util/db/discussionMessagesDb.ts index bf0da880..bbc4303c 100644 --- a/functions/src/util/db/discussionMessagesDb.ts +++ b/functions/src/util/db/discussionMessagesDb.ts @@ -1,12 +1,12 @@ -import { db } from '../../settings'; +import {db} from '../../settings'; const COLLECTION_NAME = 'discussionMessage'; -export const getDiscussionMessageById = async (discussionMessageId: string): Promise => { - return await db.collection(COLLECTION_NAME) - .doc(discussionMessageId) - .get(); -} +export const getDiscussionMessageById = async ( + discussionMessageId: string, +): Promise => { + return await db.collection(COLLECTION_NAME).doc(discussionMessageId).get(); +}; export default { - getDiscussionMessageById -}; \ No newline at end of file + getDiscussionMessageById, +}; diff --git a/functions/src/util/db/eventDbService.ts b/functions/src/util/db/eventDbService.ts index d793000d..caac7bfb 100644 --- a/functions/src/util/db/eventDbService.ts +++ b/functions/src/util/db/eventDbService.ts @@ -1,25 +1,25 @@ -import { v4 } from 'uuid'; -import { firestore } from 'firebase-admin'; +import {v4} from 'uuid'; +import {firestore} from 'firebase-admin'; -import { db } from '../index'; -import { Collections } from '../../constants'; -import { IEventEntity } from '../../event/type'; -import { BaseEntityType } from '../types'; +import {db} from '../index'; +import {Collections} from '../../constants'; +import {IEventEntity} from '../../event/type'; +import {BaseEntityType} from '../types'; export const eventCollection = db.collection(Collections.Event); -export const createEvent = async (doc: Omit): Promise => { +export const createEvent = async ( + doc: Omit, +): Promise => { const eventDoc: IEventEntity = { ...doc, id: v4(), createdAt: firestore.Timestamp.fromDate(new Date()), - updatedAt: firestore.Timestamp.fromDate(new Date()) + updatedAt: firestore.Timestamp.fromDate(new Date()), }; - await eventCollection - .doc(eventDoc.id) - .set(eventDoc); + await eventCollection.doc(eventDoc.id).set(eventDoc); return eventDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/util/db/userDbService.js b/functions/src/util/db/userDbService.js index 45e88790..c5e3f792 100644 --- a/functions/src/util/db/userDbService.js +++ b/functions/src/util/db/userDbService.js @@ -1,48 +1,41 @@ // @ts-ignore -const { db } = require('../../settings'); +const {db} = require('../../settings'); const COLLECTION_NAME = 'users'; async function getUserById(userId) { - return await db.collection(COLLECTION_NAME) - .doc(userId) - .get(); + return await db.collection(COLLECTION_NAME).doc(userId).get(); } async function findUserByAddress(ethereumAddress, key = 'safeAddress') { - const query = db.collection(COLLECTION_NAME) - .where(key, `==`, ethereumAddress) + const query = db + .collection(COLLECTION_NAME) + .where(key, `==`, ethereumAddress); - const snapshot = await query.get() - if (snapshot.size === 0) { - // eslint-disable-next-line no-console - console.error(`No member found with ${key} === ${ethereumAddress}`) - return null - } else { - const member = snapshot.docs[0] - return member - } + const snapshot = await query.get(); + if (snapshot.size === 0) { + // eslint-disable-next-line no-console + console.error(`No member found with ${key} === ${ethereumAddress}`); + return null; + } else { + const member = snapshot.docs[0]; + return member; + } } async function updateUser(userId, doc) { - - return await db.collection(COLLECTION_NAME) - .doc(userId). - set( - doc, - { - merge: true - } - ); + return await db.collection(COLLECTION_NAME).doc(userId).set(doc, { + merge: true, + }); } async function getAllUsers() { - const snapshot = await db.collection(COLLECTION_NAME).get(); - return snapshot.docs.map(doc => doc.data()); + const snapshot = await db.collection(COLLECTION_NAME).get(); + return snapshot.docs.map((doc) => doc.data()); } module.exports = { - findUserByAddress, - getUserById, - updateUser, - getAllUsers + findUserByAddress, + getUserById, + updateUser, + getAllUsers, }; diff --git a/functions/src/util/deleted/database/addDeletedEntity.ts b/functions/src/util/deleted/database/addDeletedEntity.ts index 93535e79..0cf7b7b4 100644 --- a/functions/src/util/deleted/database/addDeletedEntity.ts +++ b/functions/src/util/deleted/database/addDeletedEntity.ts @@ -1,8 +1,8 @@ import admin from 'firebase-admin'; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; -import { IDeletedEntity } from '../types'; -import { DeletionsCollection } from './index'; +import {IDeletedEntity} from '../types'; +import {DeletionsCollection} from './index'; import Timestamp = admin.firestore.Timestamp; @@ -13,7 +13,10 @@ import Timestamp = admin.firestore.Timestamp; * @param entity - The deleted entity * @param deletionId - ID, that can be used to track batch deletions */ -export const addDeletedEntity = async (entity: T, deletionId: string): Promise> => { +export const addDeletedEntity = async ( + entity: T, + deletionId: string, +): Promise> => { // Created formatted representation of the passed data const deletedDoc: IDeletedEntity = { id: v4(), @@ -22,14 +25,12 @@ export const addDeletedEntity = async (entity: T, deletionId: string): Promis updatedAt: Timestamp.now(), deletionId, - entity + entity, }; // Save the deleted doc - await DeletionsCollection - .doc(deletedDoc.id) - .set(deletedDoc); + await DeletionsCollection.doc(deletedDoc.id).set(deletedDoc); // Return the saved deleted doc return deletedDoc; -}; \ No newline at end of file +}; diff --git a/functions/src/util/deleted/database/index.ts b/functions/src/util/deleted/database/index.ts index 18807c4f..64406095 100644 --- a/functions/src/util/deleted/database/index.ts +++ b/functions/src/util/deleted/database/index.ts @@ -1,20 +1,25 @@ -import { db } from '../../index'; -import { Collections } from '../../../constants'; +import {db} from '../../index'; +import {Collections} from '../../../constants'; -import { addDeletedEntity } from './addDeletedEntity'; -import { IDeletedEntity } from '../types'; +import {addDeletedEntity} from './addDeletedEntity'; +import {IDeletedEntity} from '../types'; -export const DeletionsCollection = db.collection(Collections.Deleted) +export const DeletionsCollection = db + .collection(Collections.Deleted) .withConverter({ - fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): IDeletedEntity { + fromFirestore( + snapshot: FirebaseFirestore.QueryDocumentSnapshot, + ): IDeletedEntity { return snapshot.data() as IDeletedEntity; }, - toFirestore(object: IDeletedEntity | Partial): FirebaseFirestore.DocumentData { + toFirestore( + object: IDeletedEntity | Partial, + ): FirebaseFirestore.DocumentData { return object; - } + }, }); export const deletedDb = { - add: addDeletedEntity -}; \ No newline at end of file + add: addDeletedEntity, +}; diff --git a/functions/src/util/deleted/types.ts b/functions/src/util/deleted/types.ts index d64008cd..12baa554 100644 --- a/functions/src/util/deleted/types.ts +++ b/functions/src/util/deleted/types.ts @@ -1,4 +1,4 @@ -import { IBaseEntity } from '../types'; +import {IBaseEntity} from '../types'; export interface IDeletedEntity extends IBaseEntity { deletionId: string; diff --git a/functions/src/util/environment.ts b/functions/src/util/environment.ts index 716ad463..df72a0f2 100644 --- a/functions/src/util/environment.ts +++ b/functions/src/util/environment.ts @@ -1 +1 @@ -export const isTest = process.env.NODE_ENV === 'test'; \ No newline at end of file +export const isTest = process.env.NODE_ENV === 'test'; diff --git a/functions/src/util/errors/ArgumentError.ts b/functions/src/util/errors/ArgumentError.ts index 7850d457..0b34ea1a 100644 --- a/functions/src/util/errors/ArgumentError.ts +++ b/functions/src/util/errors/ArgumentError.ts @@ -1,5 +1,5 @@ -import { CommonError } from './CommonError'; -import { ErrorCodes, StatusCodes } from '../../constants'; +import {CommonError} from './CommonError'; +import {ErrorCodes, StatusCodes} from '../../constants'; /** * The exception that is thrown when one of the arguments provided to a @@ -14,10 +14,9 @@ export class ArgumentError extends CommonError { argumentValue, statusCode: StatusCodes.InternalServerError, - errorCode: - argumentValue - ? ErrorCodes.ArgumentError - : ErrorCodes.ArgumentNullError + errorCode: argumentValue + ? ErrorCodes.ArgumentError + : ErrorCodes.ArgumentNullError, }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/CommonError.ts b/functions/src/util/errors/CommonError.ts index dc57aed1..475187e1 100644 --- a/functions/src/util/errors/CommonError.ts +++ b/functions/src/util/errors/CommonError.ts @@ -1,6 +1,6 @@ -import { v4 as uuidv4 } from 'uuid'; +import {v4 as uuidv4} from 'uuid'; -import { ErrorCodes, StatusCodes } from '../../constants'; +import {ErrorCodes, StatusCodes} from '../../constants'; interface IErrorData { userMessage?: string; @@ -39,17 +39,14 @@ export class CommonError extends Error implements ICommonError { * @param message - the error message (required) * @param data - more data, related to the error (optional) */ - constructor( - message: string, - data: IErrorData = {} - ) { + constructor(message: string, data: IErrorData = {}) { const errorId = uuidv4(); super(`${message} (${errorId})`); Error.captureStackTrace(this, this.constructor); this.errorId = errorId; - this.name = "Common Error"; + this.name = 'Common Error'; this.errorCode = data.errorCode || ErrorCodes.GenericError; this.statusCode = data.statusCode || StatusCodes.InternalServerError; diff --git a/functions/src/util/errors/CvvVerificationError.ts b/functions/src/util/errors/CvvVerificationError.ts index 7750e9c4..a2e1a0bd 100644 --- a/functions/src/util/errors/CvvVerificationError.ts +++ b/functions/src/util/errors/CvvVerificationError.ts @@ -1,5 +1,5 @@ -import { CommonError } from './CommonError'; -import { ErrorCodes, StatusCodes } from '../../constants'; +import {CommonError} from './CommonError'; +import {ErrorCodes, StatusCodes} from '../../constants'; /** * The exception that is thrown when something is wrong with the card like @@ -12,7 +12,7 @@ export class CvvVerificationError extends CommonError { cardId, statusCode: StatusCodes.InternalServerError, - errorCode: ErrorCodes.CvvVerificationFail + errorCode: ErrorCodes.CvvVerificationFail, }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/NotFoundError.ts b/functions/src/util/errors/NotFoundError.ts index a2a17bcf..85e28f1e 100644 --- a/functions/src/util/errors/NotFoundError.ts +++ b/functions/src/util/errors/NotFoundError.ts @@ -1,5 +1,5 @@ -import { CommonError } from './CommonError'; -import { ErrorCodes, StatusCodes } from '../../constants'; +import {CommonError} from './CommonError'; +import {ErrorCodes, StatusCodes} from '../../constants'; /** * The error that is thrown when the requested object @@ -15,12 +15,11 @@ export class NotFoundError extends CommonError { */ constructor(identifier: string, entity?: string) { super(`Cannot find ${entity || 'entity'} with identifier ${identifier}`, { - userMessage: - (entity) - ? `Ooops! We were not able to find ${entity} with id ${identifier}` - : 'We were unable to find the requested resource!', + userMessage: entity + ? `Ooops! We were not able to find ${entity} with id ${identifier}` + : 'We were unable to find the requested resource!', errorCode: ErrorCodes.NotFound, - statusCode: StatusCodes.NotFound + statusCode: StatusCodes.NotFound, }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/NotImplementedError.ts b/functions/src/util/errors/NotImplementedError.ts index 06f4c225..6e06709e 100644 --- a/functions/src/util/errors/NotImplementedError.ts +++ b/functions/src/util/errors/NotImplementedError.ts @@ -1,4 +1,4 @@ -import { CommonError } from './CommonError'; +import {CommonError} from './CommonError'; /** * The error that is thrown when a requested method @@ -8,4 +8,4 @@ export class NotImplementedError extends CommonError { constructor(message?: string) { super(message || 'Not implemented!'); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/PaymentError.ts b/functions/src/util/errors/PaymentError.ts index d315979d..f6623f94 100644 --- a/functions/src/util/errors/PaymentError.ts +++ b/functions/src/util/errors/PaymentError.ts @@ -1,4 +1,4 @@ -import { CommonError } from './CommonError'; +import {CommonError} from './CommonError'; /** * The exception that is thrown when something goes @@ -8,7 +8,7 @@ export class PaymentError extends CommonError { constructor(paymentId: string, circlePaymentId: string) { super('The payment failed', { paymentId, - circlePaymentId + circlePaymentId, }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/UnauthorizedError.ts b/functions/src/util/errors/UnauthorizedError.ts index 888ac272..29fc5ca2 100644 --- a/functions/src/util/errors/UnauthorizedError.ts +++ b/functions/src/util/errors/UnauthorizedError.ts @@ -1,7 +1,7 @@ import express from 'express'; -import { ErrorCodes, StatusCodes } from '../../constants'; -import { CommonError } from './CommonError'; +import {ErrorCodes, StatusCodes} from '../../constants'; +import {CommonError} from './CommonError'; /** * The error that is thrown when authentication or authorization @@ -14,12 +14,12 @@ export class UnauthorizedError extends CommonError { req: { headers: req?.headers, query: req?.query, - body: req?.body + body: req?.body, }, rest, statusCode: StatusCodes.Unauthorized, - errorCode: ErrorCodes.AuthenticationError + errorCode: ErrorCodes.AuthenticationError, }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/UnsupportedVerionError.ts b/functions/src/util/errors/UnsupportedVerionError.ts index e33b851f..733975de 100644 --- a/functions/src/util/errors/UnsupportedVerionError.ts +++ b/functions/src/util/errors/UnsupportedVerionError.ts @@ -1,6 +1,6 @@ -import { CommonError } from './CommonError'; +import {CommonError} from './CommonError'; -export const UnsupportedVersionErrorCode = "UnsupportedVersion"; +export const UnsupportedVersionErrorCode = 'UnsupportedVersion'; export class UnsupportedVersionError extends CommonError { constructor(errorMsg: string) { diff --git a/functions/src/util/errors/ValidationError.ts b/functions/src/util/errors/ValidationError.ts index 89211a8a..b02d4799 100644 --- a/functions/src/util/errors/ValidationError.ts +++ b/functions/src/util/errors/ValidationError.ts @@ -1,15 +1,14 @@ import * as yup from 'yup'; -import { CommonError } from './CommonError'; -import { ErrorCodes, StatusCodes } from '../../constants'; - +import {CommonError} from './CommonError'; +import {ErrorCodes, StatusCodes} from '../../constants'; const handleInner = (err: yup.ValidationError[], includeValues: boolean) => { - return err.map(err => ({ + return err.map((err) => ({ field: err.path, message: err.message, value: includeValues && err.value, - ...(!(err.inner) && handleInner(err.inner, includeValues)) + ...(!err.inner && handleInner(err.inner, includeValues)), })); }; @@ -20,12 +19,13 @@ const handleInner = (err: yup.ValidationError[], includeValues: boolean) => { export class ValidationError extends CommonError { constructor(validationError: yup.ValidationError, includeValues = true) { super('Validation failed', { - userMessage: 'The request cannot be processed, because the validation failed', + userMessage: + 'The request cannot be processed, because the validation failed', statusCode: StatusCodes.UnprocessableEntity, errorCode: ErrorCodes.ValidationError, errors: validationError.errors, - detailedErrors: handleInner(validationError.inner, includeValues) + detailedErrors: handleInner(validationError.inner, includeValues), }); } -} \ No newline at end of file +} diff --git a/functions/src/util/errors/index.ts b/functions/src/util/errors/index.ts index 76c7fb53..86e1fda7 100644 --- a/functions/src/util/errors/index.ts +++ b/functions/src/util/errors/index.ts @@ -1,9 +1,12 @@ -export { CommonError } from './CommonError'; -export { PaymentError } from './PaymentError'; -export { NotFoundError } from './NotFoundError'; -export { ArgumentError } from './ArgumentError'; -export { ValidationError } from './ValidationError'; -export { UnauthorizedError } from './UnauthorizedError'; -export { NotImplementedError } from './NotImplementedError'; -export { CvvVerificationError } from './CvvVerificationError'; -export { UnsupportedVersionError, UnsupportedVersionErrorCode } from './UnsupportedVerionError'; +export {CommonError} from './CommonError'; +export {PaymentError} from './PaymentError'; +export {NotFoundError} from './NotFoundError'; +export {ArgumentError} from './ArgumentError'; +export {ValidationError} from './ValidationError'; +export {UnauthorizedError} from './UnauthorizedError'; +export {NotImplementedError} from './NotImplementedError'; +export {CvvVerificationError} from './CvvVerificationError'; +export { + UnsupportedVersionError, + UnsupportedVersionErrorCode, +} from './UnsupportedVerionError'; diff --git a/functions/src/util/externalRequestExecutor.ts b/functions/src/util/externalRequestExecutor.ts index 79628bc9..c52487e3 100644 --- a/functions/src/util/externalRequestExecutor.ts +++ b/functions/src/util/externalRequestExecutor.ts @@ -1,5 +1,5 @@ -import { CommonError } from './errors'; -import { stringify } from 'flatted'; +import {CommonError} from './errors'; +import {stringify} from 'flatted'; interface IExternalErrorData { errorCode: string; @@ -10,21 +10,26 @@ interface IExternalErrorData { [key: string]: any; } -export const externalRequestExecutor = async (func: () => T | Promise, data: IExternalErrorData): Promise => { +export const externalRequestExecutor = async ( + func: () => T | Promise, + data: IExternalErrorData, +): Promise => { try { return await func(); } catch (err) { logger.warn('Circle error response: ', { data: err.response?.data, - error: err + error: err, }); throw new CommonError( - data.message || `External service failed. ErrorCode: ${data.errorCode}`, { - userMessage: 'Request to external service failed. Please try again later', + data.message || `External service failed. ErrorCode: ${data.errorCode}`, + { + userMessage: + 'Request to external service failed. Please try again later', data, - response: stringify(err.response?.data) - } + response: stringify(err.response?.data), + }, ); // @todo The request and response objects on the error are huge, so @@ -32,7 +37,3 @@ export const externalRequestExecutor = async (func: () => T | Promise): boolean => - obj === null || - obj === undefined; + obj === null || obj === undefined; // ---- Reexports -export { externalRequestExecutor } from './externalRequestExecutor'; -export { commonApp, commonRouter } from './commonApp'; -export { addMonth } from './addMonth'; -export { sleep } from './sleep'; -export { poll } from './poll'; +export {externalRequestExecutor} from './externalRequestExecutor'; +export {commonApp, commonRouter} from './commonApp'; +export {addMonth} from './addMonth'; +export {sleep} from './sleep'; +export {poll} from './poll'; diff --git a/functions/src/util/middleware/authenticateMiddleware.ts b/functions/src/util/middleware/authenticateMiddleware.ts index 76e96e4c..883d8902 100644 --- a/functions/src/util/middleware/authenticateMiddleware.ts +++ b/functions/src/util/middleware/authenticateMiddleware.ts @@ -1,17 +1,19 @@ -import { RequestHandler } from 'express'; -import { auth } from 'firebase-admin'; +import {RequestHandler} from 'express'; +import {auth} from 'firebase-admin'; -import { CommonError, UnauthorizedError } from '../errors'; -import { ErrorCodes, StatusCodes } from '../../constants'; +import {CommonError, UnauthorizedError} from '../errors'; +import {ErrorCodes, StatusCodes} from '../../constants'; export const authenticate: RequestHandler = async (req, res, next) => { try { - if (!req.headers.authorization || typeof req.headers.authorization !== 'string') { + if ( + !req.headers.authorization || + typeof req.headers.authorization !== 'string' + ) { throw new UnauthorizedError(); } try { - // Use firebase-admin auth to verify the token passed in from the client header. // Decoding this token returns the userPayload and all the other token // claims you added while creating the custom token and adds them @@ -22,15 +24,19 @@ export const authenticate: RequestHandler = async (req, res, next) => { return next(); } catch (error) { if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'dev') { - throw new CommonError('An error occurred while authenticating the user', { - userMessage: 'An error occurred during the authentication. Please log out and sign in again!', - - errorCode: ErrorCodes.AuthenticationError, - statusCode: StatusCodes.Unauthorized, - - error, - errorString: JSON.stringify(error) - }); + throw new CommonError( + 'An error occurred while authenticating the user', + { + userMessage: + 'An error occurred during the authentication. Please log out and sign in again!', + + errorCode: ErrorCodes.AuthenticationError, + statusCode: StatusCodes.Unauthorized, + + error, + errorString: JSON.stringify(error), + }, + ); } else { // Here we should only be on test environment logger.warn(`Testing authorization is being used! ${req.requestId}`); diff --git a/functions/src/util/middleware/errorHandlingMiddleware.ts b/functions/src/util/middleware/errorHandlingMiddleware.ts index 1091b7a4..34233102 100644 --- a/functions/src/util/middleware/errorHandlingMiddleware.ts +++ b/functions/src/util/middleware/errorHandlingMiddleware.ts @@ -1,22 +1,31 @@ import express from 'express'; -import { CommonError } from '../errors'; -import { createErrorResponse } from '../createErrorResponse'; -import { ICommonError } from '../errors/CommonError'; +import {CommonError} from '../errors'; +import {createErrorResponse} from '../createErrorResponse'; +import {ICommonError} from '../errors/CommonError'; -export const errorHandling = (err: Error, req: express.Request, res: express.Response, next: express.NextFunction): void => { +export const errorHandling = ( + err: Error, + req: express.Request, + res: express.Response, + next: express.NextFunction, +): void => { if ((err as ICommonError).errorId) { createErrorResponse(req, res, err as ICommonError); } else { logger.warn('Error that is not CommonError occurred. Raw error: ', err); - createErrorResponse(req, res, new CommonError( - err.message || err as unknown as string || 'Something bad happened', - { - error: err, - errorString: JSON.stringify(err) - } - )); + createErrorResponse( + req, + res, + new CommonError( + err.message || ((err as unknown) as string) || 'Something bad happened', + { + error: err, + errorString: JSON.stringify(err), + }, + ), + ); } next(); diff --git a/functions/src/util/middleware/index.ts b/functions/src/util/middleware/index.ts index af24c520..0ada79ba 100644 --- a/functions/src/util/middleware/index.ts +++ b/functions/src/util/middleware/index.ts @@ -1,5 +1,5 @@ -export { requestLoggingMiddleware } from './requestLoggingMiddleware'; -export { routeBasedMiddleware } from './routeBasedMiddleware'; -export { errorHandling } from './errorHandlingMiddleware'; -export { authenticate } from './authenticateMiddleware'; -export { sessions } from './sessionMiddleware'; +export {requestLoggingMiddleware} from './requestLoggingMiddleware'; +export {routeBasedMiddleware} from './routeBasedMiddleware'; +export {errorHandling} from './errorHandlingMiddleware'; +export {authenticate} from './authenticateMiddleware'; +export {sessions} from './sessionMiddleware'; diff --git a/functions/src/util/middleware/requestLoggingMiddleware.ts b/functions/src/util/middleware/requestLoggingMiddleware.ts index f28f854e..7e9265f3 100644 --- a/functions/src/util/middleware/requestLoggingMiddleware.ts +++ b/functions/src/util/middleware/requestLoggingMiddleware.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from 'express'; +import {RequestHandler} from 'express'; export const requestLoggingMiddleware: RequestHandler = (req, res, next) => { logger.debug('New request received', { @@ -6,20 +6,18 @@ export const requestLoggingMiddleware: RequestHandler = (req, res, next) => { path: req.originalUrl, data: { - body: req.body, query: req.query, - params: req.params - } + params: req.params, + }, }); res.on('finish', () => { logger.debug('Request served', { statusCode: res.statusCode, - requestId: req.requestId + requestId: req.requestId, }); }); - next(); -}; \ No newline at end of file +}; diff --git a/functions/src/util/middleware/routeBasedMiddleware.ts b/functions/src/util/middleware/routeBasedMiddleware.ts index 3cb1b8ca..188fbc77 100644 --- a/functions/src/util/middleware/routeBasedMiddleware.ts +++ b/functions/src/util/middleware/routeBasedMiddleware.ts @@ -4,12 +4,12 @@ interface IRouteBasedMiddlewareOptions { /** * Only those routes will have the passed middleware */ - include?: string[], + include?: string[]; /** * Only those routes will not have the passed middleware */ - exclude?: string[], + exclude?: string[]; /** * Whether the middleware will be applied if both the include @@ -21,18 +21,21 @@ interface IRouteBasedMiddlewareOptions { const defaultOptions: IRouteBasedMiddlewareOptions = { applyByDefault: true, exclude: [], - include: [] + include: [], }; -export const routeBasedMiddleware = (middleware: express.RequestHandler, middlewareOptions: IRouteBasedMiddlewareOptions): express.RequestHandler => { +export const routeBasedMiddleware = ( + middleware: express.RequestHandler, + middlewareOptions: IRouteBasedMiddlewareOptions, +): express.RequestHandler => { const options = { ...defaultOptions, - ...middlewareOptions + ...middlewareOptions, }; return (req, res, next) => { if (options.include?.length) { - if (options.include.some(x => x === req.path)) { + if (options.include.some((x) => x === req.path)) { return middleware(req, res, next); } else { return next(); @@ -40,15 +43,13 @@ export const routeBasedMiddleware = (middleware: express.RequestHandler, middlew } if (options.exclude?.length) { - if (options.exclude.some(x => x === req.path)) { + if (options.exclude.some((x) => x === req.path)) { return next(); } else { return middleware(req, res, next); } } - return options.applyByDefault - ? middleware(req, res, next) - : next(); + return options.applyByDefault ? middleware(req, res, next) : next(); }; -}; \ No newline at end of file +}; diff --git a/functions/src/util/middleware/sessionMiddleware.ts b/functions/src/util/middleware/sessionMiddleware.ts index 5df58a4b..143db50b 100644 --- a/functions/src/util/middleware/sessionMiddleware.ts +++ b/functions/src/util/middleware/sessionMiddleware.ts @@ -1,8 +1,11 @@ import express from 'express'; -import { v4 } from 'uuid'; +import {v4} from 'uuid'; - -export const sessions = (req: express.Request, res: express.Response, next: express.NextFunction): void => { +export const sessions = ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +): void => { req.requestId = v4(); next(); diff --git a/functions/src/util/poll.ts b/functions/src/util/poll.ts index b7936d75..c9dab847 100644 --- a/functions/src/util/poll.ts +++ b/functions/src/util/poll.ts @@ -1,7 +1,6 @@ -import { CommonError } from './errors'; -import { sleep } from './sleep'; -import { Promisable } from './types'; - +import {CommonError} from './errors'; +import {sleep} from './sleep'; +import {Promisable} from './types'; export type IPollAction = () => Promisable; export type IPollValidator = (result: T) => Promisable; @@ -20,7 +19,7 @@ export const poll = async ( action: IPollAction, validate: IPollValidator, interval = 60, - maxAttempts = 64 + maxAttempts = 64, ): Promise => { let currentAttempt = 0; @@ -45,4 +44,4 @@ export const poll = async ( }; return execute(); -}; \ No newline at end of file +}; diff --git a/functions/src/util/responseExecutor.ts b/functions/src/util/responseExecutor.ts index 8fda75dd..fbe94807 100644 --- a/functions/src/util/responseExecutor.ts +++ b/functions/src/util/responseExecutor.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { StatusCodes } from '../constants'; +import {StatusCodes} from '../constants'; interface IResponseExecutorAction { (): any; @@ -14,32 +14,39 @@ interface IResponseExecutorPayload { } interface IResponseExecutor { - (action: IResponseExecutorAction, payload: IResponseExecutorPayload): Promise + ( + action: IResponseExecutorAction, + payload: IResponseExecutorPayload, + ): Promise; } -export const responseExecutor: IResponseExecutor = async (action, { req, res, next, successMessage }): Promise => { +export const responseExecutor: IResponseExecutor = async ( + action, + {req, res, next, successMessage}, +): Promise => { try { - const actionResult = await action() || {}; + const actionResult = (await action()) || {}; logger.info(`Request action successfully finished execution.`, { requestId: req.requestId, - result: actionResult + result: actionResult, }); - res - .status(StatusCodes.Ok) - .json({ - message: successMessage, - ...actionResult - }); + res.status(StatusCodes.Ok).json({ + message: successMessage, + ...actionResult, + }); return next(); } catch (e) { - logger.debug('An error occurred while executing the response executor\'s action', { - error: e - }); + logger.debug( + "An error occurred while executing the response executor's action", + { + error: e, + }, + ); return next(e); } -}; \ No newline at end of file +}; diff --git a/functions/src/util/schemas/index.ts b/functions/src/util/schemas/index.ts index 65b29cf6..945256e2 100644 --- a/functions/src/util/schemas/index.ts +++ b/functions/src/util/schemas/index.ts @@ -4,103 +4,63 @@ const isDistrictRequired = (country: string): boolean => country === 'US' || country === 'CA'; export const linkValidationSchema = yup.object({ - title: yup.string() - .max(64), + title: yup.string().max(64), - value: yup.string() - .required() - .url() + value: yup.string().required().url(), }); export const commonRuleValidationSchema = yup.object({ - title: yup - .string() - .required(), + title: yup.string().required(), - value: yup - .string() - .max(512) + value: yup.string().max(512), }); export const commonLinkValidationScheme = yup.object({ - title: yup - .string() - .required(), + title: yup.string().required(), - value: yup - .string() - .url() - .required() -}) + value: yup.string().url().required(), +}); export const fileValidationSchema = yup.object({ - value: yup - .string() - .url() - .required() + value: yup.string().url().required(), }); export const imageValidationSchema = yup.object({ - value: yup - .string() - .url() - .required() + value: yup.string().url().required(), }); export const billingDetailsValidationSchema = yup.object({ - name: yup - .string() - .required(), - - city: yup - .string() - .required(), - - country: yup - .string() - .required(), - - line1: yup - .string() - .required(), - - line2: yup - .string(), - - district: yup - .string() - .when('country', { - is: isDistrictRequired, - then: yup.string().required() - }), - - postalCode: yup - .string() - .required() + name: yup.string().required(), + + city: yup.string().required(), + + country: yup.string().required(), + + line1: yup.string().required(), + + line2: yup.string(), + + district: yup.string().when('country', { + is: isDistrictRequired, + then: yup.string().required(), + }), + + postalCode: yup.string().required(), }); export const bankAccountValidationSchema = yup.object({ - name: yup - .string(), - - city: yup - .string() - .required(), - - country: yup - .string() - .required(), - - line1: yup - .string(), - - line2: yup - .string(), - - district: yup - .string() - .when('country', { - is: isDistrictRequired, - then: yup.string().required() - }) -}) \ No newline at end of file + name: yup.string(), + + city: yup.string().required(), + + country: yup.string().required(), + + line1: yup.string(), + + line2: yup.string(), + + district: yup.string().when('country', { + is: isDistrictRequired, + then: yup.string().required(), + }), +}); diff --git a/functions/src/util/sleep.ts b/functions/src/util/sleep.ts index 3b9a24ee..710b747d 100644 --- a/functions/src/util/sleep.ts +++ b/functions/src/util/sleep.ts @@ -1 +1,2 @@ -export const sleep = (duration: number): Promise => new Promise((resolve) => setTimeout(resolve, duration)); +export const sleep = (duration: number): Promise => + new Promise((resolve) => setTimeout(resolve, duration)); diff --git a/functions/src/util/types/index.ts b/functions/src/util/types/index.ts index 31a8a14a..af15eb1f 100644 --- a/functions/src/util/types/index.ts +++ b/functions/src/util/types/index.ts @@ -1,8 +1,8 @@ import admin from 'firebase-admin'; import Timestamp = admin.firestore.Timestamp; -import { IEventEntity } from '../../event/type'; -import { EventContext } from 'firebase-functions/lib/cloud-functions'; +import {IEventEntity} from '../../event/type'; +import {EventContext} from 'firebase-functions/lib/cloud-functions'; export type valueOf = T[keyof T]; export type Nullable = T | null | undefined; @@ -34,7 +34,6 @@ export interface IPaymentRefund { status: CirclePaymentStatus; } - export interface ICircleNotification { clientId: string; notificationType: 'payments' | string; @@ -53,7 +52,7 @@ export interface ICircleNotification { updateDate: Date; refunds: IPaymentRefund[]; - } + }; } export type BaseEntityType = 'id' | 'createdAt' | 'updatedAt'; @@ -87,4 +86,7 @@ export interface IBaseEntity { * @param eventObj - The object containing the event details * @param context - Context about the currently executing trigger */ -export type IEventTrigger = (eventObj: IEventEntity, context: EventContext) => void | Promise \ No newline at end of file +export type IEventTrigger = ( + eventObj: IEventEntity, + context: EventContext, +) => void | Promise; diff --git a/functions/src/util/util.js b/functions/src/util/util.js index 22f4c73d..c5c25f3e 100644 --- a/functions/src/util/util.js +++ b/functions/src/util/util.js @@ -1,22 +1,20 @@ const admin = require('firebase-admin'); const fetch = require('node-fetch'); -const { CommonError } = require('./errors'); -const { env } = require('../constants'); +const {CommonError} = require('./errors'); +const {env} = require('../constants'); // That was imported from './error', but was not // there so I don't know what is it const CFError = { invalidIdToken: 'invalidIdToken', emptyPaymentData: 'emptyPaymentData', - emptyUserData: 'emptyUserData' -} - + emptyUserData: 'emptyUserData', +}; class Utils { - getCommonLink(commonId) { - return `https://app.common.io/common/${commonId}` + return `https://app.common.io/common/${commonId}`; } getUserRef(uid) { @@ -30,49 +28,57 @@ class Utils { async getUserById(uid) { try { const userRef = admin.firestore().collection('users').doc(uid); - const userData = await userRef.get().then(doc => { return doc.data() }) - return userData + const userData = await userRef.get().then((doc) => { + return doc.data(); + }); + return userData; } catch (err) { - throw new CommonError(CFError.emptyUserData) + throw new CommonError(CFError.emptyUserData); } } async getCommonById(commonId) { try { const userRef = admin.firestore().collection('daos').doc(commonId); - const userData = await userRef.get().then(doc => { return doc.data() }) - return userData + const userData = await userRef.get().then((doc) => { + return doc.data(); + }); + return userData; } catch (err) { - throw new CommonError(CFError.emptyUserData) + throw new CommonError(CFError.emptyUserData); } } async getPaymentById(paymentId) { try { - const paymentRef = await admin.firestore().collection('payments') + const paymentRef = await admin + .firestore() + .collection('payments') .where('id', '==', paymentId) .get(); - const paymentData = paymentRef.docs.map(doc => doc.data())[0]; + const paymentData = paymentRef.docs.map((doc) => doc.data())[0]; return paymentData; } catch (err) { - throw new CommonError(CFError.emptyUserData) + throw new CommonError(CFError.emptyUserData); } } async getPaymentByProposalId(proposalId) { try { - const paymentRef = await admin.firestore().collection('payments') + const paymentRef = await admin + .firestore() + .collection('payments') .where('proposalId', '==', proposalId) .get(); - const paymentData = paymentRef.docs.map(doc => doc.data())[0]; + const paymentData = paymentRef.docs.map((doc) => doc.data())[0]; return paymentData; } catch (err) { - throw new CommonError(CFError.emptyUserData) + throw new CommonError(CFError.emptyUserData); } } } module.exports = { Utils: new Utils(), - DAO_REGISTERED: 'registered' -} + DAO_REGISTERED: 'registered', +}; diff --git a/functions/src/util/validate.ts b/functions/src/util/validate.ts index eeae4db5..cf4f7c63 100644 --- a/functions/src/util/validate.ts +++ b/functions/src/util/validate.ts @@ -1,9 +1,9 @@ import * as yup from 'yup'; import * as _ from 'lodash'; -import { ObjectSchema } from 'yup'; +import {ObjectSchema} from 'yup'; -import { CommonError, ValidationError } from './errors'; +import {CommonError, ValidationError} from './errors'; /** * Validates the provided payload against the schema and throw formatted @@ -17,7 +17,10 @@ import { CommonError, ValidationError } from './errors'; * * @returns Promise */ -export const validate = async >(payload: T, schema: ObjectSchema): Promise => { +export const validate = async >( + payload: T, + schema: ObjectSchema, +): Promise => { try { await schema .test('no-unknown', 'No unknown keys are allowed', function (value) { @@ -29,24 +32,29 @@ export const validate = async >(payload: T, schema if (unknownKeys.length) { return this.createError({ - message: `No unknown keys are allowed. Unknown keys: ${unknownKeys.join(', ')}` + message: `No unknown keys are allowed. Unknown keys: ${unknownKeys.join( + ', ', + )}`, }); } return true; }) .validate(payload, { - abortEarly: false + abortEarly: false, }); } catch (e) { if (!(e instanceof yup.ValidationError)) { - throw new CommonError('Unknown error occurred while doing the validation', { - error: e, - validatorPayload: { - schema, - payload - } - }); + throw new CommonError( + 'Unknown error occurred while doing the validation', + { + error: e, + validatorPayload: { + schema, + payload, + }, + }, + ); } logger.debug('Validation failed with payload', payload); @@ -54,4 +62,4 @@ export const validate = async >(payload: T, schema throw new ValidationError(e); } -}; \ No newline at end of file +};