-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(admin): payout forecast (#855)
- Loading branch information
1 parent
1d56859
commit 162a96c
Showing
15 changed files
with
394 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Button } from '@mui/material'; | ||
import { DEFAULT_REGION } from '@socialincome/shared/src/firebase'; | ||
import { getFunctions, httpsCallable } from 'firebase/functions'; | ||
import { useSnackbarController } from 'firecms'; | ||
|
||
export function CreatePaymentForecastAction() { | ||
const snackbarController = useSnackbarController(); | ||
|
||
const createPaymentForecast = () => { | ||
const runPaymentForecastTask = httpsCallable(getFunctions(undefined, DEFAULT_REGION), 'runPaymentForecastTask'); | ||
runPaymentForecastTask() | ||
.then((result) => { | ||
snackbarController.open({ type: 'success', message: 'Payment forecast updated successfully' }); | ||
}) | ||
.catch((reason: Error) => { | ||
snackbarController.open({ type: 'error', message: reason.message }); | ||
}); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<Button onClick={() => createPaymentForecast()} color="primary"> | ||
Refresh Forecast | ||
</Button> | ||
</div> | ||
); | ||
} | ||
|
||
function setIsFunctionRunning(arg0: boolean) { | ||
throw new Error('Function not implemented.'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { PAYMENT_FORECAST_FIRESTORE_PATH, PaymentForecastEntry } from '@socialincome/shared/src/types/payment-forecast'; | ||
import { buildProperties, useSnackbarController } from 'firecms'; | ||
import { EntityCollection } from 'firecms/dist/types/collections'; | ||
import { CreatePaymentForecastAction } from '../actions/CreatePaymentForecastAction'; | ||
import { buildAuditedCollection } from './shared'; | ||
|
||
export const buildPaymentForecastCollection = () => { | ||
const snackbarController = useSnackbarController(); | ||
|
||
const collection: EntityCollection<PaymentForecastEntry> = { | ||
name: 'Payout Forecast', | ||
group: 'Finances', | ||
path: PAYMENT_FORECAST_FIRESTORE_PATH, | ||
textSearchEnabled: false, | ||
initialSort: ['order', 'asc'], | ||
icon: 'LocalConvenienceStore', | ||
description: 'Projected payout forecast for the next six months', | ||
Actions: CreatePaymentForecastAction, | ||
permissions: { | ||
edit: false, | ||
create: false, | ||
delete: false, | ||
}, | ||
properties: buildProperties<PaymentForecastEntry>({ | ||
order: { | ||
dataType: 'number', | ||
name: 'Order', | ||
validation: { required: true }, | ||
}, | ||
month: { | ||
dataType: 'string', | ||
name: 'Month', | ||
validation: { required: true }, | ||
}, | ||
numberOfRecipients: { | ||
dataType: 'number', | ||
name: 'Number of Recipients', | ||
validation: { required: true }, | ||
}, | ||
amount_usd: { | ||
dataType: 'number', | ||
name: 'Total Amount USD', | ||
validation: { required: true }, | ||
}, | ||
amount_sle: { | ||
dataType: 'number', | ||
name: 'Total Amount SLE', | ||
validation: { required: true }, | ||
}, | ||
}), | ||
}; | ||
return buildAuditedCollection<PaymentForecastEntry>(collection); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { onCall } from 'firebase-functions/v2/https'; | ||
import { DateTime } from 'luxon'; | ||
import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; | ||
import { PAYMENT_AMOUNT_SLE } from '../../../../../shared/src/types/payment'; | ||
import { PAYMENT_FORECAST_FIRESTORE_PATH } from '../../../../../shared/src/types/payment-forecast'; | ||
import { | ||
calcFinalPaymentDate, | ||
calcPaymentsLeft, | ||
RECIPIENT_FIRESTORE_PATH, | ||
RecipientProgramStatus, | ||
} from '../../../../../shared/src/types/recipient'; | ||
import { getLatestExchangeRate } from '../../../../../shared/src/utils/exchangeRates'; | ||
|
||
function prepareNextSixMonths(): Map<string, number> { | ||
const nextSixMonths: Map<string, number> = new Map(); | ||
const now: DateTime = DateTime.now(); | ||
for (let i = 1; i < 7; ++i) { | ||
const nextMonthDateTime = now.plus({ months: i }); | ||
nextSixMonths.set(nextMonthDateTime.toFormat('LLLL yyyy'), 0); | ||
} | ||
return nextSixMonths; | ||
} | ||
|
||
function addRecipient(nextSixMonths: Map<string, number>, paymentsLeft: number) { | ||
nextSixMonths.forEach((value, key) => { | ||
if (paymentsLeft > 0) { | ||
nextSixMonths.set(key, ++value); | ||
paymentsLeft -= 1; | ||
} | ||
}); | ||
} | ||
|
||
async function calculateUSDAmount(firestoreAdmin: FirestoreAdmin): Promise<number> { | ||
const exchangeRateUSD = await getLatestExchangeRate(firestoreAdmin, 'USD'); | ||
const exchangeRateSLE = await getLatestExchangeRate(firestoreAdmin, 'SLE'); | ||
const monthlyAllowanceInUSD = (PAYMENT_AMOUNT_SLE / exchangeRateSLE) * exchangeRateUSD; | ||
return parseFloat(monthlyAllowanceInUSD.toFixed(2)); | ||
} | ||
|
||
async function deleteAllDocuments(firestoreAdmin: FirestoreAdmin): Promise<void> { | ||
const batch = firestoreAdmin.firestore.batch(); | ||
const snapshot = await firestoreAdmin.firestore.collection(PAYMENT_FORECAST_FIRESTORE_PATH).get(); | ||
snapshot.forEach((doc) => { | ||
batch.delete(doc.ref); | ||
}); | ||
await batch.commit(); | ||
} | ||
|
||
async function fillNextSixMonths( | ||
firestoreAdmin: FirestoreAdmin, | ||
nextSixMonthsList: Map<string, number>, | ||
): Promise<void> { | ||
const batch = firestoreAdmin.firestore.batch(); | ||
const monthlyAllowanceInUSD = await calculateUSDAmount(firestoreAdmin); | ||
let count = 1; | ||
nextSixMonthsList.forEach((value, key) => { | ||
const newDocRef = firestoreAdmin.firestore.collection(PAYMENT_FORECAST_FIRESTORE_PATH).doc(); | ||
batch.set(newDocRef, { | ||
order: count, | ||
month: key, | ||
numberOfRecipients: value, | ||
amount_usd: value * monthlyAllowanceInUSD, | ||
amount_sle: value * PAYMENT_AMOUNT_SLE, | ||
}); | ||
++count; | ||
}); | ||
await batch.commit(); | ||
} | ||
|
||
export default onCall<undefined, Promise<string>>({ memory: '2GiB' }, async (request) => { | ||
const firestoreAdmin = new FirestoreAdmin(); | ||
try { | ||
await firestoreAdmin.assertGlobalAdmin(request.auth?.token?.email); | ||
const nextSixMonthsList = prepareNextSixMonths(); | ||
const recipientsSnapshot = await firestoreAdmin | ||
.collection(RECIPIENT_FIRESTORE_PATH) | ||
.where('progr_status', 'in', [RecipientProgramStatus.Active, RecipientProgramStatus.Designated]) | ||
.get(); | ||
recipientsSnapshot.docs.map((doc) => { | ||
const recipient = doc.data(); | ||
if (recipient.si_start_date && recipient.progr_status === RecipientProgramStatus.Active) { | ||
addRecipient( | ||
nextSixMonthsList, | ||
calcPaymentsLeft( | ||
calcFinalPaymentDate(DateTime.fromSeconds(recipient.si_start_date._seconds, { zone: 'utc' })), | ||
), | ||
); | ||
} else if (recipient.progr_status === RecipientProgramStatus.Designated) { | ||
addRecipient(nextSixMonthsList, 6); | ||
} | ||
}); | ||
|
||
await deleteAllDocuments(firestoreAdmin); | ||
await fillNextSixMonths(firestoreAdmin, nextSixMonthsList); | ||
|
||
return 'Function executed successfully.'; | ||
} catch (error) { | ||
console.error('Error during function execution:', error); | ||
throw new Error('An error occurred while processing your request.'); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.