Skip to content

Commit

Permalink
refactor(general): update expenses collection (#698)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Jan 19, 2024
1 parent 91dc333 commit 74308bb
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 67 deletions.
4 changes: 2 additions & 2 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'firecms';
import { adminsCollection } from './collections/Admins';
import { buildContributionsCollection } from './collections/Contributions';
import { operationalExpensesCollection } from './collections/OperationalExpenses';
import { expensesCollection } from './collections/Expenses';
import { buildPartnerOrganisationsCollection } from './collections/PartnerOrganisations';
import { usersCollection } from './collections/Users';
import { buildRecipientsCollection } from './collections/recipients/Recipients';
Expand Down Expand Up @@ -50,7 +50,7 @@ export default function App() {
buildPartnerOrganisationsCollection(),
buildSurveysCollection({ collectionGroup: true }),
adminsCollection,
operationalExpensesCollection,
expensesCollection,
usersCollection,
buildContributionsCollection({ collectionGroup: true }),
];
Expand Down
41 changes: 41 additions & 0 deletions admin/src/collections/Expenses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { buildProperties } from 'firecms';
import { Expense, EXPENSES_FIRESTORE_PATH, ExpenseType } from '../../../shared/src/types/expense';
import { buildAuditedCollection } from './shared';

export const expensesCollection = buildAuditedCollection<Expense>({
name: 'Expenses',
group: 'Finances',
path: EXPENSES_FIRESTORE_PATH,
textSearchEnabled: false,
icon: 'LocalAtm',
description: 'Project expenses displayed on transparency page',
initialSort: ['year', 'desc'],
permissions: {
edit: true,
create: true,
delete: true,
},
properties: buildProperties<Expense>({
type: {
dataType: 'string',
name: 'Type',
enumValues: [
{ id: ExpenseType.DeliveryFees, label: 'Delivery fees' },
{ id: ExpenseType.DonationFees, label: 'Donation fees' },
{ id: ExpenseType.ExchangeRateFluctuation, label: 'Exchange rate fluctuation' },
],
validation: { required: true },
},
year: {
dataType: 'number',
name: 'Year',
enumValues: { 2020: '2020', 2021: '2021', 2022: '2022', 2023: '2023', 2024: '2024', 2025: '2025' },
validation: { required: true },
},
amount_chf: {
dataType: 'number',
name: 'Amount Chf',
validation: { required: true },
},
}),
});
47 changes: 0 additions & 47 deletions admin/src/collections/OperationalExpenses.ts

This file was deleted.

2 changes: 1 addition & 1 deletion seed/auth_export/accounts.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions seed/firebase-export-metadata.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"version": "12.9.1",
"version": "13.0.2",
"firestore": {
"version": "1.18.2",
"path": "firestore_export",
"metadata_file": "firestore_export/firestore_export.overall_export_metadata"
},
"auth": {
"version": "12.9.1",
"version": "13.0.2",
"path": "auth_export"
},
"storage": {
"version": "12.9.1",
"version": "13.0.2",
"path": "storage_export"
}
}
Binary file not shown.
Binary file modified seed/firestore_export/all_namespaces/all_kinds/output-0
Binary file not shown.
Binary file modified seed/firestore_export/firestore_export.overall_export_metadata
Binary file not shown.
13 changes: 13 additions & 0 deletions shared/src/types/expense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const EXPENSES_FIRESTORE_PATH = 'expenses';

export enum ExpenseType {
DonationFees = 'donation_fees',
DeliveryFees = 'delivery_fees',
ExchangeRateFluctuation = 'exchange_rate_fluctuation',
}

export type Expense = {
type: ExpenseType;
year: number;
amount_chf: number;
};
8 changes: 0 additions & 8 deletions shared/src/types/operational-expense.ts

This file was deleted.

3 changes: 2 additions & 1 deletion shared/src/utils/exchangeRates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FirestoreAdmin } from '../firebase/admin/FirestoreAdmin';
import { Currency } from '../types/currency';
import { EXCHANGE_RATES_PATH, ExchangeRatesEntry } from '../types/exchange-rates';

