diff --git a/server/src/core/app.ts b/server/src/core/app.ts index aece4ded..071908f3 100644 --- a/server/src/core/app.ts +++ b/server/src/core/app.ts @@ -154,8 +154,8 @@ export function loadAppConfigFromEnv(env: { [key: string]: string }): AppConfig distributionPeriodLength: (env.DISTRIBUTION_PERIOD_LENGTH && Number(env.DISTRIBUTION_PERIOD_LENGTH)) || 30, distributionInterval: (env.DISTRIBUTION_INTERVAL) || `0 */2 * * * *`, vettedDistributionInterval: (env.VETTED_DISTRIBUTION_INTERVAL) || `0 */5 * * * *`, - dailyDistributionReportingInterval: (env.DAILY_DISTRIBUTION_REPORTING_INTERVAL) || `0 18 * * * *`, - monthlyDistributionReportingInterval: (env.MONTHLY_DISTRIBUTION_REPORTING_INTERVAL) || `0 6 1 * * *`, + dailyDistributionReportingInterval: (env.DAILY_DISTRIBUTION_REPORTING_INTERVAL) || `0 0 18 * * *`, + monthlyDistributionReportingInterval: (env.MONTHLY_DISTRIBUTION_REPORTING_INTERVAL) || `0 0 10 1 * *`, 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/core/distribution-report/distribution-report-service.ts b/server/src/core/distribution-report/distribution-report-service.ts index 75df80e1..c579414b 100644 --- a/server/src/core/distribution-report/distribution-report-service.ts +++ b/server/src/core/distribution-report/distribution-report-service.ts @@ -13,7 +13,7 @@ export const REPORT_TYPE_DAILY = 'daily'; export const REPORT_TYPE_MONTHLY = 'monthly'; const DEFAULT_DONATION_AMOUNT = 2000; -export interface DISTRIBUTION_REPORT_ENHANCED { +export interface EnhancedDistributionReport { donorId: string, donor: User, beneficiaryIds: string[], @@ -116,7 +116,7 @@ export class DistributionReports implements DistributionReportService { const amount = report.totalDistributedAmountFromDonor < DEFAULT_DONATION_AMOUNT ? DEFAULT_DONATION_AMOUNT : report.totalDistributedAmountFromDonor; const donateLink = await this.args.links.getUserDonateLink(donor, amount); - const reportEnhanced: DISTRIBUTION_REPORT_ENHANCED = { + const reportEnhanced: EnhancedDistributionReport = { ...report, donorId: report.donor, donor, diff --git a/server/src/core/distribution-report/index.ts b/server/src/core/distribution-report/index.ts index 774d6ccc..c11ca3f3 100644 --- a/server/src/core/distribution-report/index.ts +++ b/server/src/core/distribution-report/index.ts @@ -1,2 +1,2 @@ export * from './types'; -export { DistributionReports, REPORT_TYPE_DAILY, REPORT_TYPE_MONTHLY, DISTRIBUTION_REPORT_ENHANCED } from './distribution-report-service'; \ No newline at end of file +export { DistributionReports, REPORT_TYPE_DAILY, REPORT_TYPE_MONTHLY, EnhancedDistributionReport } from './distribution-report-service'; \ No newline at end of file diff --git a/server/src/core/link-generator/bitly-link-shortener.ts b/server/src/core/link-generator/bitly-link-shortener.ts index 2793b8c8..ded06816 100644 --- a/server/src/core/link-generator/bitly-link-shortener.ts +++ b/server/src/core/link-generator/bitly-link-shortener.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { LinkShortener } from './types'; import { rethrowIfAppError, createBitlyApiError, createLinkShorteningFailedError } from '../error'; export interface DeepLink { @@ -34,10 +35,6 @@ export interface BitlyArgs { apiLink: string; } -export interface LinkShortener { - shortenLink(link: string): Promise; -} - export class BitlyLinkShortener implements LinkShortener { private apiKey: string; private apiLink: string; diff --git a/server/src/core/link-generator/index.ts b/server/src/core/link-generator/index.ts index 4a63ac82..911b1446 100644 --- a/server/src/core/link-generator/index.ts +++ b/server/src/core/link-generator/index.ts @@ -1,2 +1,3 @@ +export * from './types'; export { Links } from './link-generator-service'; -export { BitlyLinkShortener, LinkShortener } from './bitly-link-shortener'; \ No newline at end of file +export { BitlyLinkShortener } from './bitly-link-shortener'; diff --git a/server/src/core/link-generator/link-generator-service.ts b/server/src/core/link-generator/link-generator-service.ts index 953cc1fc..1bdb632d 100644 --- a/server/src/core/link-generator/link-generator-service.ts +++ b/server/src/core/link-generator/link-generator-service.ts @@ -1,8 +1,8 @@ -import { LinkGeneratorService, DonateLinkArgs } from "./types"; -import { LinkShortener } from './bitly-link-shortener'; +import { LinkGeneratorService, DonateLinkArgs, LinkShortener } from './types'; import { User } from '../user'; import * as queryString from 'querystring'; import { rethrowIfAppError } from '../error'; +import { removePhoneCountryCode } from '../util'; interface LinksArgs { baseUrl: string; @@ -18,21 +18,43 @@ export class Links implements LinkGeneratorService { async getUserDonateLink(user: User, amount: number, shorten: boolean = true): Promise { const { name, email, phone } = user; + const shortPhone = removePhoneCountryCode(phone); + try { - const longLink = this.getDonateLink({ name, email, phone, amount }); - if (shorten) { - const shortLink = await this.args.shortener.shortenLink(longLink); - return shortLink; - } + const longLink = await this.getDonateLink({ name, email, phone: shortPhone, amount }, shorten); return longLink; } catch (e) { rethrowIfAppError(e); } } - getDonateLink(args: DonateLinkArgs): string { - const { name, email, phone, amount } = args; - const nameQuery = queryString.stringify({ n: name}); // n=first%20last - return `${this.args.baseUrl}?donate=true&${nameQuery}&e=${email}&p=${phone}&a=${amount}`; + + async getDonateLink(args: DonateLinkArgs, shorten: boolean = true): Promise { + const query: any = {}; + + if (args.name) { + query.n = args.name; + } + + if (args.email) { + query.e = args.email; + } + + if (args.phone) { + query.p = args.phone; + } + + if (args.amount) { + query.a = args.amount; + } + + const encodedQuery = queryString.stringify(query); + const link = `${this.args.baseUrl}?donate=1&${encodedQuery}`; + + if (shorten) { + return this.args.shortener.shortenLink(link); + } + + return link; } } \ No newline at end of file diff --git a/server/src/core/link-generator/tests/link-generator-service.test.ts b/server/src/core/link-generator/tests/link-generator-service.test.ts new file mode 100644 index 00000000..be3fe5a4 --- /dev/null +++ b/server/src/core/link-generator/tests/link-generator-service.test.ts @@ -0,0 +1,74 @@ +import { Links } from '../link-generator-service'; +import { User } from '../../user'; +import { LinkShortener } from '../types'; + +describe('LinkGeneratorService tests', () => { + const baseUrl = 'https://test.com'; + let shortener: LinkShortener; + + function getService() { + return new Links({ shortener, baseUrl: baseUrl }); + } + + beforeEach(() => { + shortener = { + shortenLink: (link) => Promise.resolve(`shorten_${link}`) + }; + }); + + + describe('getDonateLink', () => { + test('should generate shortened donate link with arguments url encoded', async () => { + const service = getService(); + const link = await service.getDonateLink({ + name: 'John Doe', + phone: '711000222', + email: 'test@mailer.com', + amount: 3000 + }); + + expect(link).toEqual('shorten_https://test.com?donate=1&n=John%20Doe&e=test%40mailer.com&p=711000222&a=3000'); + }); + + test('should omit missing values from generated link', async () => { + const service = getService(); + const link = await service.getDonateLink({ + name: 'John Doe', + email: 'test@mailer.com' + }); + + expect(link).toEqual('shorten_https://test.com?donate=1&n=John%20Doe&e=test%40mailer.com'); + }); + + test('should not shorten link if shorten option is false', async () => { + const service = getService(); + const link = await service.getDonateLink({ + name: 'John Doe', + phone: '711000222', + email: 'test@mailer.com', + amount: 3000 + }, false); + + expect(link).toEqual('https://test.com?donate=1&n=John%20Doe&e=test%40mailer.com&p=711000222&a=3000'); + }); + }); + + describe('getUserDonateLink', () => { + test('should generate a shortened link for the specified user', async () => { + const service = getService(); + const link = await service.getUserDonateLink({ + _id: 'u1', + name: 'John Doe', + email: 'test@mailer.com', + phone: '254711000222', + addedBy: '', + donors: [], + roles: [], + createdAt: new Date(), + updatedAt: new Date() + }, 3000); + + expect(link).toEqual('shorten_https://test.com?donate=1&n=John%20Doe&e=test%40mailer.com&p=711000222&a=3000'); + }); + }); +}); diff --git a/server/src/core/link-generator/types.ts b/server/src/core/link-generator/types.ts index f210e17e..1163fcec 100644 --- a/server/src/core/link-generator/types.ts +++ b/server/src/core/link-generator/types.ts @@ -1,13 +1,44 @@ import { User } from '../user'; export interface DonateLinkArgs { - name: string, - email: string, - phone: string, - amount: number + /** + * name of the donor + */ + name?: string, + /** + * email of the donor + */ + email?: string, + /** + * phone number of the donor (should not include country code) + */ + phone?: string, + /** + * The amount to donate + */ + amount?: number, + /** + * Whether to shorten the link using a LinkShortener + */ + shorten?: boolean } export interface LinkGeneratorService { - getUserDonateLink(user: User, amount: number): Promise; - getDonateLink(args: DonateLinkArgs): string; -} \ No newline at end of file + /** + * Generates a donation link for the specified user + * @param user the user to generate a link for + * @param amount the amount to donate + * @param shorten whether to shorten the link (defaults to true) + */ + getUserDonateLink(user: User, amount: number, shorten?: boolean): Promise; + /** + * Generates a donation link with the specified arguments + * @param args + * @param shorten whether to shorten link (defaults to true) + */ + getDonateLink(args: DonateLinkArgs, shorten?: boolean): Promise; +} + +export interface LinkShortener { + shortenLink(link: string): Promise; +} diff --git a/server/src/core/message/message.ts b/server/src/core/message/message.ts index 52978bb8..575752f5 100644 --- a/server/src/core/message/message.ts +++ b/server/src/core/message/message.ts @@ -1,4 +1,4 @@ -import { DISTRIBUTION_REPORT_ENHANCED } from '../distribution-report'; +import { EnhancedDistributionReport } from '../distribution-report'; import { DistributionReport, MonthlyDistributionReport } from '../payment'; import { User } from '../user'; import { extractFirstName } from '../util'; @@ -21,11 +21,11 @@ export function createDailyDistributionReportEmailMessage(report: DistributionRe

`; } -export function createMonthlyDistributionReportSmsMessageForContributingDonor(donorsReport: DISTRIBUTION_REPORT_ENHANCED, systemWideReport: MonthlyDistributionReport, donateLink: string): string { +export function createMonthlyDistributionReportSmsMessageForContributingDonor(donorsReport: EnhancedDistributionReport, systemWideReport: MonthlyDistributionReport, donateLink: string): string { return `Hello ${extractFirstName(donorsReport.donor.name)}, last month a total of Ksh ${systemWideReport.totalDonations} was donated by ${systemWideReport.distributionReports.length} Social Relief donors to ${systemWideReport.totalBeneficiaries} beneficiaries. Ksh ${donorsReport.totalDistributedAmountFromDonor} was sent from your donations to ${beneficiariesAndAmountReceived(donorsReport.beneficiaries, donorsReport.receivedAmount, 'sms')}. Thank you for your contribution. To donate again, click ${donateLink}`; } -export function createMonthlyDistributionReportEmailMessageForContributingDonor(donorsReport: DISTRIBUTION_REPORT_ENHANCED, systemWideReport: MonthlyDistributionReport, donateLink: string): string { +export function createMonthlyDistributionReportEmailMessageForContributingDonor(donorsReport: EnhancedDistributionReport, systemWideReport: MonthlyDistributionReport, donateLink: string): string { return `

Hello ${extractFirstName(donorsReport.donor.name)},

Last month a total of Ksh ${systemWideReport.totalDonations} was donated by ${systemWideReport.distributionReports.length} Social Relief donors to ${systemWideReport.totalBeneficiaries} beneficiaries. Ksh ${donorsReport.totalDistributedAmountFromDonor} was sent from your donations to:
diff --git a/server/src/core/util/index.ts b/server/src/core/util/index.ts index 7eeacaa2..4614f3fa 100644 --- a/server/src/core/util/index.ts +++ b/server/src/core/util/index.ts @@ -72,4 +72,14 @@ export async function verifyGoogleIdToken(token: string): Promise 711222333 + * @param phone + */ +export function removePhoneCountryCode(phone: string) { + return phone.substr(3); } \ No newline at end of file diff --git a/server/src/worker/monthly-distribution-report-worker.ts b/server/src/worker/monthly-distribution-report-worker.ts index 631cfaaf..784642bd 100644 --- a/server/src/worker/monthly-distribution-report-worker.ts +++ b/server/src/worker/monthly-distribution-report-worker.ts @@ -1,6 +1,5 @@ import { App } from '../core'; import { CronJob } from 'cron'; -import { REPORT_TYPE_MONTHLY } from '../core/distribution-report'; export function runMonthlyDistributionReportingWorker(app: App, interval: string) { const job = new CronJob(interval, async () => {