From afcede41c5675decd58453c0cec7cd82d7dd4d83 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 19 Oct 2020 21:14:19 +0200 Subject: [PATCH 01/28] Use npm module cron to better schedule both regular and vetted donation distribution processes --- server/package.json | 2 ++ server/src/start-worker.ts | 4 +-- server/src/worker/distribution-worker.ts | 31 +++++++++--------- .../vetted-beneficiary-distribution-worker.ts | 32 +++++++++---------- server/yarn.lock | 27 ++++++++++++++++ 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/server/package.json b/server/package.json index e52c9339..13dccb89 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/axios": "^0.14.0", "@types/cors": "^2.8.6", + "@types/cron": "^1.7.2", "@types/express": "^4.17.3", "@types/express-rate-limit": "^5.0.0", "@types/hapi__joi": "^17.1.0", @@ -33,6 +34,7 @@ "axios": "^0.19.2", "commander": "^6.1.0", "cors": "^2.8.5", + "cron": "^1.8.2", "dotenv": "^8.2.0", "express": "^4.17.1", "express-rate-limit": "^5.1.3", diff --git a/server/src/start-worker.ts b/server/src/start-worker.ts index a58f09e4..7c56b360 100644 --- a/server/src/start-worker.ts +++ b/server/src/start-worker.ts @@ -7,8 +7,8 @@ export async function startWorker() { try { const config = loadAppConfigFromEnv(process.env); const app = await bootstrap(config); - runDistributionWorker(app, config.distributionInterval * MILLISECONDS_PER_MINUTE); - runVettedBeneficiaryDistributionWorker(app, config.vettedDistributionInterval * MILLISECONDS_PER_MINUTE); + runDistributionWorker(app, config.distributionInterval); + runVettedBeneficiaryDistributionWorker(app, config.vettedDistributionInterval); runStatsComputationWorker(app, config.statsComputationInterval * MILLISECONDS_PER_MINUTE); } catch (e) { diff --git a/server/src/worker/distribution-worker.ts b/server/src/worker/distribution-worker.ts index 5a3b8bec..c130dbf8 100644 --- a/server/src/worker/distribution-worker.ts +++ b/server/src/worker/distribution-worker.ts @@ -1,20 +1,19 @@ import { App } from '../core'; +import { CronJob } from 'cron'; -export function runDistributionWorker(app: App, intervalMilliseconds: number) { - async function workLoop() { - try { - console.log(`Starting distribution process at ${new Date()}...`); - const result = await app.donationDistributions.distributeDonations(); - console.log(`Completed distribution process at ${new Date()}`); - console.log(result); - console.log(); - setTimeout(workLoop, intervalMilliseconds); - } - catch(e) { - console.error(e); - setTimeout(workLoop, intervalMilliseconds); - } - } +export function runDistributionWorker(app: App, intervalMinutes: number) { + const job = new CronJob(`* ${intervalMinutes} * * * * *`, async () => { + const result = await app.donationDistributions.distributeDonations(); + console.log(`Completed distribution process at ${new Date()}`); + console.log(result); + console.log(); + }, null, true, 'Africa/Nairobi'); - workLoop(); + try { + job.start(); + } + catch(e) { + console.error(e); + job.start(); + } } \ No newline at end of file diff --git a/server/src/worker/vetted-beneficiary-distribution-worker.ts b/server/src/worker/vetted-beneficiary-distribution-worker.ts index 203f3e1e..065b88a9 100644 --- a/server/src/worker/vetted-beneficiary-distribution-worker.ts +++ b/server/src/worker/vetted-beneficiary-distribution-worker.ts @@ -1,20 +1,20 @@ import { App } from '../core'; +import { CronJob } from 'cron'; -export function runVettedBeneficiaryDistributionWorker(app: App, intervalMilliseconds: number) { - async function workLoop() { - try { - console.log(`Starting distribution process for vetted beneficiaries at ${new Date()}...`); - const result = await app.donationDistributions.distributeDonations(true); - console.log(`Completed distribution process for vetted beneficiaries at ${new Date()}`); - console.log(result); - console.log(); - setTimeout(workLoop, intervalMilliseconds); - } - catch(e) { - console.error(e); - setTimeout(workLoop, intervalMilliseconds); - } - } +export function runVettedBeneficiaryDistributionWorker(app: App, intervalMinutes: number) { + const job = new CronJob(`* ${intervalMinutes} * * * * *`, async () => { + console.log(`Starting distribution process for vetted beneficiaries at ${new Date()}...`); + const result = await app.donationDistributions.distributeDonations(true); + console.log(`Completed distribution process for vetted beneficiaries at ${new Date()}`); + console.log(result); + console.log(); + }, null, true, 'Africa/Nairobi'); - workLoop(); + try { + job.start(); + } + catch(e) { + console.error(e); + job.start(); + } } \ No newline at end of file diff --git a/server/yarn.lock b/server/yarn.lock index a03a1f97..23513fb7 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -661,6 +661,14 @@ dependencies: "@types/express" "*" +"@types/cron@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/cron/-/cron-1.7.2.tgz#e9fb420da616920dae82d13adfca53282ffaab6e" + integrity sha512-AEpNLRcsVSc5AdseJKNHpz0d4e8+ow+abTaC0fKDbAU86rF1evoFF0oC2fV9FdqtfVXkG2LKshpLTJCFOpyvTg== + dependencies: + "@types/node" "*" + moment ">=2.14.0" + "@types/express-rate-limit@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/express-rate-limit/-/express-rate-limit-5.0.0.tgz#46b0dbae748a53347a5e1c3bdbb5a54e3f5c34f9" @@ -1583,6 +1591,13 @@ cors@^2.8.5: object-assign "^4" vary "^1" +cron@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/cron/-/cron-1.8.2.tgz#4ac5e3c55ba8c163d84f3407bde94632da8370ce" + integrity sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg== + dependencies: + moment-timezone "^0.5.x" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -3667,6 +3682,18 @@ mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +moment-timezone@^0.5.x: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@>=2.14.0: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + mongodb@^3.5.5: version "3.5.5" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.5.tgz#1334c3e5a384469ac7ef0dea69d59acc829a496a" From 9f614b51b32370ddf8c5ca377ae08d7530631cdc Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 19 Oct 2020 21:44:57 +0200 Subject: [PATCH 02/28] Append an asterisk and a forward slash before the minutes field value within the CronTime parameter --- server/src/worker/distribution-worker.ts | 2 +- server/src/worker/vetted-beneficiary-distribution-worker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/worker/distribution-worker.ts b/server/src/worker/distribution-worker.ts index c130dbf8..2e5af4be 100644 --- a/server/src/worker/distribution-worker.ts +++ b/server/src/worker/distribution-worker.ts @@ -2,7 +2,7 @@ import { App } from '../core'; import { CronJob } from 'cron'; export function runDistributionWorker(app: App, intervalMinutes: number) { - const job = new CronJob(`* ${intervalMinutes} * * * * *`, async () => { + const job = new CronJob(`0 */${intervalMinutes} * * * *`, async () => { const result = await app.donationDistributions.distributeDonations(); console.log(`Completed distribution process at ${new Date()}`); console.log(result); diff --git a/server/src/worker/vetted-beneficiary-distribution-worker.ts b/server/src/worker/vetted-beneficiary-distribution-worker.ts index 065b88a9..6d9862a1 100644 --- a/server/src/worker/vetted-beneficiary-distribution-worker.ts +++ b/server/src/worker/vetted-beneficiary-distribution-worker.ts @@ -2,7 +2,7 @@ import { App } from '../core'; import { CronJob } from 'cron'; export function runVettedBeneficiaryDistributionWorker(app: App, intervalMinutes: number) { - const job = new CronJob(`* ${intervalMinutes} * * * * *`, async () => { + const job = new CronJob(`0 */${intervalMinutes} * * * *`, async () => { console.log(`Starting distribution process for vetted beneficiaries at ${new Date()}...`); const result = await app.donationDistributions.distributeDonations(true); console.log(`Completed distribution process for vetted beneficiaries at ${new Date()}`); From c4ea196387072c650ea00a6d8393a0ecb5973ca4 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 19 Oct 2020 22:04:30 +0200 Subject: [PATCH 03/28] Pass interval in CronTime syntax to both runDistributionWorker and runVettedBeneficiaryDistributionWorker --- server/src/start-worker.ts | 4 ++-- server/src/worker/distribution-worker.ts | 4 ++-- server/src/worker/vetted-beneficiary-distribution-worker.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/start-worker.ts b/server/src/start-worker.ts index 7c56b360..7b8ed71e 100644 --- a/server/src/start-worker.ts +++ b/server/src/start-worker.ts @@ -7,8 +7,8 @@ export async function startWorker() { try { const config = loadAppConfigFromEnv(process.env); const app = await bootstrap(config); - runDistributionWorker(app, config.distributionInterval); - runVettedBeneficiaryDistributionWorker(app, config.vettedDistributionInterval); + runDistributionWorker(app, `0 */${config.distributionInterval} * * * *`); + runVettedBeneficiaryDistributionWorker(app, `0 */${config.vettedDistributionInterval} * * * *`); runStatsComputationWorker(app, config.statsComputationInterval * MILLISECONDS_PER_MINUTE); } catch (e) { diff --git a/server/src/worker/distribution-worker.ts b/server/src/worker/distribution-worker.ts index 2e5af4be..e14feb06 100644 --- a/server/src/worker/distribution-worker.ts +++ b/server/src/worker/distribution-worker.ts @@ -1,8 +1,8 @@ import { App } from '../core'; import { CronJob } from 'cron'; -export function runDistributionWorker(app: App, intervalMinutes: number) { - const job = new CronJob(`0 */${intervalMinutes} * * * *`, async () => { +export function runDistributionWorker(app: App, interval: string) { + const job = new CronJob(interval, async () => { const result = await app.donationDistributions.distributeDonations(); console.log(`Completed distribution process at ${new Date()}`); console.log(result); diff --git a/server/src/worker/vetted-beneficiary-distribution-worker.ts b/server/src/worker/vetted-beneficiary-distribution-worker.ts index 6d9862a1..189a8684 100644 --- a/server/src/worker/vetted-beneficiary-distribution-worker.ts +++ b/server/src/worker/vetted-beneficiary-distribution-worker.ts @@ -1,8 +1,8 @@ import { App } from '../core'; import { CronJob } from 'cron'; -export function runVettedBeneficiaryDistributionWorker(app: App, intervalMinutes: number) { - const job = new CronJob(`0 */${intervalMinutes} * * * *`, async () => { +export function runVettedBeneficiaryDistributionWorker(app: App, interval: string) { + const job = new CronJob(interval, async () => { console.log(`Starting distribution process for vetted beneficiaries at ${new Date()}...`); const result = await app.donationDistributions.distributeDonations(true); console.log(`Completed distribution process for vetted beneficiaries at ${new Date()}`); From 19a5143056738b0f4d4617b5f637498081ca37fc Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 19 Oct 2020 22:15:56 +0200 Subject: [PATCH 04/28] Re-configure env vars distributionInterval and vettedDistributionInterval to be of type string and to have CronTime format values --- server/src/core/app.ts | 12 ++++++------ server/src/start-worker.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/core/app.ts b/server/src/core/app.ts index d633cdf9..7ff674a0 100644 --- a/server/src/core/app.ts +++ b/server/src/core/app.ts @@ -94,15 +94,15 @@ export interface AppConfig { */ distributionPeriodLength: number; /** - * Interval delay in minutes between two distribution + * Interval delay in CronTime syntax/format between two distribution * processes */ - distributionInterval: number; + distributionInterval: string; /** - * Interval delay in minutes between two distribution + * Interval delay in CronTime syntax/format between two distribution * processes for vetted beneficiaires */ - vettedDistributionInterval: number; + vettedDistributionInterval: string; /** * Interval delay in minutes between two * processes of computing statistics @@ -138,8 +138,8 @@ export function loadAppConfigFromEnv(env: { [key: string]: string }): AppConfig flutterwaveWebhooksRoot: env.FLUTTERWAVE_WEBHOOKS || '/webhooks/flutterwave', distributionPeriodLimit: (env.DISTRIBUTION_PERIOD_LIMIT && Number(env.DISTRIBUTION_PERIOD_LIMIT)) || 2000, distributionPeriodLength: (env.DISTRIBUTION_PERIOD_LENGTH && Number(env.DISTRIBUTION_PERIOD_LENGTH)) || 30, - distributionInterval: (env.DISTRIBUTION_INTERVAL && Number(env.DISTRIBUTION_INTERVAL)) || 2, - vettedDistributionInterval: (env.VETTED_DISTRIBUTION_INTERVAL && Number(env.VETTED_DISTRIBUTION_INTERVAL)) || 5, + distributionInterval: (env.DISTRIBUTION_INTERVAL) || `0 */5 * * * *`, + vettedDistributionInterval: (env.VETTED_DISTRIBUTION_INTERVAL) || `0 */5 * * * *`, statsComputationInterval: (env.STATS_COMPUTATION_INTERVAL && Number(env.STATS_COMPUTATION_INTERVAL)) || 1, googleClientId: env.GOOGLE_CLIENT_ID, sendgridApiKey: env.SENDGRID_API_KEY || '', diff --git a/server/src/start-worker.ts b/server/src/start-worker.ts index 7b8ed71e..7c56b360 100644 --- a/server/src/start-worker.ts +++ b/server/src/start-worker.ts @@ -7,8 +7,8 @@ export async function startWorker() { try { const config = loadAppConfigFromEnv(process.env); const app = await bootstrap(config); - runDistributionWorker(app, `0 */${config.distributionInterval} * * * *`); - runVettedBeneficiaryDistributionWorker(app, `0 */${config.vettedDistributionInterval} * * * *`); + runDistributionWorker(app, config.distributionInterval); + runVettedBeneficiaryDistributionWorker(app, config.vettedDistributionInterval); runStatsComputationWorker(app, config.statsComputationInterval * MILLISECONDS_PER_MINUTE); } catch (e) { From 8fc0d4e974b7c431ee227d3705c97fc082bf9137 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Mon, 19 Oct 2020 22:16:47 +0200 Subject: [PATCH 05/28] Re-configure env vars distributionInterval and vettedDistributionInterval to be of type string and to have CronTime format values --- server/src/core/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/core/app.ts b/server/src/core/app.ts index 7ff674a0..f2291d59 100644 --- a/server/src/core/app.ts +++ b/server/src/core/app.ts @@ -138,7 +138,7 @@ export function loadAppConfigFromEnv(env: { [key: string]: string }): AppConfig flutterwaveWebhooksRoot: env.FLUTTERWAVE_WEBHOOKS || '/webhooks/flutterwave', distributionPeriodLimit: (env.DISTRIBUTION_PERIOD_LIMIT && Number(env.DISTRIBUTION_PERIOD_LIMIT)) || 2000, distributionPeriodLength: (env.DISTRIBUTION_PERIOD_LENGTH && Number(env.DISTRIBUTION_PERIOD_LENGTH)) || 30, - distributionInterval: (env.DISTRIBUTION_INTERVAL) || `0 */5 * * * *`, + distributionInterval: (env.DISTRIBUTION_INTERVAL) || `0 */2 * * * *`, vettedDistributionInterval: (env.VETTED_DISTRIBUTION_INTERVAL) || `0 */5 * * * *`, statsComputationInterval: (env.STATS_COMPUTATION_INTERVAL && Number(env.STATS_COMPUTATION_INTERVAL)) || 1, googleClientId: env.GOOGLE_CLIENT_ID, From bc8855f1fa87c7ab99f9f04ea92bd7610998d3fa Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 20:22:04 +0200 Subject: [PATCH 06/28] Add and implement getters and setters for preferred payment provider for handling refunds --- server/src/core/payment/provider-registry.ts | 9 +++++++++ server/src/core/payment/transaction-service.ts | 4 ++++ server/src/core/payment/types.ts | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/server/src/core/payment/provider-registry.ts b/server/src/core/payment/provider-registry.ts index ca787127..cc8d2136 100644 --- a/server/src/core/payment/provider-registry.ts +++ b/server/src/core/payment/provider-registry.ts @@ -7,6 +7,7 @@ export class PaymentProviders implements PaymentProviderRegistry { } = {}; private preferredSending: string; + private preferredRefund: string; private preferredReceiving: string; register(provider: PaymentProvider): void { @@ -36,4 +37,12 @@ export class PaymentProviders implements PaymentProviderRegistry { this.preferredSending = name; } + getPreferredForRefund(): PaymentProvider { + return this.getProvider(this.preferredRefund); + } + + setPreferredForRefund(name: string): void { + this.preferredRefund = name; + } + } \ No newline at end of file diff --git a/server/src/core/payment/transaction-service.ts b/server/src/core/payment/transaction-service.ts index e6e02797..9f59f871 100644 --- a/server/src/core/payment/transaction-service.ts +++ b/server/src/core/payment/transaction-service.ts @@ -352,6 +352,10 @@ export class Transactions implements TransactionService { return this.providers.getPreferredForReceiving(); } + private refundProvider(): PaymentProvider { + return this.providers.getPreferredForRefund(); + } + private sendingProvider(): PaymentProvider { return this.providers.getPreferredForSending(); } diff --git a/server/src/core/payment/types.ts b/server/src/core/payment/types.ts index d6b420fa..b4ed4449 100644 --- a/server/src/core/payment/types.ts +++ b/server/src/core/payment/types.ts @@ -166,13 +166,23 @@ export interface PaymentProviderRegistry { */ setPreferredForReceiving(name: string): void; /** - * sets preferred payment provider for + * gets preferred payment provider for * sending money to users */ getPreferredForSending(): PaymentProvider; /** - * gets preferred payment provider for sending money to users + * sets preferred payment provider for sending money to users * @param name */ setPreferredForSending(name: string): void; + /** + * gets preferred payment provider for + * refunding donated money + */ + getPreferredForRefund(): PaymentProvider; + /** + * sets preferred payment provider for refunding donated money + * @param name + */ + setPreferredForRefund(name: string): void; } \ No newline at end of file From 578698a11c961686694f07b6c436e89438a2385e Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 20:24:22 +0200 Subject: [PATCH 07/28] Set the preferred payment provider for handling refunds to be flutterwave in bootstrap --- server/src/core/bootstrap.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/core/bootstrap.ts b/server/src/core/bootstrap.ts index 73e1acda..6a7e3b2b 100644 --- a/server/src/core/bootstrap.ts +++ b/server/src/core/bootstrap.ts @@ -40,6 +40,7 @@ export async function bootstrap(config: AppConfig): Promise { paymentProviders.register(manualPayProvider); paymentProviders.setPreferredForReceiving(flwPaymentProvider.name()); paymentProviders.setPreferredForSending(manualPayProvider.name()); + paymentProviders.setPreferredForRefund(flwPaymentProvider.name()); const systemLocks = new SystemLocks(db); const transactions = new Transactions(db, { paymentProviders, eventBus }); From f68a858343da7073e83aabe84b6d46cbaecfdb2f Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 20:38:57 +0200 Subject: [PATCH 08/28] Add and implement getters and setters for preferred payment provider for handling refunds --- server/src/core/bootstrap.ts | 2 +- server/src/core/payment/provider-registry.ts | 10 +++++----- server/src/core/payment/types.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/core/bootstrap.ts b/server/src/core/bootstrap.ts index 6a7e3b2b..ed88499e 100644 --- a/server/src/core/bootstrap.ts +++ b/server/src/core/bootstrap.ts @@ -40,7 +40,7 @@ export async function bootstrap(config: AppConfig): Promise { paymentProviders.register(manualPayProvider); paymentProviders.setPreferredForReceiving(flwPaymentProvider.name()); paymentProviders.setPreferredForSending(manualPayProvider.name()); - paymentProviders.setPreferredForRefund(flwPaymentProvider.name()); + paymentProviders.setPreferredForRefunds(flwPaymentProvider.name()); const systemLocks = new SystemLocks(db); const transactions = new Transactions(db, { paymentProviders, eventBus }); diff --git a/server/src/core/payment/provider-registry.ts b/server/src/core/payment/provider-registry.ts index cc8d2136..304f414f 100644 --- a/server/src/core/payment/provider-registry.ts +++ b/server/src/core/payment/provider-registry.ts @@ -7,7 +7,7 @@ export class PaymentProviders implements PaymentProviderRegistry { } = {}; private preferredSending: string; - private preferredRefund: string; + private preferredRefunds: string; private preferredReceiving: string; register(provider: PaymentProvider): void { @@ -37,12 +37,12 @@ export class PaymentProviders implements PaymentProviderRegistry { this.preferredSending = name; } - getPreferredForRefund(): PaymentProvider { - return this.getProvider(this.preferredRefund); + getPreferredForRefunds(): PaymentProvider { + return this.getProvider(this.preferredRefunds); } - setPreferredForRefund(name: string): void { - this.preferredRefund = name; + setPreferredForRefunds(name: string): void { + this.preferredRefunds = name; } } \ No newline at end of file diff --git a/server/src/core/payment/types.ts b/server/src/core/payment/types.ts index b4ed4449..3fb6b1db 100644 --- a/server/src/core/payment/types.ts +++ b/server/src/core/payment/types.ts @@ -179,10 +179,10 @@ export interface PaymentProviderRegistry { * gets preferred payment provider for * refunding donated money */ - getPreferredForRefund(): PaymentProvider; + getPreferredForRefunds(): PaymentProvider; /** * sets preferred payment provider for refunding donated money * @param name */ - setPreferredForRefund(name: string): void; + setPreferredForRefunds(name: string): void; } \ No newline at end of file From ed69abf9942ab463883332f013714a68656cf370 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 20:40:23 +0200 Subject: [PATCH 09/28] Implement a refundsProvider method that returns the corresponding provider in the transaction service --- server/src/core/payment/transaction-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/core/payment/transaction-service.ts b/server/src/core/payment/transaction-service.ts index 9f59f871..480906ac 100644 --- a/server/src/core/payment/transaction-service.ts +++ b/server/src/core/payment/transaction-service.ts @@ -352,8 +352,8 @@ export class Transactions implements TransactionService { return this.providers.getPreferredForReceiving(); } - private refundProvider(): PaymentProvider { - return this.providers.getPreferredForRefund(); + private refundsProvider(): PaymentProvider { + return this.providers.getPreferredForRefunds(); } private sendingProvider(): PaymentProvider { From 17455bc0eff09c124e9eedd22bf4b8baea75b4d9 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 20:53:34 +0200 Subject: [PATCH 10/28] Specify refund payment provider within method initiateRefund by invoking private method refundsProvider --- server/src/core/bootstrap.ts | 4 ++-- server/src/core/payment/transaction-service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/core/bootstrap.ts b/server/src/core/bootstrap.ts index ed88499e..f00f1254 100644 --- a/server/src/core/bootstrap.ts +++ b/server/src/core/bootstrap.ts @@ -39,8 +39,8 @@ export async function bootstrap(config: AppConfig): Promise { paymentProviders.register(flwPaymentProvider); paymentProviders.register(manualPayProvider); paymentProviders.setPreferredForReceiving(flwPaymentProvider.name()); - paymentProviders.setPreferredForSending(manualPayProvider.name()); - paymentProviders.setPreferredForRefunds(flwPaymentProvider.name()); + paymentProviders.setPreferredForSending(flwPaymentProvider.name()); + paymentProviders.setPreferredForRefunds(manualPayProvider.name()); const systemLocks = new SystemLocks(db); const transactions = new Transactions(db, { paymentProviders, eventBus }); diff --git a/server/src/core/payment/transaction-service.ts b/server/src/core/payment/transaction-service.ts index 480906ac..0561c94e 100644 --- a/server/src/core/payment/transaction-service.ts +++ b/server/src/core/payment/transaction-service.ts @@ -138,7 +138,7 @@ export class Transactions implements TransactionService { fromExternal: false, from: user._id, type: 'refund', - provider: this.sendingProvider().name() + provider: this.refundsProvider().name() }; const trx = await this.sendFundsToUser(user, args); From b184ccf86a243cbbc08d43cd6d21842a59821979 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Tue, 20 Oct 2020 21:34:32 +0200 Subject: [PATCH 11/28] Update transaction service tests --- .../payment/tests/transaction-service.test.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/server/src/core/payment/tests/transaction-service.test.ts b/server/src/core/payment/tests/transaction-service.test.ts index d1722ebb..795978a5 100644 --- a/server/src/core/payment/tests/transaction-service.test.ts +++ b/server/src/core/payment/tests/transaction-service.test.ts @@ -11,7 +11,8 @@ const COLLECTION = 'transactions'; describe('TransactionService tests', () => { const dbUtils = createDbUtils(DB, COLLECTION); let paymentProviders: PaymentProviderRegistry; - let testPaymentProvider: PaymentProvider; + let testPaymentProvider1: PaymentProvider; + let testPaymentProvider2: PaymentProvider; beforeAll(async () => { await dbUtils.setupDb(); @@ -27,17 +28,27 @@ describe('TransactionService tests', () => { beforeEach(() => { paymentProviders = new PaymentProviders(); - testPaymentProvider = { - name: () => 'testPaymentProvider', + testPaymentProvider1 = { + name: () => 'testPaymentProvider1', + requestPaymentFromUser: jest.fn(), + handlePaymentNotification: jest.fn(), + getTransaction: jest.fn(), + sendFundsToUser: jest.fn() + }; + + testPaymentProvider2 = { + name: () => 'testPaymentProvider2', requestPaymentFromUser: jest.fn(), handlePaymentNotification: jest.fn(), getTransaction: jest.fn(), sendFundsToUser: jest.fn() }; - paymentProviders.register(testPaymentProvider); - paymentProviders.setPreferredForSending(testPaymentProvider.name()); - paymentProviders.setPreferredForReceiving(testPaymentProvider.name()); + paymentProviders.register(testPaymentProvider1); + paymentProviders.register(testPaymentProvider2); + paymentProviders.setPreferredForSending(testPaymentProvider1.name()); + paymentProviders.setPreferredForReceiving(testPaymentProvider2.name()); + paymentProviders.setPreferredForRefunds(testPaymentProvider1.name()); }); function createService() { @@ -68,7 +79,7 @@ describe('TransactionService tests', () => { describe('initiateRefund', () => { test('creates transaction and sends balance to user', async () => { const providerTransactionId = 'providerTx1' - testPaymentProvider.sendFundsToUser = jest.fn().mockResolvedValue({ providerTransactionId, status: 'pending' }); + testPaymentProvider1.sendFundsToUser = jest.fn().mockResolvedValue({ providerTransactionId, status: 'pending' }); const service = createService(); const res = await service.initiateRefund({ @@ -90,7 +101,7 @@ describe('TransactionService tests', () => { expect(res.toExternal).toBe(true); expect(res.to).toBe(''); expect(res.providerTransactionId).toBe(providerTransactionId); - expect(res.provider).toBe('testPaymentProvider'); + expect(res.provider).toBe('testPaymentProvider1'); expect(res.status).toBe('pending'); expect(res.type).toBe('refund'); From 72c39a0d1fab5c6d50bfc9c049710f996ada7776 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Wed, 21 Oct 2020 21:24:35 +0200 Subject: [PATCH 12/28] Implement flutterwave payment provider method sendFundsToUser --- .../payment/flutterwave-payment-provider.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/server/src/core/payment/flutterwave-payment-provider.ts b/server/src/core/payment/flutterwave-payment-provider.ts index 97abebc8..21b602b4 100644 --- a/server/src/core/payment/flutterwave-payment-provider.ts +++ b/server/src/core/payment/flutterwave-payment-provider.ts @@ -1,4 +1,4 @@ -import * as axios from 'axios'; +import * as axios from 'axios'; import { PaymentProvider, PaymentRequestResult, ProviderTransactionInfo, SendFundsResult, TransactionStatus, Transaction } from './types'; import { User } from '../user'; import { generateId } from '../util'; @@ -161,8 +161,30 @@ export class FlutterwavePaymentProvider implements PaymentProvider { throw createFlutterwaveApiError(e.response.data && e.response.data.message || e.message); } } - sendFundsToUser(user: User, amount: number, metadata: any): Promise { - throw new Error('Method not implemented.'); + async sendFundsToUser(user: User, amount: number, metadata: any): Promise { + const transferArgs = { + account_bank: 'MPS', + account_number: user.phone, + amount, + narration: 'New transfer', + currency: 'KES', + reference: generateId(), + beneficiary_name: user.name + } + try { + const url = getUrl(`/transfers`); + const res = await axios.default.post(url, transferArgs, { headers: { Authorization: `Bearer ${this.args.secretKey}`}}); + + const { data, status } = res.data; + + return { + providerTransactionId: data.id, + status: status as TransactionStatus, + } + } + catch(e) { + throw createFlutterwaveApiError(e.response.data && e.response.data.message || e.message); + } } } From fa4c75150f6e35f5ba7d7d1fe8e72d24123815e3 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Wed, 21 Oct 2020 21:49:34 +0200 Subject: [PATCH 13/28] Use args.provider in method Transactions.sendsFundsToUser to fetch the right provider --- server/src/core/payment/transaction-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/core/payment/transaction-service.ts b/server/src/core/payment/transaction-service.ts index 0561c94e..0ca48d23 100644 --- a/server/src/core/payment/transaction-service.ts +++ b/server/src/core/payment/transaction-service.ts @@ -293,7 +293,7 @@ export class Transactions implements TransactionService { private async sendFundsToUser(user: User, args: TransactionCreateArgs): Promise { let trx: Transaction; - const provider = this.sendingProvider(); + const provider = this.provider(args.provider); const modifiedArgs = { ...args }; modifiedArgs.provider = provider.name(); modifiedArgs.status = 'pending'; From a6f2c0fbfbf9683441104bbf37c95933fd5f98aa Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Thu, 22 Oct 2020 20:07:30 +0200 Subject: [PATCH 14/28] Address PR comments --- server/src/core/payment/flutterwave-payment-provider.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/core/payment/flutterwave-payment-provider.ts b/server/src/core/payment/flutterwave-payment-provider.ts index 21b602b4..182302f6 100644 --- a/server/src/core/payment/flutterwave-payment-provider.ts +++ b/server/src/core/payment/flutterwave-payment-provider.ts @@ -164,13 +164,14 @@ export class FlutterwavePaymentProvider implements PaymentProvider { async sendFundsToUser(user: User, amount: number, metadata: any): Promise { const transferArgs = { account_bank: 'MPS', - account_number: user.phone, + account_number: `0${user.phone.substring(3)}`, amount, - narration: 'New transfer', + narration: 'Social Relief transfer', currency: 'KES', reference: generateId(), beneficiary_name: user.name - } + }; + try { const url = getUrl(`/transfers`); const res = await axios.default.post(url, transferArgs, { headers: { Authorization: `Bearer ${this.args.secretKey}`}}); From 910518515af67dd9adacb3ee708a4f4f1e1721e4 Mon Sep 17 00:00:00 2001 From: Jason Mahirwe Date: Fri, 23 Oct 2020 21:36:30 +0200 Subject: [PATCH 15/28] Prefill donate modal with donation details for anonymous user --- .../components/donate-anonymously-modal.vue | 15 ++++++++++++++- webapp/src/store/actions.ts | 5 ++++- webapp/src/store/index.ts | 1 + webapp/src/store/mutations.ts | 8 +++++++- webapp/src/types.ts | 1 + webapp/src/views/home/home.vue | 19 ++++++++++++++----- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/donate-anonymously-modal.vue b/webapp/src/components/donate-anonymously-modal.vue index 0098a188..428adfad 100644 --- a/webapp/src/components/donate-anonymously-modal.vue +++ b/webapp/src/components/donate-anonymously-modal.vue @@ -8,6 +8,7 @@ hide-footer no-stacking @hidden="hideDialog()" + @shown="showDialog()" content-class="rounded" >