Skip to content

Commit

Permalink
Functions: Fix first payout email function (#1023)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Jan 24, 2025
1 parent b8afaca commit 22dc5c2
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 18 deletions.
4 changes: 2 additions & 2 deletions admin/src/views/NextSurveysView.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Box, Button, Link, Popover, Typography } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { Recipient } from '@socialincome/shared/src/types/recipient';
import { Survey, SurveyStatus, getSurveyUrl } from '@socialincome/shared/src/types/survey';
import { getSurveyUrl, Survey, SurveyStatus } from '@socialincome/shared/src/types/survey';
import { toDate, toDateTime } from '@socialincome/shared/src/utils/date';
import {
QueryDocumentSnapshot,
and,
collectionGroup,
getDoc,
Expand All @@ -13,6 +12,7 @@ import {
or,
orderBy,
query,
QueryDocumentSnapshot,
where,
} from 'firebase/firestore';
import { StringPropertyPreview } from 'firecms';
Expand Down
22 changes: 13 additions & 9 deletions functions/src/functions/cron/first-payout-email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
FirstPayoutEmailTemplateData,
SendgridMailClient,
} from '../../../../../shared/src/sendgrid/SendgridMailClient';
import { CONTRIBUTION_FIRESTORE_PATH, Contribution } from '../../../../../shared/src/types/contribution';
import { Contribution, CONTRIBUTION_FIRESTORE_PATH } from '../../../../../shared/src/types/contribution';
import { LanguageCode } from '../../../../../shared/src/types/language';
import { USER_FIRESTORE_PATH, User } from '../../../../../shared/src/types/user';
import { User, USER_FIRESTORE_PATH } from '../../../../../shared/src/types/user';
import { toDateTime } from '../../../../../shared/src/utils/date';

