Skip to content

Commit

Permalink
feature(functions): improve payment process and improve local functio…
Browse files Browse the repository at this point in the history
…ns testing setup
  • Loading branch information
mkue committed Jul 7, 2023
1 parent bcb7be1 commit c815c5b
Show file tree
Hide file tree
Showing 62 changed files with 1,839 additions and 1,582 deletions.
52 changes: 26 additions & 26 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,42 @@
},
"devDependencies": {
"@firebase/rules-unit-testing": "^2.0.7",
"@jest/globals": "^29.5.0",
"@playwright/test": "^1.31.2",
"@types/jest": "^29.5.0",
"@types/luxon": "^3.2.0",
"@types/node": "^16.18.16",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"css-loader": "^6.7.3",
"jest": "^29.5.0",
"@jest/globals": "^29.6.0",
"@playwright/test": "^1.35.1",
"@types/jest": "^29.5.2",
"@types/luxon": "^3.3.0",
"@types/node": "^20.3.3",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.1",
"css-loader": "^6.8.1",
"jest": "^29.6.0",
"process": "^0.11.10",
"style-loader": "^3.3.2",
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"style-loader": "^3.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"typescript": "^5.1.6",
"vite": "^4.3.9"
},
"dependencies": {
"firecms": "2.0.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
"@mui/lab": "^5.0.0-alpha.111",
"@mui/material": "^5.11.13",
"@mui/x-date-pickers": "^5.0.20",
"@mui/x-data-grid": "^6.3.0",
"firecms": "2.0.4",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.13.7",
"@mui/lab": "^5.0.0-alpha.135",
"@mui/material": "^5.13.7",
"@mui/x-date-pickers": "^6.9.1",
"@mui/x-data-grid": "^6.9.1",
"@socialincome/shared": "^1.0.0",
"algoliasearch": "^4.15.0",
"firebase": "^9.18.0",
"algoliasearch": "^4.18.0",
"firebase": "^9.23.0",
"lodash": "^4.17.21",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.4.4",
"react-router-dom": "^6.9.0",
"react-router": "^6.14.1",
"react-router-dom": "^6.14.1",
"react-scripts": "^5.0.1"
},
"browserslist": {
Expand Down
47 changes: 31 additions & 16 deletions admin/src/actions/PaymentProcessAction.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Box, Button, CircularProgress, Modal, Tooltip, Typography } from '@mui/material';
import { PaymentProcessTaskType } from '@socialincome/shared/src/types';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { downloadStringAsFile } from '@socialincome/shared/src/utils/html';
import { getFunctions, httpsCallable } from 'firebase/functions';
import { useSnackbarController } from 'firecms';
import { DateTime } from 'luxon';
import { useState } from 'react';
import { PaymentProcessTaskProps } from '../../../functions/src/webhooks/admin/payment-process/PaymentTaskProcessor';
import { PaymentProcessTaskType, toPaymentDate } from '../../..//shared/src/types';
import { PaymentProcessProps } from '../../../functions/src/webhooks/admin/payment-process';