export const getLatestExchangeRate = async (firestoreAdmin: FirestoreAdmin, currency: string): Promise<number> => {
export const getLatestExchangeRate = async (firestoreAdmin: FirestoreAdmin, currency: Currency): Promise<number> => {
if (currency === 'CHF') return 1.0;
const exchangeRates = await firestoreAdmin
.collection<ExchangeRatesEntry>(EXCHANGE_RATES_PATH)
Expand Down
3 changes: 2 additions & 1 deletion shared/src/utils/stats/ContributionStatsCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'lodash';
import { DateTime } from 'luxon';
import { FirestoreAdmin } from '../../firebase/admin/FirestoreAdmin';
import { Contribution, CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '../../types/contribution';
import { Currency } from '../../types/currency';
import { User, USER_FIRESTORE_PATH } from '../../types/user';
import { getLatestExchangeRate } from '../exchangeRates';
import { cumulativeSum, StatsEntry } from './utils';
Expand Down Expand Up @@ -58,7 +59,7 @@ export class ContributionStatsCalculator {
*/
static async build(
firestoreAdmin: FirestoreAdmin,
currency: string,
currency: Currency,
contributionFilter = (c: Contribution) => c.status === StatusKey.SUCCEEDED,
): Promise<ContributionStatsCalculator> {
const exchangeRate = await getLatestExchangeRate(firestoreAdmin, currency);
Expand Down
56 changes: 56 additions & 0 deletions shared/src/utils/stats/ExpensesStatsCalculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import _ from 'lodash';
import { FirestoreAdmin } from '../../firebase/admin/FirestoreAdmin';
import { Currency } from '../../types/currency';
import { Expense, EXPENSES_FIRESTORE_PATH, ExpenseType } from '../../types/expense';
import { getLatestExchangeRate } from '../exchangeRates';

type ExpenseStatsEntry = Expense & {
amount: number;
};

export class ExpensesStatsCalculator {
private expenses: _.Collection<ExpenseStatsEntry>;

private constructor(expenses: _.Collection<ExpenseStatsEntry>) {
this.expenses = expenses;
}

public static async build(firestoreAdmin: FirestoreAdmin, currency: Currency): Promise<ExpensesStatsCalculator> {
let expenses = await firestoreAdmin.collection<Expense>(EXPENSES_FIRESTORE_PATH).get();
const exchangeRate = await getLatestExchangeRate(firestoreAdmin, currency);

return new ExpensesStatsCalculator(
_(expenses.docs).map((doc) => {
const data = doc.data();
return { amount: exchangeRate * data.amount_chf, ...data };
}),
);
}

public totalExpensesBy(group: 'type' | 'year'): { [type in string]?: number } {
return this.expenses
.groupBy(group)
.map((expenses, group) => ({ [group]: _.sumBy(expenses, 'amount') }))
.reduce((a, b) => ({ ...a, ...b }), {});
}

public totalExpensesByYearAndType(): { [year: string]: { [type in ExpenseType]: number } } {
return this.expenses
.groupBy('year')
.map((expenses, year) => ({
[year]: _(expenses)
.groupBy('type')
.map((objs, type) => ({ [type]: _.sumBy(objs, 'amount') }))
.reduce((a, b) => ({ ...a, ...b }), {}),
}))
.reduce((a, b) => ({ ...a, ...b }), {});
}

public allStats() {
return {
totalExpensesByYear: this.totalExpensesBy('year'),
totalExpensesByType: this.totalExpensesBy('type'),
totalExpensesByYearAndType: this.totalExpensesByYearAndType(),
};
}
}
3 changes: 2 additions & 1 deletion shared/src/utils/stats/PaymentStatsCalculator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';
import { FirestoreAdmin } from '../../firebase/admin/FirestoreAdmin';
import { Currency } from '../../types/currency';
import { Payment, PAYMENT_FIRESTORE_PATH } from '../../types/payment';
import { RECIPIENT_FIRESTORE_PATH } from '../../types/recipient';
import { User } from '../../types/user';
Expand Down Expand Up @@ -44,7 +45,7 @@ export class PaymentStatsCalculator {
* Calls the firestore database to retrieve the payments and constructs the
* PaymentStatsCalculator with the flattened intermediate data structure.
*/
static async build(firestoreAdmin: FirestoreAdmin, currency: string): Promise<PaymentStatsCalculator> {
static async build(firestoreAdmin: FirestoreAdmin, currency: Currency): Promise<PaymentStatsCalculator> {
const recipients = await firestoreAdmin.collection<User>(RECIPIENT_FIRESTORE_PATH).get();
const payments = (
await Promise.all(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { DefaultPageProps, DefaultParams } from '@/app/[lang]/[region]';
import { CurrencyRedirect } from '@/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect';
import { firestoreAdmin } from '@/firebase-admin';
import { WebsiteCurrency, WebsiteLanguage, WebsiteRegion, websiteCurrencies } from '@/i18n';
import { Currency } from '@socialincome/shared/src/types/currency';
import {
ContributionStats,
ContributionStatsCalculator,
} from '@socialincome/shared/src/utils/stats/ContributionStatsCalculator';
import { ExpensesStatsCalculator } from '@socialincome/shared/src/utils/stats/ExpensesStatsCalculator';
import { PaymentStats, PaymentStatsCalculator } from '@socialincome/shared/src/utils/stats/PaymentStatsCalculator';
import { Section1 } from './section-1';
import { Section2 } from './section-2';
Expand Down Expand Up @@ -37,15 +39,21 @@ export type SectionProps = {
};

export default async function Page({ params }: TransparencyPageProps) {
const getStats = async (currency: string) => {
const getStats = async (currency: Currency) => {
const contributionCalculator = await ContributionStatsCalculator.build(firestoreAdmin, currency);
const contributionStats = contributionCalculator.allStats();
const paymentCalculator = await PaymentStatsCalculator.build(firestoreAdmin, currency);
const paymentStats = paymentCalculator.allStats();
return { contributionStats, paymentStats };

const expensesStatsCalculator = await ExpensesStatsCalculator.build(firestoreAdmin, currency);
const expensesStats = expensesStatsCalculator.allStats();
return { contributionStats, expensesStats, paymentStats };
};
const currency = params.currency.toUpperCase() as WebsiteCurrency;
const { contributionStats, paymentStats } = await getStats(currency);
const { contributionStats, expensesStats, paymentStats } = await getStats(currency);

console.info(JSON.stringify(expensesStats, null, 2));

// TODO: Calculate these costs dynamically
const costs = {
transaction: 8800,
Expand Down

0 comments on commit 74308bb

Please sign in to comment.