export const getFirstPayoutEmailReceivers = async (
const getFirstPayoutEmailReceivers = async (
firestoreAdmin: FirestoreAdmin,
from: DateTime,
to: DateTime,
Expand Down Expand Up @@ -65,23 +65,21 @@ export const getFirstPayoutEmailReceivers = async (
).flat();
};

// Run on the 16th of every month at 15:00 UTC
export default onSchedule({ schedule: '0 15 16 * *', memory: '2GiB' }, async () => {
let message: string = '';
export async function sendFirstPayoutEmail() {
let message = '';
const sendgridClient = new SendgridMailClient(process.env.SENDGRID_API_KEY!);
try {
const firestoreAdmin = new FirestoreAdmin();
const now = DateTime.now();
const fromDate = DateTime.fromObject({ year: now.year, month: now.month - 1, day: 16, hour: 0 }, { zone: 'utc' });
const toDate = DateTime.fromObject({ year: now.year, month: now.month, day: 16, hour: 0 }, { zone: 'utc' });
const fromDate = now.minus({ months: 1 }).set({ day: 16, hour: 0, minute: 0, second: 0, millisecond: 0 });
const toDate = now.set({ day: 16, hour: 0, minute: 0, second: 0, millisecond: 0 });
const firstPayoutEmailReceivers = await getFirstPayoutEmailReceivers(firestoreAdmin, fromDate, toDate);

await Promise.all(
firstPayoutEmailReceivers.map(async ({ email, language, templateData }) => {
await sendgridClient.sendFirstPayoutEmail(email, language, templateData);
}),
);

message = `Successfully sent first payout emails to ${firstPayoutEmailReceivers.length} users`;
logger.info(message);
} catch (error) {
Expand All @@ -95,4 +93,10 @@ export default onSchedule({ schedule: '0 15 16 * *', memory: '2GiB' }, async ()
text: message,
});
}
}

// Run on the 16th of every month at 15:00 UTC
export default onSchedule({ schedule: '0 15 16 * *', memory: '2GiB' }, async () => {
await sendFirstPayoutEmail();
logger.info('First payout email cron job finished');
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Change } from 'firebase-functions';
import { isEqual } from 'lodash';
import { DateTime } from 'luxon';
import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin';

import { toFirebaseAdminTimestamp } from '../../../../../shared/src/firebase/admin/utils';

/**
Expand All @@ -16,6 +17,7 @@ export class FirestoreAuditor {
constructor() {
this.firestoreAdmin = new FirestoreAdmin();
}

/**
* Takes care of
* - updating the last_updated_at to now field in the document which got changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { AuthAdmin } from '../../../../../../shared/src/firebase/admin/AuthAdmin
import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin';
import { toFirebaseAdminTimestamp } from '../../../../../../shared/src/firebase/admin/utils';
import {
RECIPIENT_FIRESTORE_PATH,
Recipient,
RECIPIENT_FIRESTORE_PATH,
RecipientMainLanguage,
RecipientProgramStatus,
} from '../../../../../../shared/src/types/recipient';
import {
SURVEY_FIRETORE_PATH,
recipientSurveys,
Survey,
SURVEY_FIRETORE_PATH,
SurveyQuestionnaire,
SurveyStatus,
recipientSurveys,
} from '../../../../../../shared/src/types/survey';
import { rndString } from '../../../../../../shared/src/utils/crypto';
import { toDateTime } from '../../../../../../shared/src/utils/date';
Expand Down
2 changes: 1 addition & 1 deletion functions/src/lib/PostfinancePaymentsFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
StatusKey,
} from '../../../shared/src/types/contribution';
import { Currency } from '../../../shared/src/types/currency';
import { USER_FIRESTORE_PATH, User } from '../../../shared/src/types/user';
import { User, USER_FIRESTORE_PATH } from '../../../shared/src/types/user';
import {
POSTFINANCE_FTP_HOST,
POSTFINANCE_FTP_PORT,
Expand Down
4 changes: 2 additions & 2 deletions shared/src/stripe/StripeEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
StripeContribution,
} from '../types/contribution';
import { CountryCode } from '../types/country';
import { Currency, bestGuessCurrency } from '../types/currency';
import { USER_FIRESTORE_PATH, User, splitName } from '../types/user';
import { bestGuessCurrency, Currency } from '../types/currency';
import { splitName, User, USER_FIRESTORE_PATH } from '../types/user';

export class StripeEventHandler {
readonly stripe: Stripe;
Expand Down
113 changes: 113 additions & 0 deletions shared/src/utils/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Timestamp } from 'firebase/firestore';
import { DateTime } from 'luxon';
import { toFirebaseAdminTimestamp } from '../firebase/admin/utils';
import { daysUntilTs, getMonthId, getMonthIDs, toDate, toDateTime } from './date';

describe('getMonthId', () => {
test('formats single digit month with leading zero', () => {
expect(getMonthId(2023, 5)).toBe('2023-05');
});

test('formats double digit month correctly', () => {
expect(getMonthId(2023, 12)).toBe('2023-12');
});
});

describe('getMonthIDs', () => {
test('returns correct number of months', () => {
const date = new Date('2023-05-15');
const result = getMonthIDs(date, 3);
expect(result).toHaveLength(3);
});

test('handles year rollover correctly', () => {
const date = new Date('2023-02-15');
const result = getMonthIDs(date, 3);
expect(result).toEqual(['2023-02', '2023-01', '2022-12']);
});

test('returns empty array for last_n = 0', () => {
const date = new Date('2023-05-15');
const result = getMonthIDs(date, 0);
expect(result).toEqual([]);
});
});

describe('toDateTime', () => {
test('converts Date to DateTime', () => {
const date = new Date('2023-05-15T12:00:00Z');
const result = toDateTime(date);
expect(result.isValid).toBe(true);
expect(result.toISO()).toBe('2023-05-15T12:00:00.000Z');
});

test('converts number (milliseconds) to DateTime', () => {
const timestamp = 1684147200000; // 2023-05-15T10:40:00Z
const result = toDateTime(timestamp);
expect(result.isValid).toBe(true);
expect(result.toISO()).toBe('2023-05-15T10:40:00.000Z');
});

test('converts Timestamp to DateTime', () => {
const timestamp = Timestamp.fromDate(new Date('2023-05-15T10:40:00Z'));
const result = toDateTime(timestamp);
expect(result.isValid).toBe(true);
expect(result.toISO()).toBe('2023-05-15T10:40:00.000Z');
});

test('respects timezone parameter', () => {
const date = new Date('2023-05-15T12:00:00Z');
const result = toDateTime(date, 'America/New_York');
expect(result.zoneName).toBe('America/New_York');
});
});

describe('toDate', () => {
test('converts DateTime to Date', () => {
const dateTime = DateTime.fromISO('2023-05-15T12:00:00Z');
const result = toDate(dateTime);
expect(result instanceof Date).toBe(true);
expect(result.toISOString()).toBe('2023-05-15T12:00:00.000Z');
});
});

describe('daysUntilTs', () => {
beforeEach(() => {
// Mock current date to make tests deterministic
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-05-15T12:00:00Z'));
});

afterEach(() => {
jest.useRealTimers();
});

test('calculates positive days for future date', () => {
const futureDate = new Date('2023-05-20T12:00:00Z');
expect(daysUntilTs(futureDate)).toBe(5);
});

test('calculates negative days for past date', () => {
const pastDate = new Date('2023-05-10T12:00:00Z');
expect(daysUntilTs(pastDate)).toBe(-5);
});

test('returns 0 for same day', () => {
const sameDate = new Date('2023-05-15T12:00:00Z');
expect(daysUntilTs(sameDate)).toBe(0);
});
});

describe('toFirebaseAdminTimestamp', () => {
test('converts Date to Timestamp', () => {
const date = new Date('2023-05-15T12:00:00Z');
const result = toFirebaseAdminTimestamp(date);
expect(result.toDate().toISOString()).toBe('2023-05-15T12:00:00.000Z');
});

test('converts DateTime to Timestamp', () => {
const dateTime = DateTime.fromISO('2023-05-15T12:00:00Z');
const result = toFirebaseAdminTimestamp(dateTime);
expect(result.toDate().toISOString()).toBe('2023-05-15T12:00:00.000Z');
});
});
2 changes: 1 addition & 1 deletion shared/src/utils/stats/ContributionStatsCalculator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FirestoreAdmin } from '../../firebase/admin/FirestoreAdmin';
import { getOrInitializeFirebaseAdmin } from '../../firebase/admin/app';
import { toFirebaseAdminTimestamp } from '../../firebase/admin/utils';
import { CONTRIBUTION_FIRESTORE_PATH, ContributionSourceKey, StatusKey } from '../../types/contribution';
import { USER_FIRESTORE_PATH, User } from '../../types/user';
import { User, USER_FIRESTORE_PATH } from '../../types/user';
import { ContributionStatsCalculator } from './ContributionStatsCalculator';

const projectId = 'contribution-stats-calculator-test';
Expand Down

0 comments on commit 22dc5c2

Please sign in to comment.