const STYLE = {
position: 'absolute' as 'absolute',
const BOX_STYLE = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
Expand All @@ -17,38 +18,38 @@ const STYLE = {
p: 4,
};

const createNewPaymentsDescription = 'Set payment status to paid and create new payments for the upcoming month.';

export function PaymentProcessAction() {
const snackbarController = useSnackbarController();
const [isOpen, setIsOpen] = useState(false);
const [confirmCreateNewPayments, setConfirmCreateNewPayments] = useState(false);
const [isFunctionRunning, setIsFunctionRunning] = useState(false);
const [paymentDate, setPaymentDate] = useState<DateTime>(toPaymentDate(DateTime.now()));

const handleOpen = () => setIsOpen(true);
const handleClose = () => {
setIsOpen(false);
};

const triggerFirebaseFunction = (task: PaymentProcessTaskType) => {
const runAdminPaymentProcessTask = httpsCallable<PaymentProcessTaskProps, string>(
getFunctions(),
'runAdminPaymentProcessTask'
);
const runPaymentProcessTask = httpsCallable<PaymentProcessProps, string>(getFunctions(), 'runPaymentProcessTask');
setIsFunctionRunning(true);
runAdminPaymentProcessTask({
runPaymentProcessTask({
type: task,
timestamp: DateTime.now().toSeconds(),
timestamp: paymentDate.toSeconds(),
})
.then((result) => {
if (task === PaymentProcessTaskType.GetRegistrationCSV || task === PaymentProcessTaskType.GetPaymentCSV) {
const fileName = `11866_${new Date().toLocaleDateString('sv')}.csv`; // 11866_YYYY-MM-DD.csv
const fileName = `11866_${paymentDate.toFormat('yyyy_MM_dd')}.csv`;
downloadStringAsFile(result.data, fileName);
} else {
snackbarController.open({ type: 'success', message: result.data });
}
setConfirmCreateNewPayments(false);
})
.catch(() => {
snackbarController.open({ type: 'error', message: 'Oops, something went wrong.' });
.catch((reason: Error) => {
snackbarController.open({ type: 'error', message: reason.message });
})
.finally(() => setIsFunctionRunning(false));
};
Expand All @@ -59,7 +60,7 @@ export function PaymentProcessAction() {
Payment Process
</Button>
<Modal open={isOpen} onClose={handleClose}>
<Box sx={STYLE}>
<Box sx={BOX_STYLE}>
{isFunctionRunning ? (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<CircularProgress />
Expand All @@ -76,6 +77,20 @@ export function PaymentProcessAction() {
<Typography variant="h5" textAlign="center">
Payment Process
</Typography>
<DatePicker
label="Payment month"
views={['month', 'year']}
value={paymentDate.toJSDate()}
onChange={(value) => {
if (value) setPaymentDate(toPaymentDate(DateTime.fromJSDate(value)));
}}
/>
<Button
variant="outlined"
onClick={() => triggerFirebaseFunction(PaymentProcessTaskType.UpdateRecipients)}
>
Update Recipients
</Button>
<Button
variant="outlined"
onClick={() => triggerFirebaseFunction(PaymentProcessTaskType.GetRegistrationCSV)}
Expand All @@ -86,7 +101,7 @@ export function PaymentProcessAction() {
Payments CSV
</Button>
{!confirmCreateNewPayments && (
<Tooltip title="This will create new payments for all active recipients for this month and the next months if the payments don't exist yet.">
<Tooltip title={createNewPaymentsDescription}>
<Button variant="outlined" onClick={() => setConfirmCreateNewPayments(true)}>
Create new payments
</Button>
Expand All @@ -95,7 +110,7 @@ export function PaymentProcessAction() {
{confirmCreateNewPayments && (
<Button
variant="contained"
onClick={() => triggerFirebaseFunction(PaymentProcessTaskType.CreateNewPayments)}
onClick={() => triggerFirebaseFunction(PaymentProcessTaskType.CreatePayments)}
>
Confirm
</Button>
Expand Down
13 changes: 7 additions & 6 deletions admin/src/collections/recipients/Recipients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { Chip, Tooltip } from '@mui/material';
import {
RECIPIENT_FIRESTORE_PATH,
Recipient,
calcLastPaymentDate,
calcFinalPaymentDate,
calcPaymentsLeft,
} from '@socialincome/shared/src/types';
import { DateTime } from 'luxon';
import { messagesCollection } from '../Messages';
import { paymentsCollection } from '../Payments';
import { buildAuditedCollection } from '../shared';
Expand Down Expand Up @@ -35,13 +36,13 @@ export const PaymentsLeft: AdditionalFieldDelegate<Partial<Recipient>> = {
id: 'payments_left',
name: 'Payments Left',
Builder: ({ entity }) => {
const lastPaymentDate = entity.values.si_start_date
? calcLastPaymentDate(entity.values.si_start_date as Date)
const finalPaymentDate = entity.values.si_start_date
? calcFinalPaymentDate(DateTime.fromJSDate(entity.values.si_start_date as Date))
: undefined;
const paymentsLeft = lastPaymentDate ? calcPaymentsLeft(lastPaymentDate) : undefined;
if (paymentsLeft && lastPaymentDate) {
const paymentsLeft = finalPaymentDate ? calcPaymentsLeft(finalPaymentDate) : undefined;
if (paymentsLeft && finalPaymentDate) {
return (
<Tooltip title={'Last Payment Date ' + lastPaymentDate.toFormat('dd/MM/yyyy')}>
<Tooltip title={'Last Payment Date ' + finalPaymentDate.toFormat('dd/MM/yyyy')}>
<Chip
size={'small'}
color={paymentsLeft < 0 ? 'info' : paymentsLeft <= 1 ? 'error' : paymentsLeft <= 3 ? 'warning' : 'success'}
Expand Down
4 changes: 2 additions & 2 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "tsc && npm run build:sync-files",
"build:sync-files": "rsync -av --delete ../shared/assets dist/shared && rsync -av --delete ../shared/locales dist/shared && rsync -av --delete ../shared/templates dist/shared",
"serve": "npm run build:sync-files && tsc --watch",
"test": "firebase emulators:exec --only firestore,functions,storage --project social-income-staging --config ../firebase.json --import ../seed 'npm run test:app'",
"test": "firebase emulators:exec --only firestore,storage,functions --config ../firebase.json 'npm run test:app'",
"test:app": "jest --forceExit --roots src/",
"test:playwright": "firebase emulators:exec --project social-income-staging --only firestore --config ../firebase.json --import ../seed 'npx playwright install --with-deps && playwright test'",
"test:playwright:update": "firebase emulators:exec --project social-income-staging --only firestore --config ../firebase.json --import ../seed 'npx playwright install --with-deps && playwright test --update-snapshots'"
Expand All @@ -34,7 +34,7 @@
"axios": "^1.3.1",
"dotenv": "^16.1.3",
"firebase-admin": "^11.5.0",
"firebase-functions": "^4.3.1",
"firebase-functions": "^4.4.1",
"handlebars": "^4.7.7",
"handlebars-i18next": "^1.0.3",
"i18next-resources-to-backend": "^1.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test } from '@jest/globals';
import functions from 'firebase-functions-test';
import { FirestoreAdmin } from '../../../shared/src/firebase/admin/FirestoreAdmin';
import { getOrInitializeFirebaseAdmin } from '../../../shared/src/firebase/admin/app';
import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../shared/src/types';
import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin';
import { getOrInitializeFirebaseAdmin } from '../../../../shared/src/firebase/admin/app';
import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../../shared/src/types';
import { ExchangeRateImporter, ExchangeRateResponse } from './ExchangeRateImporter';

describe('importExchangeRates', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,23 @@
import axios from 'axios';
import * as functions from 'firebase-functions';
import { DateTime } from 'luxon';
import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../shared/src/types';
import { EXCHANGE_RATES_API } from '../config';
import { AbstractFirebaseAdmin, FunctionProvider } from '../firebase';
import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin';
import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../../shared/src/types';
import { EXCHANGE_RATES_API } from '../../config';

export type ExchangeRateResponse = {
base: string;
date: string;
rates: ExchangeRates;
};

export class ExchangeRateImporter extends AbstractFirebaseAdmin implements FunctionProvider {
export class ExchangeRateImporter {
static readonly secondsInDay = 60 * 60 * 24;
static readonly startTimestamp = 1583020800; // 2020-03-01 00:00:00
private readonly firestoreAdmin: FirestoreAdmin;

/**
* Function periodically scrapes currency exchange rates and saves them to firebase
*/
getFunction() {
return functions
.runWith({
timeoutSeconds: 540,
})
.pubsub.schedule('0 1 * * *')
.onRun(async () => {
const existingExchangeRates = await this.getAllExchangeRates();
for (
let timestamp = ExchangeRateImporter.startTimestamp;
timestamp <= Date.now() / 1000;
timestamp += ExchangeRateImporter.secondsInDay
) {
if (!existingExchangeRates.has(timestamp)) {
try {
await this.fetchAndStoreExchangeRates(DateTime.fromSeconds(timestamp));
} catch (error) {
functions.logger.error(`Could not ingest exchange rate`, error);
}
}
}
});
constructor() {
this.firestoreAdmin = new FirestoreAdmin();
}

getAllExchangeRates = async (): Promise<Map<number, ExchangeRates>> => {
Expand Down Expand Up @@ -86,7 +64,7 @@ export class ExchangeRateImporter extends AbstractFirebaseAdmin implements Funct
.set(exchangeRates);
};

private fetchAndStoreExchangeRates = async (dt: DateTime): Promise<ExchangeRateResponse> => {
fetchAndStoreExchangeRates = async (dt: DateTime): Promise<ExchangeRateResponse> => {
const rates = await this.fetchExchangeRates(dt);
await this.storeExchangeRates(rates);
functions.logger.info('Ingested exchange rates');
Expand Down
29 changes: 29 additions & 0 deletions functions/src/cron/exchange-rate-import/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as functions from 'firebase-functions';
import { DateTime } from 'luxon';
import { ExchangeRateImporter } from './ExchangeRateImporter';

/**
* Function periodically scrapes currency exchange rates and saves them to firebase
*/
export default functions
.runWith({
timeoutSeconds: 540,
})
.pubsub.schedule('0 1 * * *')
.onRun(async () => {
const exchangeRateImporter = new ExchangeRateImporter();
const existingExchangeRates = await exchangeRateImporter.getAllExchangeRates();
for (
let timestamp = ExchangeRateImporter.startTimestamp;
timestamp <= Date.now() / 1000;
timestamp += ExchangeRateImporter.secondsInDay
) {
if (!existingExchangeRates.has(timestamp)) {
try {
await exchangeRateImporter.fetchAndStoreExchangeRates(DateTime.fromSeconds(timestamp));
} catch (error) {
functions.logger.error(`Could not ingest exchange rate`, error);
}
}
}
});
8 changes: 4 additions & 4 deletions functions/src/cron/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExchangeRateImporter } from './ExchangeRateImporter';
import { PostFinanceImporter } from './PostFinanceImporter';
import importExchangeRatesFunction from './exchange-rate-import';
import importBalanceMailFunction from './postfinance-import';

export const importBalanceMail = new PostFinanceImporter().getFunction();
export const importExchangeRates = new ExchangeRateImporter().getFunction();
export const importBalanceMail = importBalanceMailFunction;
export const importExchangeRates = importExchangeRatesFunction;
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { describe, expect, test } from '@jest/globals';
import functions from 'firebase-functions-test';
import { FirestoreAdmin } from '../../../shared/src/firebase/admin/FirestoreAdmin';
import { getOrInitializeFirebaseAdmin } from '../../../shared/src/firebase/admin/app';
import { BANK_BALANCE_FIRESTORE_PATH, BankBalance, getIdFromBankBalance } from '../../../shared/src/types';
import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin';
import { getOrInitializeFirebaseAdmin } from '../../../../shared/src/firebase/admin/app';
import { BANK_BALANCE_FIRESTORE_PATH, BankBalance, getIdFromBankBalance } from '../../../../shared/src/types';
import { PostFinanceImporter } from './PostFinanceImporter';

describe('importPostfinanceBalance', () => {
const projectId = 'test' + new Date().getTime();
const testEnv = functions({ projectId: projectId });
const firestoreAdmin = new FirestoreAdmin(getOrInitializeFirebaseAdmin({ projectId: projectId }));
const postfinanceImporter = new PostFinanceImporter({ firestoreAdmin });
const postfinanceImporter = new PostFinanceImporter();

beforeEach(async () => {
await testEnv.firestore.clearFirestoreData({ projectId: projectId });
Expand Down
Loading

0 comments on commit c815c5b

Please sign in to comment.