diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index cd1895b16..68a768567 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -89,9 +89,6 @@ jobs: echo SENDGRID_API_KEY=${{ secrets.SENDGRID_API_KEY }} >> .env echo STRIPE_API_READ_KEY=${{ (inputs.project == 'social-income-prod' && secrets.STRIPE_API_READ_KEY) || secrets.STRIPE_API_READ_KEY_STAGING }} >> .env echo STRIPE_WEBHOOK_SECRET=${{ (inputs.project == 'social-income-prod' && secrets.STRIPE_WEBHOOK_SECRET) || secrets.STRIPE_WEBHOOK_SECRET_STAGING }} >> .env - echo TWILIO_SENDER_PHONE=${{ secrets.TWILIO_SENDER_PHONE }} >> .env - echo TWILIO_SID=${{ secrets.TWILIO_SID }} >> .env - echo TWILIO_TOKEN=${{ secrets.TWILIO_TOKEN }} >> .env - name: Build functions run: npm run functions:build diff --git a/.github/workflows/functions.yml b/.github/workflows/functions.yml index c0d174003..b297b9ffa 100644 --- a/.github/workflows/functions.yml +++ b/.github/workflows/functions.yml @@ -32,10 +32,7 @@ jobs: uses: ./.github/workflows/actions/init - name: Run tests - env: - TWILIO_SID: ${{ secrets.TWILIO_TEST_SID }} - TWILIO_TOKEN: ${{ secrets.TWILIO_TEST_TOKEN }} - TWILIO_SENDER_PHONE: ${{ secrets.TWILIO_TEST_SENDER_PHONE }} + env: {} run: npm run functions:test # TODO: re-enable diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 79224dba9..8a5548270 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -32,8 +32,5 @@ jobs: uses: ./.github/workflows/actions/init - name: Run tests - env: - TWILIO_SID: ${{ secrets.TWILIO_TEST_SID }} - TWILIO_TOKEN: ${{ secrets.TWILIO_TEST_TOKEN }} - TWILIO_SENDER_PHONE: ${{ secrets.TWILIO_TEST_SENDER_PHONE }} + env: {} run: npm run shared:test diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 26e93baa9..2b07f0bda 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -10,10 +10,10 @@ import { import { adminsCollection } from './collections/Admins'; import { campaignsCollection } from './collections/Campaigns'; import { buildContributionsCollection } from './collections/Contributions'; +import { contributorsCollection } from './collections/Contributors'; import { expensesCollection } from './collections/Expenses'; import { buildPartnerOrganisationsCollection } from './collections/PartnerOrganisations'; import { buildPaymentForecastCollection } from './collections/PaymentForecast'; -import { usersCollection } from './collections/Users'; import { buildRecipientsCollection } from './collections/recipients/Recipients'; import { buildRecipientsPaymentsCollection } from './collections/recipients/RecipientsPayments'; import { buildSurveysCollection } from './collections/surveys/Surveys'; @@ -49,14 +49,14 @@ export default function App() { const collections = [ buildRecipientsCollection(), buildRecipientsPaymentsCollection(), + contributorsCollection, + buildContributionsCollection({ collectionGroup: true }), buildPartnerOrganisationsCollection(), buildSurveysCollection({ collectionGroup: true }), adminsCollection, expensesCollection, buildPaymentForecastCollection(), - usersCollection, campaignsCollection, - buildContributionsCollection({ collectionGroup: true }), ]; const views: CMSView[] = [ diff --git a/admin/src/actions/CreateDonationCertificatesAction.tsx b/admin/src/actions/CreateDonationCertificatesAction.tsx index a64e15b62..9a523e1cf 100644 --- a/admin/src/actions/CreateDonationCertificatesAction.tsx +++ b/admin/src/actions/CreateDonationCertificatesAction.tsx @@ -16,7 +16,7 @@ import { getFunctions, httpsCallable } from 'firebase/functions'; import { CollectionActionsProps, useAuthController, useSnackbarController } from 'firecms'; import _ from 'lodash'; import React from 'react'; -import { CreateDonationCertificatesFunctionProps } from '../../../functions/src/webhooks/admin/donation-certificates'; +import { CreateDonationCertificatesProps } from '../../../functions/src/lib/donation-certificates'; const style = { position: 'absolute' as 'absolute', @@ -44,7 +44,7 @@ export function CreateDonationCertificatesAction({ selectionController }: Collec if (!isGlobalAdmin) return null; const functions = getFunctions(undefined, DEFAULT_REGION); - const createDonationCertificatesFunction = httpsCallable( + const createDonationCertificatesFunction = httpsCallable( functions, 'createDonationCertificates', ); @@ -54,8 +54,7 @@ export function CreateDonationCertificatesAction({ selectionController }: Collec if ((year && selectedEntities?.length > 0) || createAll) { createDonationCertificatesFunction({ year: year, - userIds: selectedEntities, - createAll: createAll, + userIds: selectedEntities.length > 0 ? selectedEntities : undefined, }) .then((result) => { snackbarController.open({ diff --git a/admin/src/actions/InviteWhatsappAction.tsx b/admin/src/actions/InviteWhatsappAction.tsx deleted file mode 100644 index 61547a57c..000000000 --- a/admin/src/actions/InviteWhatsappAction.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Box, Button, Modal, Typography } from '@mui/material'; -import { DEFAULT_REGION } from '@socialincome/shared/src/firebase'; -import { Recipient } from '@socialincome/shared/src/types/recipient'; -import { getFunctions, httpsCallable } from 'firebase/functions'; -import { CollectionActionsProps, useAuthController, useSnackbarController } from 'firecms'; -import React from 'react'; -import { TwilioOutgoingMessageFunctionProps } from '../../../functions/src/webhooks/twilio/TwilioOutgoingMessageHandler'; - -const STYLE = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - p: 4, -}; - -export function InviteWhatsappAction({ selectionController }: CollectionActionsProps) { - const snackbarController = useSnackbarController(); - const isGlobalAdmin = useAuthController().extra?.isGlobalAdmin; - - const [open, setOpen] = React.useState(false); - const [countSelectedEntities, setCountSelectedEntities] = React.useState(0); - - if (!isGlobalAdmin) return null; - - const handleOpen = () => { - setOpen(true); - setCountSelectedEntities(selectionController?.selectedEntities.length); - }; - const handleClose = () => setOpen(false); - - const functions = getFunctions(undefined, DEFAULT_REGION); - const twilioOutgoingMessage = httpsCallable( - functions, - 'twilioOutgoingMessage', - ); - - const onClick = () => { - const selectedEntities = selectionController?.selectedEntities; - if (selectedEntities?.length > 0) { - twilioOutgoingMessage({ - recipients: selectedEntities, - template: 'opt-in', - }) - .then((result) => { - snackbarController.open({ - type: 'success', - message: result.data, - }); - }) - .catch(() => { - snackbarController.open({ - type: 'error', - message: `An error occurred during opt-ins for Whatsapp.`, - }); - }); - } else { - snackbarController.open({ - type: 'error', - message: `Please select a year and entries to generate Donation Certificates.`, - }); - } - }; - - return ( -
- - - - - - Send Whatsapp Invites - - - - - -
- ); -} diff --git a/admin/src/actions/PaymentProcessAction.tsx b/admin/src/actions/PaymentProcessAction.tsx index edf8a2ff6..e7c9db8aa 100644 --- a/admin/src/actions/PaymentProcessAction.tsx +++ b/admin/src/actions/PaymentProcessAction.tsx @@ -8,7 +8,7 @@ import { getFunctions, httpsCallable } from 'firebase/functions'; import { useSnackbarController } from 'firecms'; import { DateTime } from 'luxon'; import { useState } from 'react'; -import { PaymentProcessProps } from '../../../functions/src/webhooks/admin/payment-process'; +import type { PaymentProcessProps } from '../../../functions/src/functions/webhooks/admin/payment-process'; const BOX_STYLE = { position: 'absolute', @@ -113,13 +113,6 @@ export function PaymentProcessAction() { Confirm )} - {' '} diff --git a/admin/src/collections/Users.tsx b/admin/src/collections/Contributors.tsx similarity index 89% rename from admin/src/collections/Users.tsx rename to admin/src/collections/Contributors.tsx index 47361c600..03b4c303a 100644 --- a/admin/src/collections/Users.tsx +++ b/admin/src/collections/Contributors.tsx @@ -1,3 +1,5 @@ +import { COUNTRY_CODES } from '@socialincome/shared/src/types/country'; +import { LANGUAGE_CODES } from '@socialincome/shared/src/types/language'; import { USER_FIRESTORE_PATH, User, UserReferralSource } from '@socialincome/shared/src/types/user'; import { buildProperties } from 'firecms'; import { CreateDonationCertificatesAction } from '../actions/CreateDonationCertificatesAction'; @@ -5,11 +7,10 @@ import { buildContributionsCollection } from './Contributions'; import { donationCertificateCollection } from './DonationCertificate'; import { buildAuditedCollection } from './shared'; -// @ts-ignore -export const usersCollection = buildAuditedCollection({ +export const contributorsCollection = buildAuditedCollection({ path: USER_FIRESTORE_PATH, group: 'Contributors', - icon: 'VolunteerActivism', + icon: 'Person', name: 'Contributors', singularName: 'Contributor', description: 'Lists all contributors', @@ -81,6 +82,9 @@ export const usersCollection = buildAuditedCollection({ name: 'Country', dataType: 'string', validation: { required: true }, + enumValues: { + ...COUNTRY_CODES.map((code) => ({ id: code, label: code })), + }, }, city: { name: 'City', @@ -103,6 +107,7 @@ export const usersCollection = buildAuditedCollection({ language: { name: 'Language', dataType: 'string', + enumValues: { ...LANGUAGE_CODES.map((code) => ({ id: code, label: code })) }, }, currency: { name: 'Currency', diff --git a/admin/src/collections/DonationCertificate.tsx b/admin/src/collections/DonationCertificate.tsx index 283d55205..89523dea4 100644 --- a/admin/src/collections/DonationCertificate.tsx +++ b/admin/src/collections/DonationCertificate.tsx @@ -2,22 +2,9 @@ import { DONATION_CERTIFICATE_FIRESTORE_PATH, DonationCertificate, } from '@socialincome/shared/src/types/donation-certificate'; -import { AdditionalFieldDelegate, buildProperties } from 'firecms'; +import { buildProperties } from 'firecms'; import { buildAuditedCollection } from './shared'; -const DownloadLinkColumn: AdditionalFieldDelegate = { - id: 'download_link', - name: 'Download Link', - Builder: ({ entity }) => { - return ( - - Download - - ); - }, - dependencies: ['url'], -}; - export const donationCertificateCollection = buildAuditedCollection({ name: 'Donation Certificates', group: 'Finances', @@ -25,7 +12,6 @@ export const donationCertificateCollection = buildAuditedCollection({ - url: { - dataType: 'string', - name: 'URL', - disabled: { - hidden: true, - }, - }, country: { dataType: 'string', name: 'Created for country', @@ -50,5 +29,10 @@ export const donationCertificateCollection = buildAuditedCollection>({ - name: 'Messages', - group: 'Messages', - permissions: { - create: false, - delete: false, - edit: false, - }, - path: MESSAGE_FIRESTORE_PATH, - icon: 'SupervisorAccountTwoTone', - description: 'Lists all messages for one recipient or user', - customId: true, - properties: buildProperties>({ - type: { - dataType: 'string', - readOnly: true, - enumValues: [ - { id: 'sms', label: 'SMS' }, - { id: 'email', label: 'Email' }, - { id: 'whatsapp', label: 'Whatsapp' }, - ], - }, - body: { - dataType: 'string', - readOnly: true, - }, - status: { - dataType: 'string', - readOnly: true, - }, - }), -}); diff --git a/admin/src/collections/recipients/Recipients.tsx b/admin/src/collections/recipients/Recipients.tsx index fe3b75f02..f358f4714 100644 --- a/admin/src/collections/recipients/Recipients.tsx +++ b/admin/src/collections/recipients/Recipients.tsx @@ -8,7 +8,6 @@ import { import { toDateTime } from '@socialincome/shared/src/utils/date'; import { AdditionalFieldDelegate, buildProperties } from 'firecms'; import { EntityCollection, PropertiesOrBuilders } from 'firecms/dist/types'; -import { messagesCollection } from '../Messages'; import { paymentsCollection } from '../Payments'; import { buildAuditedCollection } from '../shared'; import { buildSurveysCollection } from '../surveys/Surveys'; @@ -94,7 +93,7 @@ export const buildRecipientsCollection = () => { si_start_date: SIStartDateProperty, test_recipient: TestRecipientProperty, }), - subcollections: [paymentsCollection, buildSurveysCollection(), messagesCollection], + subcollections: [paymentsCollection, buildSurveysCollection()], }; return buildAuditedCollection>(collection); }; diff --git a/functions/.env.sample b/functions/.env.sample index edf3aa772..0e6c7f051 100644 --- a/functions/.env.sample +++ b/functions/.env.sample @@ -3,8 +3,3 @@ POSTFINANCE_FTP_HOST=ftp.postfinance.example POSTFINANCE_FTP_PORT=21 POSTFINANCE_FTP_USER=example POSTFINANCE_FTP_RSA_PRIVATE_KEY_BASE64="CKSLI...." - -# To work with the Twilio API locally, configure the test credentials from Twilio in your .env file -TWILIO_SID=ACXXXXXXXXXXXXXXXXXXXX -TWILIO_TOKEN=yyyyyyyyyyyyyyyyyyyyy -TWILIO_SENDER_PHONE=+15005550006 diff --git a/functions/package.json b/functions/package.json index e756985e6..24736700c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -48,7 +48,6 @@ "ssh2-sftp-client": "^11.0.0", "stripe": "^17.1.0", "tmp-promise": "^3.0.3", - "twilio": "^5.3.3", "xpath": "^0.0.34" } } diff --git a/functions/src/config.ts b/functions/src/config.ts index a3b5aa93a..45da48337 100644 --- a/functions/src/config.ts +++ b/functions/src/config.ts @@ -14,8 +14,4 @@ export const POSTFINANCE_FTP_HOST = process.env.POSTFINANCE_FTP_HOST!; export const POSTFINANCE_FTP_PORT = process.env.POSTFINANCE_FTP_PORT!; export const POSTFINANCE_FTP_USER = process.env.POSTFINANCE_FTP_USER!; -export const TWILIO_SID = process.env.TWILIO_SID!; -export const TWILIO_TOKEN = process.env.TWILIO_TOKEN!; -export const TWILIO_SENDER_PHONE = process.env.TWILIO_SENDER_PHONE!; - export const EXCHANGE_RATES_API = process.env.EXCHANGE_RATES_API!; diff --git a/functions/src/firebase.ts b/functions/src/firebase.ts index 8f3283c41..55bf6f2fc 100644 --- a/functions/src/firebase.ts +++ b/functions/src/firebase.ts @@ -199,7 +199,7 @@ export async function initializeGlobalTestData(projectId?: string) { await firestoreAdmin.doc(RECIPIENT_FIRESTORE_PATH).set({ gender: 'male', - organisation: 'organisations/aurora', + organisation: firestoreAdmin.doc('organisations', 'aurora'), progr_status: RecipientProgramStatus.Former, birth_date: new Date(2001, 0, 1), first_name: 'Test1', @@ -213,7 +213,7 @@ export async function initializeGlobalTestData(projectId?: string) { await firestoreAdmin.doc(RECIPIENT_FIRESTORE_PATH).set({ gender: 'female', - organisation: 'organisations/aurora', + organisation: firestoreAdmin.doc('organisations', 'aurora'), progr_status: RecipientProgramStatus.Active, birth_date: new Date(2001, 0, 2), first_name: 'Test2', @@ -227,7 +227,7 @@ export async function initializeGlobalTestData(projectId?: string) { await firestoreAdmin.doc(RECIPIENT_FIRESTORE_PATH).set({ gender: 'female', - organisation: 'organisations/aurora', + organisation: firestoreAdmin.doc('organisations', 'aurora'), progr_status: RecipientProgramStatus.Active, birth_date: new Date(2001, 0, 3), first_name: 'Test3', @@ -241,7 +241,7 @@ export async function initializeGlobalTestData(projectId?: string) { await firestoreAdmin.doc(RECIPIENT_FIRESTORE_PATH, '3RqjohcNgUXaejFC7av8').set({ gender: 'female', - organisation: 'organisations/aurora', + organisation: firestoreAdmin.doc('organisations', 'aurora'), progr_status: RecipientProgramStatus.Designated, birth_date: new Date(2001, 0, 4), first_name: 'Test4', @@ -255,7 +255,7 @@ export async function initializeGlobalTestData(projectId?: string) { await firestoreAdmin.doc(RECIPIENT_FIRESTORE_PATH).set({ gender: 'male', - organisation: 'organisations/aurora', + organisation: firestoreAdmin.doc('organisations', 'aurora'), progr_status: RecipientProgramStatus.Waitlisted, birth_date: new Date(2001, 0, 5), first_name: 'Test5', diff --git a/functions/src/functions/cron/donation-certificates/index.ts b/functions/src/functions/cron/donation-certificates/index.ts new file mode 100644 index 000000000..e2714d4c6 --- /dev/null +++ b/functions/src/functions/cron/donation-certificates/index.ts @@ -0,0 +1,10 @@ +import { logger } from 'firebase-functions'; +import { onSchedule } from 'firebase-functions/v2/scheduler'; +import { DateTime } from 'luxon'; +import { createDonationCertificates } from '../../../lib/donation-certificates'; + +export default onSchedule({ schedule: '0 0 2 1 *', memory: '4GiB' }, async () => { + const now = DateTime.now(); + const msg = await createDonationCertificates({ year: now.year }); + logger.info(msg); +}); diff --git a/functions/src/cron/exchange-rate-import/ExchangeRateImporter.test.ts b/functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.test.ts similarity index 83% rename from functions/src/cron/exchange-rate-import/ExchangeRateImporter.test.ts rename to functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.test.ts index 9d9efdf72..4d06fdefd 100644 --- a/functions/src/cron/exchange-rate-import/ExchangeRateImporter.test.ts +++ b/functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.test.ts @@ -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/exchange-rates'; +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/exchange-rates'; import { ExchangeRateImporter, ExchangeRateResponse } from './ExchangeRateImporter'; describe('importExchangeRates', () => { diff --git a/functions/src/cron/exchange-rate-import/ExchangeRateImporter.ts b/functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.ts similarity index 92% rename from functions/src/cron/exchange-rate-import/ExchangeRateImporter.ts rename to functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.ts index 5c15d4688..770af57b5 100644 --- a/functions/src/cron/exchange-rate-import/ExchangeRateImporter.ts +++ b/functions/src/functions/cron/exchange-rate-import/ExchangeRateImporter.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import { logger } from 'firebase-functions'; import { DateTime } from 'luxon'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../../shared/src/types/exchange-rates'; -import { EXCHANGE_RATES_API } from '../../config'; +import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { EXCHANGE_RATES_PATH, ExchangeRates, ExchangeRatesEntry } from '../../../../../shared/src/types/exchange-rates'; +import { EXCHANGE_RATES_API } from '../../../config'; export type ExchangeRateResponse = { base: string; diff --git a/functions/src/cron/exchange-rate-import/index.ts b/functions/src/functions/cron/exchange-rate-import/index.ts similarity index 100% rename from functions/src/cron/exchange-rate-import/index.ts rename to functions/src/functions/cron/exchange-rate-import/index.ts diff --git a/functions/src/cron/first-payout-email/index.ts b/functions/src/functions/cron/first-payout-email/index.ts similarity index 84% rename from functions/src/cron/first-payout-email/index.ts rename to functions/src/functions/cron/first-payout-email/index.ts index a8b09029b..21e491d58 100644 --- a/functions/src/cron/first-payout-email/index.ts +++ b/functions/src/functions/cron/first-payout-email/index.ts @@ -1,13 +1,16 @@ import { logger } from 'firebase-functions'; import { onSchedule } from 'firebase-functions/v2/scheduler'; import { DateTime } from 'luxon'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { toFirebaseAdminTimestamp } from '../../../../shared/src/firebase/admin/utils'; -import { FirstPayoutEmailTemplateData, SendgridMailClient } from '../../../../shared/src/sendgrid/SendgridMailClient'; -import { CONTRIBUTION_FIRESTORE_PATH, Contribution } from '../../../../shared/src/types/contribution'; -import { LanguageCode } from '../../../../shared/src/types/language'; -import { USER_FIRESTORE_PATH, User } from '../../../../shared/src/types/user'; -import { toDateTime } from '../../../../shared/src/utils/date'; +import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { toFirebaseAdminTimestamp } from '../../../../../shared/src/firebase/admin/utils'; +import { + FirstPayoutEmailTemplateData, + SendgridMailClient, +} from '../../../../../shared/src/sendgrid/SendgridMailClient'; +import { CONTRIBUTION_FIRESTORE_PATH, Contribution } from '../../../../../shared/src/types/contribution'; +import { LanguageCode } from '../../../../../shared/src/types/language'; +import { USER_FIRESTORE_PATH, User } from '../../../../../shared/src/types/user'; +import { toDateTime } from '../../../../../shared/src/utils/date'; export const getFirstPayoutEmailReceivers = async ( firestoreAdmin: FirestoreAdmin, diff --git a/functions/src/cron/index.ts b/functions/src/functions/cron/index.ts similarity index 100% rename from functions/src/cron/index.ts rename to functions/src/functions/cron/index.ts diff --git a/functions/src/cron/postfinance-payments-files-import/index.ts b/functions/src/functions/cron/postfinance-payments-files-import/index.ts similarity index 62% rename from functions/src/cron/postfinance-payments-files-import/index.ts rename to functions/src/functions/cron/postfinance-payments-files-import/index.ts index 24af305f2..1936c6ee2 100644 --- a/functions/src/cron/postfinance-payments-files-import/index.ts +++ b/functions/src/functions/cron/postfinance-payments-files-import/index.ts @@ -1,6 +1,6 @@ import { onSchedule } from 'firebase-functions/v2/scheduler'; -import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../config'; -import { PostfinancePaymentsFileHandler } from '../../utils/PostfinancePaymentsFileHandler'; +import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../../config'; +import { PostfinancePaymentsFileHandler } from '../../../lib/PostfinancePaymentsFileHandler'; export default onSchedule('0 * * * *', async () => { const paymentsFileHandler = new PostfinancePaymentsFileHandler(POSTFINANCE_PAYMENTS_FILES_BUCKET); diff --git a/functions/src/firestore/firestore-auditor/FirestoreAuditor.test.ts b/functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.test.ts similarity index 97% rename from functions/src/firestore/firestore-auditor/FirestoreAuditor.test.ts rename to functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.test.ts index 938ee4876..0284795b9 100644 --- a/functions/src/firestore/firestore-auditor/FirestoreAuditor.test.ts +++ b/functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, test } from '@jest/globals'; import functionsTest from 'firebase-functions-test'; import auditFirestore from '.'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { getOrInitializeFirebaseAdmin } from '../../../../shared/src/firebase/admin/app'; +import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { getOrInitializeFirebaseAdmin } from '../../../../../shared/src/firebase/admin/app'; describe('FirestoreAuditor', () => { const projectId = 'auditor' + new Date().getTime(); diff --git a/functions/src/firestore/firestore-auditor/FirestoreAuditor.ts b/functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.ts similarity index 93% rename from functions/src/firestore/firestore-auditor/FirestoreAuditor.ts rename to functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.ts index aa98be5f0..379114159 100644 --- a/functions/src/firestore/firestore-auditor/FirestoreAuditor.ts +++ b/functions/src/functions/firestore/firestore-auditor/FirestoreAuditor.ts @@ -2,8 +2,8 @@ import { DocumentSnapshot } from '@google-cloud/firestore'; 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'; +import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { toFirebaseAdminTimestamp } from '../../../../../shared/src/firebase/admin/utils'; /** * Watches write updates in collection and subcollections and inserts the previous record value into a diff --git a/functions/src/firestore/firestore-auditor/index.ts b/functions/src/functions/firestore/firestore-auditor/index.ts similarity index 100% rename from functions/src/firestore/firestore-auditor/index.ts rename to functions/src/functions/firestore/firestore-auditor/index.ts diff --git a/functions/src/firestore/index.ts b/functions/src/functions/firestore/index.ts similarity index 100% rename from functions/src/firestore/index.ts rename to functions/src/functions/firestore/index.ts diff --git a/functions/src/storage/index.ts b/functions/src/functions/storage/index.ts similarity index 100% rename from functions/src/storage/index.ts rename to functions/src/functions/storage/index.ts diff --git a/functions/src/storage/postfinance-payments-files/index.ts b/functions/src/functions/storage/postfinance-payments-files/index.ts similarity index 67% rename from functions/src/storage/postfinance-payments-files/index.ts rename to functions/src/functions/storage/postfinance-payments-files/index.ts index b094ea2d4..f9e026b27 100644 --- a/functions/src/storage/postfinance-payments-files/index.ts +++ b/functions/src/functions/storage/postfinance-payments-files/index.ts @@ -1,6 +1,6 @@ import { onObjectFinalized } from 'firebase-functions/v2/storage'; -import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../config'; -import { PostfinancePaymentsFileHandler } from '../../utils/PostfinancePaymentsFileHandler'; +import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../../config'; +import { PostfinancePaymentsFileHandler } from '../../../lib/PostfinancePaymentsFileHandler'; export default onObjectFinalized({ bucket: POSTFINANCE_PAYMENTS_FILES_BUCKET }, async (event) => { const paymentsFileHandler = new PostfinancePaymentsFileHandler(POSTFINANCE_PAYMENTS_FILES_BUCKET); diff --git a/functions/src/functions/webhooks/admin/donation-certificates/index.ts b/functions/src/functions/webhooks/admin/donation-certificates/index.ts new file mode 100644 index 000000000..03848db91 --- /dev/null +++ b/functions/src/functions/webhooks/admin/donation-certificates/index.ts @@ -0,0 +1,10 @@ +import { onCall } from 'firebase-functions/v2/https'; +import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { createDonationCertificates, CreateDonationCertificatesProps } from '../../../../lib/donation-certificates'; + +export default onCall>({ memory: '4GiB' }, async (request) => { + const firestoreAdmin = new FirestoreAdmin(); + await firestoreAdmin.assertGlobalAdmin(request.auth?.token?.email); + + return await createDonationCertificates(request.data); +}); diff --git a/functions/src/webhooks/admin/payment-forecast/index.ts b/functions/src/functions/webhooks/admin/payment-forecast/index.ts similarity index 90% rename from functions/src/webhooks/admin/payment-forecast/index.ts rename to functions/src/functions/webhooks/admin/payment-forecast/index.ts index 5553d804b..5d373914b 100644 --- a/functions/src/webhooks/admin/payment-forecast/index.ts +++ b/functions/src/functions/webhooks/admin/payment-forecast/index.ts @@ -1,15 +1,15 @@ 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 { 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'; +} from '../../../../../../shared/src/types/recipient'; +import { getLatestExchangeRate } from '../../../../../../shared/src/utils/exchangeRates'; function prepareNextSixMonths(): Map { const nextSixMonths: Map = new Map(); diff --git a/functions/src/webhooks/admin/payment-process/index.ts b/functions/src/functions/webhooks/admin/payment-process/index.ts similarity index 73% rename from functions/src/webhooks/admin/payment-process/index.ts rename to functions/src/functions/webhooks/admin/payment-process/index.ts index 8037c2ed6..99ac3424c 100644 --- a/functions/src/webhooks/admin/payment-process/index.ts +++ b/functions/src/functions/webhooks/admin/payment-process/index.ts @@ -1,19 +1,18 @@ import * as functions from 'firebase-functions'; import { onCall } from 'firebase-functions/v2/https'; import { DateTime } from 'luxon'; -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { PaymentProcessTaskType } from '../../../../../shared/src/types/payment'; -import { toPaymentDate } from '../../../../../shared/src/types/recipient'; +import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { PaymentProcessTaskType } from '../../../../../../shared/src/types/payment'; +import { toPaymentDate } from '../../../../../../shared/src/types/recipient'; import { PaymentCSVTask } from './tasks/PaymentCSVTask'; import { PaymentTask } from './tasks/PaymentTask'; import { RegistrationCSVTask } from './tasks/RegistrationCSVTask'; -import { SendNotificationsTask } from './tasks/SendNotificationsTask'; import { UpdateDatabaseEntriesTask } from './tasks/UpdateDatabaseEntriesTask'; -export interface PaymentProcessProps { +export type PaymentProcessProps = { type: PaymentProcessTaskType; timestamp: number; // seconds -} +}; export default onCall>( { memory: '2GiB' }, @@ -33,9 +32,6 @@ export default onCall>( case PaymentProcessTaskType.CreatePayments: task = new UpdateDatabaseEntriesTask(firestoreAdmin); break; - case PaymentProcessTaskType.SendNotifications: - task = new SendNotificationsTask(firestoreAdmin); - break; default: throw new functions.https.HttpsError('invalid-argument', 'Invalid AdminPaymentProcessTask'); } diff --git a/functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts similarity index 85% rename from functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts index 6561d9abb..c5e1f8b72 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.test.ts @@ -1,8 +1,8 @@ import functionsTest from 'firebase-functions-test'; import { DateTime } from 'luxon'; -import { PaymentProcessTaskType } from '../../../../../../shared/src/types/payment'; -import { toPaymentDate } from '../../../../../../shared/src/types/recipient'; -import { initializeGlobalTestData } from '../../../../firebase'; +import { PaymentProcessTaskType } from '../../../../../../../shared/src/types/payment'; +import { toPaymentDate } from '../../../../../../../shared/src/types/recipient'; +import { initializeGlobalTestData } from '../../../../../firebase'; import { runPaymentProcessTask } from '../../../index'; const projectId = 'payment-csv-task-test'; diff --git a/functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts similarity index 91% rename from functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts index ce0e57ca3..0ecfe7bc1 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentCSVTask.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon'; -import { PAYMENT_AMOUNT_SLE } from '../../../../../../shared/src/types/payment'; +import { PAYMENT_AMOUNT_SLE } from '../../../../../../../shared/src/types/payment'; import { PaymentTask } from './PaymentTask'; export class PaymentCSVTask extends PaymentTask { diff --git a/functions/src/webhooks/admin/payment-process/tasks/PaymentTask.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentTask.ts similarity index 83% rename from functions/src/webhooks/admin/payment-process/tasks/PaymentTask.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/PaymentTask.ts index 77bc3e68d..6cbdb3e68 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/PaymentTask.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/PaymentTask.ts @@ -1,11 +1,11 @@ import { QueryDocumentSnapshot } from 'firebase-admin/firestore'; import { DateTime } from 'luxon'; -import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { FirestoreAdmin } from '../../../../../../../shared/src/firebase/admin/FirestoreAdmin'; import { Recipient, RECIPIENT_FIRESTORE_PATH, RecipientProgramStatus, -} from '../../../../../../shared/src/types/recipient'; +} from '../../../../../../../shared/src/types/recipient'; export abstract class PaymentTask { readonly firestoreAdmin: FirestoreAdmin; diff --git a/functions/src/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts similarity index 83% rename from functions/src/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts index e9a112c2d..8ab22b38c 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/RegistrationCSVTask.test.ts @@ -1,8 +1,8 @@ import functionsTest from 'firebase-functions-test'; import { DateTime } from 'luxon'; -import { PaymentProcessTaskType } from '../../../../../../shared/src/types/payment'; -import { toPaymentDate } from '../../../../../../shared/src/types/recipient'; -import { initializeGlobalTestData } from '../../../../firebase'; +import { PaymentProcessTaskType } from '../../../../../../../shared/src/types/payment'; +import { toPaymentDate } from '../../../../../../../shared/src/types/recipient'; +import { initializeGlobalTestData } from '../../../../../firebase'; import { runPaymentProcessTask } from '../../../index'; const projectId = 'registration-csv-task-test'; diff --git a/functions/src/webhooks/admin/payment-process/tasks/RegistrationCSVTask.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/RegistrationCSVTask.ts similarity index 100% rename from functions/src/webhooks/admin/payment-process/tasks/RegistrationCSVTask.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/RegistrationCSVTask.ts diff --git a/functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts similarity index 89% rename from functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts index be17a0b5c..f569551ef 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.test.ts @@ -1,21 +1,21 @@ import functionsTest from 'firebase-functions-test'; import { DateTime } from 'luxon'; -import { getOrInitializeFirebaseAdmin } from '../../../../../../shared/src/firebase/admin/app'; -import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { getOrInitializeFirebaseAdmin } from '../../../../../../../shared/src/firebase/admin/app'; +import { FirestoreAdmin } from '../../../../../../../shared/src/firebase/admin/FirestoreAdmin'; import { Payment, PAYMENT_FIRESTORE_PATH, PaymentProcessTaskType, PaymentStatus, -} from '../../../../../../shared/src/types/payment'; +} from '../../../../../../../shared/src/types/payment'; import { Recipient, RECIPIENT_FIRESTORE_PATH, RecipientProgramStatus, toPaymentDate, -} from '../../../../../../shared/src/types/recipient'; -import { toDateTime } from '../../../../../../shared/src/utils/date'; -import { initializeGlobalTestData } from '../../../../firebase'; +} from '../../../../../../../shared/src/types/recipient'; +import { toDateTime } from '../../../../../../../shared/src/utils/date'; +import { initializeGlobalTestData } from '../../../../../firebase'; import { runPaymentProcessTask } from '../../../index'; const projectId = 'create-payments-task-test'; diff --git a/functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts b/functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts similarity index 94% rename from functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts rename to functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts index 9a6e4d206..c025dbb6f 100644 --- a/functions/src/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts +++ b/functions/src/functions/webhooks/admin/payment-process/tasks/UpdateDatabaseEntriesTask.ts @@ -1,13 +1,13 @@ import { DateTime } from 'luxon'; -import { toFirebaseAdminTimestamp } from '../../../../../../shared/src/firebase/admin/utils'; +import { toFirebaseAdminTimestamp } from '../../../../../../../shared/src/firebase/admin/utils'; import { Payment, PAYMENT_AMOUNT_SLE, PAYMENT_FIRESTORE_PATH, PAYMENTS_COUNT, PaymentStatus, -} from '../../../../../../shared/src/types/payment'; -import { RECIPIENT_FIRESTORE_PATH, RecipientProgramStatus } from '../../../../../../shared/src/types/recipient'; +} from '../../../../../../../shared/src/types/payment'; +import { RECIPIENT_FIRESTORE_PATH, RecipientProgramStatus } from '../../../../../../../shared/src/types/recipient'; import { ExchangeRateImporter } from '../../../../cron/exchange-rate-import/ExchangeRateImporter'; import { PaymentTask } from './PaymentTask'; diff --git a/functions/src/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts b/functions/src/functions/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts similarity index 87% rename from functions/src/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts rename to functions/src/functions/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts index 061cd6969..53241ebfe 100644 --- a/functions/src/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts +++ b/functions/src/functions/webhooks/admin/scripts/BatchAddCHFToPayments.test.ts @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; -import { toFirebaseAdminTimestamp } from '../../../../../shared/src/firebase/admin/utils'; -import { ExchangeRates } from '../../../../../shared/src/types/exchange-rates'; -import { Payment, PaymentStatus } from '../../../../../shared/src/types/payment'; +import { toFirebaseAdminTimestamp } from '../../../../../../shared/src/firebase/admin/utils'; +import { ExchangeRates } from '../../../../../../shared/src/types/exchange-rates'; +import { Payment, PaymentStatus } from '../../../../../../shared/src/types/payment'; import { PaymentsManager } from './PaymentsManager'; test('BatchAddCHFToPayments', async () => { diff --git a/functions/src/webhooks/admin/scripts/PaymentsManager.ts b/functions/src/functions/webhooks/admin/scripts/PaymentsManager.ts similarity index 90% rename from functions/src/webhooks/admin/scripts/PaymentsManager.ts rename to functions/src/functions/webhooks/admin/scripts/PaymentsManager.ts index 8b9e26e7f..1e8c4c8cf 100644 --- a/functions/src/webhooks/admin/scripts/PaymentsManager.ts +++ b/functions/src/functions/webhooks/admin/scripts/PaymentsManager.ts @@ -1,6 +1,6 @@ -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { ExchangeRates } from '../../../../../shared/src/types/exchange-rates'; -import { Payment, PAYMENT_FIRESTORE_PATH } from '../../../../../shared/src/types/payment'; +import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { ExchangeRates } from '../../../../../../shared/src/types/exchange-rates'; +import { Payment, PAYMENT_FIRESTORE_PATH } from '../../../../../../shared/src/types/payment'; import { ExchangeRateImporter } from '../../../cron/exchange-rate-import/ExchangeRateImporter'; export class PaymentsManager { diff --git a/functions/src/webhooks/admin/scripts/SurveyManager.ts b/functions/src/functions/webhooks/admin/scripts/SurveyManager.ts similarity index 87% rename from functions/src/webhooks/admin/scripts/SurveyManager.ts rename to functions/src/functions/webhooks/admin/scripts/SurveyManager.ts index d9cf29165..a20e09a2e 100644 --- a/functions/src/webhooks/admin/scripts/SurveyManager.ts +++ b/functions/src/functions/webhooks/admin/scripts/SurveyManager.ts @@ -1,22 +1,22 @@ import { DateTime } from 'luxon'; -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 { 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, RecipientMainLanguage, RecipientProgramStatus, -} from '../../../../../shared/src/types/recipient'; +} from '../../../../../../shared/src/types/recipient'; import { SURVEY_FIRETORE_PATH, Survey, SurveyQuestionnaire, SurveyStatus, recipientSurveys, -} from '../../../../../shared/src/types/survey'; -import { rndString } from '../../../../../shared/src/utils/crypto'; -import { toDateTime } from '../../../../../shared/src/utils/date'; +} from '../../../../../../shared/src/types/survey'; +import { rndString } from '../../../../../../shared/src/utils/crypto'; +import { toDateTime } from '../../../../../../shared/src/utils/date'; /** * Takes care of creating surveys for recipients diff --git a/functions/src/webhooks/admin/scripts/index.ts b/functions/src/functions/webhooks/admin/scripts/index.ts similarity index 89% rename from functions/src/webhooks/admin/scripts/index.ts rename to functions/src/functions/webhooks/admin/scripts/index.ts index c53dcee3c..782d03bfe 100644 --- a/functions/src/webhooks/admin/scripts/index.ts +++ b/functions/src/functions/webhooks/admin/scripts/index.ts @@ -1,8 +1,8 @@ import { logger } from 'firebase-functions'; import { onCall } from 'firebase-functions/v2/https'; -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { StripeEventHandler } from '../../../../../shared/src/stripe/StripeEventHandler'; -import { STRIPE_API_READ_KEY } from '../../../config'; +import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { StripeEventHandler } from '../../../../../../shared/src/stripe/StripeEventHandler'; +import { STRIPE_API_READ_KEY } from '../../../../config'; import { PaymentsManager } from './PaymentsManager'; import { SurveyManager } from './SurveyManager'; diff --git a/functions/src/webhooks/index.ts b/functions/src/functions/webhooks/index.ts similarity index 100% rename from functions/src/webhooks/index.ts rename to functions/src/functions/webhooks/index.ts diff --git a/functions/src/webhooks/stripe/index.ts b/functions/src/functions/webhooks/stripe/index.ts similarity index 84% rename from functions/src/webhooks/stripe/index.ts rename to functions/src/functions/webhooks/stripe/index.ts index c48f4629d..ab0fb6e22 100644 --- a/functions/src/webhooks/stripe/index.ts +++ b/functions/src/functions/webhooks/stripe/index.ts @@ -1,13 +1,13 @@ import { DocumentReference, DocumentSnapshot } from 'firebase-admin/firestore'; import { logger } from 'firebase-functions'; import { onRequest } from 'firebase-functions/v2/https'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { SendgridSubscriptionClient } from '../../../../shared/src/sendgrid/SendgridSubscriptionClient'; -import { StripeEventHandler } from '../../../../shared/src/stripe/StripeEventHandler'; -import { Contribution } from '../../../../shared/src/types/contribution'; -import { CountryCode } from '../../../../shared/src/types/country'; -import { User } from '../../../../shared/src/types/user'; -import { STRIPE_API_READ_KEY, STRIPE_WEBHOOK_SECRET } from '../../config'; +import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { SendgridSubscriptionClient } from '../../../../../shared/src/sendgrid/SendgridSubscriptionClient'; +import { StripeEventHandler } from '../../../../../shared/src/stripe/StripeEventHandler'; +import { Contribution } from '../../../../../shared/src/types/contribution'; +import { CountryCode } from '../../../../../shared/src/types/country'; +import { User } from '../../../../../shared/src/types/user'; +import { STRIPE_API_READ_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config'; const addContributorToNewsletter = async (contributionRef: DocumentReference) => { const newsletterClient = new SendgridSubscriptionClient({ diff --git a/functions/src/webhooks/website/survey-login/index.ts b/functions/src/functions/webhooks/website/survey-login/index.ts similarity index 85% rename from functions/src/webhooks/website/survey-login/index.ts rename to functions/src/functions/webhooks/website/survey-login/index.ts index 4c2a489e3..e0011b977 100644 --- a/functions/src/webhooks/website/survey-login/index.ts +++ b/functions/src/functions/webhooks/website/survey-login/index.ts @@ -1,13 +1,13 @@ import assert from 'assert'; import { onCall } from 'firebase-functions/v2/https'; -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { Recipient, RECIPIENT_FIRESTORE_PATH } from '../../../../../shared/src/types/recipient'; +import { FirestoreAdmin } from '../../../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { Recipient, RECIPIENT_FIRESTORE_PATH } from '../../../../../../shared/src/types/recipient'; import { Survey, SURVEY_FIRETORE_PATH, SurveyCredentialRequest, SurveyCredentialResponse, -} from '../../../../../shared/src/types/survey'; +} from '../../../../../../shared/src/types/survey'; export default onCall>(async (request) => { const firestoreAdmin = new FirestoreAdmin(); diff --git a/functions/src/index.ts b/functions/src/index.ts index c284df669..e901b6d3e 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,7 @@ import { DEFAULT_REGION } from '../../shared/src/firebase'; setGlobalOptions({ maxInstances: 10, region: DEFAULT_REGION }); -export * from './cron/index'; -export * from './firestore/index'; -export * from './storage/index'; -export * from './webhooks/index'; +export * from './functions/cron/index'; +export * from './functions/firestore/index'; +export * from './functions/storage/index'; +export * from './functions/webhooks/index'; diff --git a/functions/src/utils/PostfinancePaymentsFileHandler.ts b/functions/src/lib/PostfinancePaymentsFileHandler.ts similarity index 100% rename from functions/src/utils/PostfinancePaymentsFileHandler.ts rename to functions/src/lib/PostfinancePaymentsFileHandler.ts diff --git a/functions/src/webhooks/admin/donation-certificates/DonationCertificatePDFTemplate.tsx b/functions/src/lib/donation-certificates/DonationCertificatePDFTemplate.tsx similarity index 99% rename from functions/src/webhooks/admin/donation-certificates/DonationCertificatePDFTemplate.tsx rename to functions/src/lib/donation-certificates/DonationCertificatePDFTemplate.tsx index 961e65dae..51f03a8d0 100644 --- a/functions/src/webhooks/admin/donation-certificates/DonationCertificatePDFTemplate.tsx +++ b/functions/src/lib/donation-certificates/DonationCertificatePDFTemplate.tsx @@ -1,4 +1,5 @@ // @ts-nocheck + // TODO: Use this basic working example to render PDF instead of pdfkit. import ReactPDF, { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer'; import { User } from '../../../../../shared/src/types'; @@ -24,6 +25,7 @@ interface DonationCertificatePDFTemplateProps { translator: Translator; year: number; } + function DonationCertificatePDFTemplate({ user, translator }: DonationCertificatePDFTemplateProps) { const country = translator.t(user.address?.country as string, { namespace: 'countries' }); const header = translator.t('header'); diff --git a/functions/src/webhooks/admin/donation-certificates/DonationCertificateWriter.ts b/functions/src/lib/donation-certificates/DonationCertificateWriter.ts similarity index 70% rename from functions/src/webhooks/admin/donation-certificates/DonationCertificateWriter.ts rename to functions/src/lib/donation-certificates/DonationCertificateWriter.ts index be9e350f1..52ed406b3 100644 --- a/functions/src/webhooks/admin/donation-certificates/DonationCertificateWriter.ts +++ b/functions/src/lib/donation-certificates/DonationCertificateWriter.ts @@ -1,37 +1,33 @@ import assert from 'assert'; +import { DocumentSnapshot } from 'firebase-admin/firestore'; import { createWriteStream } from 'fs'; import _ from 'lodash'; import * as path from 'path'; import PDFDocument from 'pdfkit'; -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { Contribution, CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '../../../../../shared/src/types/contribution'; -import { User, USER_FIRESTORE_PATH } from '../../../../../shared/src/types/user'; -import { Translator } from '../../../../../shared/src/utils/i18n'; -import { ASSET_DIR } from '../../../config'; +import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { Contribution, CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '../../../../shared/src/types/contribution'; +import { User } from '../../../../shared/src/types/user'; +import { Translator } from '../../../../shared/src/utils/i18n'; +import { ASSET_DIR } from '../../config'; export class DonationCertificateWriter { public readonly year: number; - public readonly user: User; - public readonly contributionsByCurrency: _.Object<{ [p: string]: number }>; + public readonly userDoc: DocumentSnapshot; - private constructor(user: User, year: number, contributionsByCurrency: _.Object<{ [p: string]: number }>) { - assert(user.address?.country === 'CH', 'Donation Certificates are supported for Swiss residents'); - this.user = user; + constructor(userDoc: DocumentSnapshot, year: number) { + assert(userDoc.get('address.country') === 'CH', 'Donation Certificates are supported for Swiss residents'); + this.userDoc = userDoc; this.year = year; - this.contributionsByCurrency = contributionsByCurrency; } - public static async getInstance(userId: string, year: number) { - const firestoreAdmin = new FirestoreAdmin(); - const user = (await firestoreAdmin.doc(USER_FIRESTORE_PATH, userId).get()).data() as User; - const contributionsByCurrency = await this.getContributionsByCurrency(firestoreAdmin, userId, year); - return new DonationCertificateWriter(user, year, contributionsByCurrency); - } - - private static getContributionsByCurrency = async (firestoreAdmin: FirestoreAdmin, userId: string, year: number) => { + private getContributionsByCurrency = async ( + firestoreAdmin: FirestoreAdmin, + userDoc: DocumentSnapshot, + year: number, + ) => { let contributions: Contribution[] = []; await firestoreAdmin - .collection(`${USER_FIRESTORE_PATH}/${userId}/${CONTRIBUTION_FIRESTORE_PATH}`) + .collection(`${userDoc.ref.path}/${CONTRIBUTION_FIRESTORE_PATH}`) .where('status', '==', StatusKey.SUCCEEDED) .get() .then((querySnapshot) => { @@ -51,22 +47,24 @@ export class DonationCertificateWriter { * The function terminates when the PDF file has been written to the file at the given path. */ writeDonationCertificatePDF = async (filePath: string) => { - // TODO: Use @react-pdf/renderer instead of pdfkit, see DonationCertificatePDFTemplate.tsx + const firestoreAdmin = new FirestoreAdmin(); + const contributionsByCurrency = await this.getContributionsByCurrency(firestoreAdmin, this.userDoc, this.year); + const user = this.userDoc.data() as User; const translator = await Translator.getInstance({ - language: this.user.language || 'de', + language: this.userDoc.get('language') || 'de', namespaces: ['donation-certificate', 'countries'], }); const header = translator.t('header'); const location = translator.t('location', { context: { date: new Date() } }); - const country = translator.t(this.user.address?.country as string, { namespace: 'countries' }); + const country = translator.t(user.address?.country as string, { namespace: 'countries' }); const title = translator.t('title', { context: { year: this.year } }); const text1 = translator.t('text-1', { context: { - firstname: this.user.personal?.name, - lastname: this.user.personal?.lastname, - city: this.user.address?.city, + firstname: user.personal?.name, + lastname: user.personal?.lastname, + city: user.address?.city, year: this.year, }, }); @@ -107,9 +105,9 @@ export class DonationCertificateWriter { pdfDocument.fontSize(10).text(header, 45, 20, { align: 'right' }); pdfDocument.moveDown(6); pdfDocument.fontSize(12); - pdfDocument.text(`${this.user.personal?.name} ${this.user.personal?.lastname}`); - pdfDocument.text(`${this.user.address?.street} ${this.user.address?.number}`); - pdfDocument.text(`${this.user.address?.zip} ${this.user.address?.city}`); + pdfDocument.text(`${user.personal?.name} ${user.personal?.lastname}`); + pdfDocument.text(`${user.address?.street} ${user.address?.number}`); + pdfDocument.text(`${user.address?.zip} ${user.address?.city}`); pdfDocument.text(country); pdfDocument.moveDown(6); @@ -124,15 +122,19 @@ export class DonationCertificateWriter { pdfDocument.text(text1); pdfDocument.moveDown(); - this.contributionsByCurrency.keys().forEach((currency) => { - pdfDocument.text( - '– ' + - translator.t('contribution', { - // TODO: use correct locale - context: { currency, amount: this.contributionsByCurrency.get(currency), locale: 'de-CH' }, - }), - ); - }); + if (contributionsByCurrency.size() === 0) { + pdfDocument.text(translator.t('no-contributions'), { underline: true }); + } else { + contributionsByCurrency.keys().forEach((currency) => { + pdfDocument.text( + '– ' + + translator.t('contribution', { + // TODO: use correct locale + context: { currency, amount: contributionsByCurrency.get(currency), locale: 'de-CH' }, + }), + ); + }); + } pdfDocument.moveDown(); pdfDocument.text(text2); @@ -147,10 +149,9 @@ export class DonationCertificateWriter { pdfDocument.text(text5); pdfDocument.moveDown(2); - yPosition = pdfDocument.y; pdfDocument.image(path.join(ASSET_DIR, 'signatures', 'signature_sandino.png'), 45, yPosition, { width: 200 }); pdfDocument.image(path.join(ASSET_DIR, 'signatures', 'signature_kerrin.png'), 240, yPosition, { width: 200 }); - pdfDocument.moveDown(); + pdfDocument.moveDown(4); yPosition = pdfDocument.y; pdfDocument.text('Sandino Scheidegger', 45, yPosition); diff --git a/functions/src/lib/donation-certificates/index.ts b/functions/src/lib/donation-certificates/index.ts new file mode 100644 index 000000000..3b67c8902 --- /dev/null +++ b/functions/src/lib/donation-certificates/index.ts @@ -0,0 +1,68 @@ +import { withFile } from 'tmp-promise'; +import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; +import { StorageAdmin } from '../../../../shared/src/firebase/admin/StorageAdmin'; +import { + DONATION_CERTIFICATE_FIRESTORE_PATH, + DonationCertificate, +} from '../../../../shared/src/types/donation-certificate'; +import { User, USER_FIRESTORE_PATH } from '../../../../shared/src/types/user'; +import { DonationCertificateWriter } from './DonationCertificateWriter'; + +export type CreateDonationCertificatesProps = { + year: number; + userIds?: string[]; +}; + +export async function createDonationCertificates({ year, userIds }: CreateDonationCertificatesProps): Promise { + const storageAdmin = new StorageAdmin(); + const firestoreAdmin = new FirestoreAdmin(); + + let [successCount, usersWithFailures] = [0, [] as string[]]; + + if (!userIds) { + userIds = ( + await firestoreAdmin + .collection(USER_FIRESTORE_PATH) + .where('address.country', '==', 'CH') + .where('address.street', '!=', null) + .select() + .get() + ).docs.map((doc) => doc.id); + } + + await Promise.all( + userIds.map(async (userId) => { + try { + const userDoc = await firestoreAdmin.doc(USER_FIRESTORE_PATH, userId).get(); + const writer = new DonationCertificateWriter(userDoc, year); + const user = userDoc.data() as User; + + // The Firebase auth user ID is used in the storage path to check the user's permissions in the storage rules. + const storagePath = `users/${user.auth_user_id}/donation-certificates/${year}.pdf`; + + await withFile(async ({ path }) => { + await writer.writeDonationCertificatePDF(path); + await storageAdmin.uploadFile({ sourceFilePath: path, destinationFilePath: storagePath }); + await firestoreAdmin + .collection(`${USER_FIRESTORE_PATH}/${userId}/${DONATION_CERTIFICATE_FIRESTORE_PATH}`) + .doc(`${writer.year}-${user.address.country}`) + .set( + { + year, + country: user.address.country, + storage_path: storagePath, + } as DonationCertificate, + { merge: true }, + ); + console.info(`Donation certificate document written for user ${userId}`); + successCount += 1; + }); + } catch (e) { + usersWithFailures.push(userId); + console.error(e); + } + }), + ); + return `Successfully created ${successCount} donation certificates for ${year} + Users with errors (${usersWithFailures.length}): ${usersWithFailures.join(',')}`; +} diff --git a/functions/src/webhooks/admin/donation-certificates/index.ts b/functions/src/webhooks/admin/donation-certificates/index.ts deleted file mode 100644 index cc0f805c2..000000000 --- a/functions/src/webhooks/admin/donation-certificates/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { onCall } from 'firebase-functions/v2/https'; -import { withFile } from 'tmp-promise'; -import { FirestoreAdmin } from '../../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { StorageAdmin } from '../../../../../shared/src/firebase/admin/StorageAdmin'; -import { - DONATION_CERTIFICATE_FIRESTORE_PATH, - DonationCertificate, -} from '../../../../../shared/src/types/donation-certificate'; -import { USER_FIRESTORE_PATH } from '../../../../../shared/src/types/user'; -import { DonationCertificateWriter } from './DonationCertificateWriter'; - -export interface CreateDonationCertificatesFunctionProps { - year: number; - userIds: string[]; - createAll: boolean; // if true, certificates for all CH users are created -} - -export default onCall>({ memory: '2GiB' }, async (request) => { - const firestoreAdmin = new FirestoreAdmin(); - await firestoreAdmin.assertGlobalAdmin(request.auth?.token?.email); - const storageAdmin = new StorageAdmin(); - let [successCount, usersWithFailures] = [0, [] as string[]]; - - let userIds: string[]; - if (request.data.createAll) { - userIds = ( - await firestoreAdmin - .collection(USER_FIRESTORE_PATH) - .where('address.country', '==', 'CH') - .where('address.street', '!=', null) - .select() - .get() - ).docs.map((doc) => doc.id); - } else { - userIds = request.data.userIds; - } - - await Promise.all( - userIds.map(async (userId) => { - try { - const writer = await DonationCertificateWriter.getInstance(userId, request.data.year); - if (writer.contributionsByCurrency.size() === 0) { - console.info(`No contributions found for user ${userId}`); - return; - } - - await withFile(async ({ path }) => { - await writer.writeDonationCertificatePDF(path); - - await storageAdmin.uploadFile({ - sourceFilePath: path, - destinationFilePath: `users/${userId}/donation-certificates/${writer.year}_${writer.user.language}.pdf`, - }); - - await firestoreAdmin - .collection(`${USER_FIRESTORE_PATH}/${userId}/${DONATION_CERTIFICATE_FIRESTORE_PATH}`) - .doc(`${writer.year}-${writer.user.address?.country}`) - .set({ year: writer.year, country: writer.user.address?.country } as DonationCertificate, { merge: true }); - console.info(`Donation certificate document written for user ${userId}`); - successCount += 1; - - // TODO: Send email via Sendgrid - }); - } catch (e) { - usersWithFailures.push(userId); - console.error(e); - } - }), - ); - return `Successfully created ${successCount} donation certificates for ${request.data.year} - Users with errors (${usersWithFailures.length}): ${usersWithFailures.join(',')}`; -}); diff --git a/functions/src/webhooks/admin/payment-process/tasks/SendNotificationsTask.ts b/functions/src/webhooks/admin/payment-process/tasks/SendNotificationsTask.ts deleted file mode 100644 index 179eef077..000000000 --- a/functions/src/webhooks/admin/payment-process/tasks/SendNotificationsTask.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DateTime } from 'luxon'; -import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import { MESSAGE_FIRESTORE_PATH, MessageType, TwilioMessage } from '../../../../../../shared/src/types/message'; -import { PAYMENT_FIRESTORE_PATH, Payment } from '../../../../../../shared/src/types/payment'; -import { RECIPIENT_FIRESTORE_PATH, Recipient } from '../../../../../../shared/src/types/recipient'; -import { sendSms } from '../../../../../../shared/src/utils/messaging/sms'; -import { TWILIO_SENDER_PHONE, TWILIO_SID, TWILIO_TOKEN } from '../../../../config'; -import { PaymentTask } from './PaymentTask'; - -export class SendNotificationsTask extends PaymentTask { - async run(): Promise { - let [notificationsSent, existingNotifications, failedMessages] = [0, 0, 0]; - const now = DateTime.now(); - - for (const recipientDoc of await this.getRecipients()) { - if (recipientDoc.get('test_recipient')) continue; - - const paymentDocRef = this.firestoreAdmin.doc( - `${RECIPIENT_FIRESTORE_PATH}/${recipientDoc.id}/${PAYMENT_FIRESTORE_PATH}`, - now.toFormat('yyyy-MM'), - ); - - if ((await paymentDocRef.get()).exists) { - const paymentDocSnap = await paymentDocRef.get(); - const payment = paymentDocSnap.data() as Payment; - const recipient: Recipient = recipientDoc.data(); - if (!payment.message && recipient.mobile_money_phone) { - try { - const message: MessageInstance = await sendSms({ - from: TWILIO_SENDER_PHONE, - to: `+${recipient.mobile_money_phone.phone}`, - twilioConfig: { sid: TWILIO_SID, token: TWILIO_TOKEN }, - templateProps: { - hbsTemplatePath: 'message/freetext.hbs', - context: { - content: 'You should have received a payment by Social Income. If you have not, please contact us.', - }, - }, - }); - const messageCollection = this.firestoreAdmin.collection( - `${RECIPIENT_FIRESTORE_PATH}/${recipientDoc.id}/${MESSAGE_FIRESTORE_PATH}`, - ); - const messageDocRef = await messageCollection.add({ type: MessageType.SMS, ...message.toJSON() }); - await paymentDocRef.update({ message: messageDocRef }); - } catch (error) { - console.error(error); - failedMessages += 1; - } - notificationsSent++; - } else { - existingNotifications++; - } - } else { - console.log("Payment doesn't exist", paymentDocRef.path); - } - } - return `Sent ${notificationsSent} new payment notifications — ${existingNotifications} already sent, ${failedMessages} failed to send`; - } -} diff --git a/functions/src/webhooks/twilio/TwilioIncomingMessageHandler.ts b/functions/src/webhooks/twilio/TwilioIncomingMessageHandler.ts deleted file mode 100644 index 822df8ba1..000000000 --- a/functions/src/webhooks/twilio/TwilioIncomingMessageHandler.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as functions from 'firebase-functions'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { MESSAGE_FIRESTORE_PATH, MessageType, TwilioMessage } from '../../../../shared/src/types/message'; -import { RECIPIENT_FIRESTORE_PATH, Recipient } from '../../../../shared/src/types/recipient'; - -export class TwilioIncomingMessageHandler { - /** - * For local testing purposes we use ngrok to forward webhooks: - * 1. Setup Whatsapp Sandbox in Twilio and activate your personal phone number on https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn - * 2. Install ngrok on your local machine and make an account on ngrok - * 3. Start ngrok locally with ngrok http 5001 - * 4. Update on https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn - * the incoming message field with https://xxxx-yyy-vvv-www-zzz.eu.ngrok.io/social-income-staging/us-central1/twilioIncomingMessage - * 5. Send test whatsapp to your personal phone number, by answering the function below is triggered - */ - private readonly firestoreAdmin: FirestoreAdmin; - - constructor() { - this.firestoreAdmin = new FirestoreAdmin(); - } - - getFunction() { - return functions.https.onRequest(async (request, response) => { - if (request.body) { - console.log(request.body); - - const recipientId: string = await this.findRecipient(request.body.WaId); - console.log(recipientId); - - if (recipientId.length > 0) { - switch ((request.body.Body as String).toLocaleLowerCase()) { - case 'yes': { - await this.optInWhatsapp(recipientId); - break; - } - //TODO: Implement logic to deal with confirmed payments - /* - case "confirmed": { - await this.confirmPaymentWhatsapp(recipientId); - break; - } - */ - default: { - await this.defaultWhatsapp(recipientId); - } - } - - const messageCollection = this.firestoreAdmin.collection( - `${RECIPIENT_FIRESTORE_PATH}/${recipientId}/${MESSAGE_FIRESTORE_PATH}`, - ); - await messageCollection.add({ type: MessageType.WHATSAPP, ...request.body }); - } - } - response.send(); - }); - } - defaultWhatsapp = async (WaID: string) => { - //We can think whether we want to implement a re-try logic, but there is a risk that we pay a lot of money - }; - - optInWhatsapp = async (recipientId: string) => { - if (recipientId.length > 0) { - const recipientDocRef = this.firestoreAdmin.doc(`${RECIPIENT_FIRESTORE_PATH}`, recipientId); - try { - await recipientDocRef.update({ - 'communication_mobile_phone.whatsapp_activated': true, - }); - console.log('Update successful'); - } catch (error) { - console.log('Something went wrong with updating '); - } - } - }; - - findRecipient = async (WaID: string): Promise => { - const phoneNumber = parseInt(WaID); - let recipientId = ''; - await this.firestoreAdmin - .collection(`${RECIPIENT_FIRESTORE_PATH}`) - .where('communication_mobile_phone.phone', '==', phoneNumber) - .get() - .then((querySnapshot) => { - querySnapshot.forEach((doc) => { - recipientId = doc.id; - }); - }) - .catch((error) => { - console.log(`No recipient found with ${error}`); - }); - return recipientId; - }; -} diff --git a/functions/src/webhooks/twilio/TwilioOutgoingMessageHandler.ts b/functions/src/webhooks/twilio/TwilioOutgoingMessageHandler.ts deleted file mode 100644 index 6bd47d179..000000000 --- a/functions/src/webhooks/twilio/TwilioOutgoingMessageHandler.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { onCall } from 'firebase-functions/v2/https'; -import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; -import { Entity } from '../../../../shared/src/types'; -import { MESSAGE_FIRESTORE_PATH, MessageType, TwilioMessage } from '../../../../shared/src/types/message'; -import { RECIPIENT_FIRESTORE_PATH, Recipient } from '../../../../shared/src/types/recipient'; -import { sendWhatsapp } from '../../../../shared/src/utils/messaging/whatsapp'; -import { TWILIO_SENDER_PHONE, TWILIO_SID, TWILIO_TOKEN } from '../../config'; - -export interface TwilioOutgoingMessageFunctionProps { - recipients: Entity[]; - template: 'opt-in'; -} - -export class TwilioOutgoingMessageHandler { - private readonly firestoreAdmin: FirestoreAdmin; - - constructor() { - this.firestoreAdmin = new FirestoreAdmin(); - } - - getFunction = () => - onCall(async ({ data: { template, recipients }, auth }) => { - await this.firestoreAdmin.assertGlobalAdmin(auth?.token?.email); - let [successCount, skippedCount] = [0, 0]; - for await (const { values: recipient, id } of recipients) { - let message: MessageInstance | undefined; - switch (template) { - case 'opt-in': - message = await sendWhatsapp({ - from: TWILIO_SENDER_PHONE, - to: `+${recipient.communication_mobile_phone?.phone}`, - twilioConfig: { sid: TWILIO_SID, token: TWILIO_TOKEN }, - templateProps: { - language: 'en', - translationNamespace: 'message-whatsapp-opt-in', - hbsTemplatePath: 'message/freetext.hbs', - context: { name: recipient.calling_name ? recipient.calling_name : recipient.first_name }, - }, - }); - break; - //Here we could add more templates to start a Whatsapp conversation. - default: - console.log('Error: Template could not be found'); - skippedCount += 1; - } - - if (message) { - const messageCollection = this.firestoreAdmin.collection( - `${RECIPIENT_FIRESTORE_PATH}/${id}/${MESSAGE_FIRESTORE_PATH}`, - ); - await messageCollection.add({ type: MessageType.WHATSAPP, ...message.toJSON() }); - successCount += 1; - } - } - return `Sent ${successCount} new whatsapp notifications (${skippedCount} failed).`; - }); -} diff --git a/package-lock.json b/package-lock.json index 927c1f6c7..3e4a06812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,6 @@ "ssh2-sftp-client": "^11.0.0", "stripe": "^17.1.0", "tmp-promise": "^3.0.3", - "twilio": "^5.3.3", "xpath": "^0.0.34" }, "devDependencies": { @@ -16277,12 +16276,6 @@ "url": "https://opencollective.com/date-fns" } }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -32535,12 +32528,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", - "license": "BSD-3-Clause" - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -35440,72 +35427,6 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, - "node_modules/twilio": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.4.0.tgz", - "integrity": "sha512-kEmxzdOLTzXzUEXIkBVwT1Itxlbp+rtGrQogNfPtSE3EjoEsxrxB/9tdMIEbrsioL8CzTk/+fiKNJekAyHxjuQ==", - "license": "MIT", - "dependencies": { - "axios": "^1.7.4", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.2", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/twilio/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/twilio/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/twilio/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/twilio/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -37515,15 +37436,6 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "license": "Apache-2.0" }, - "node_modules/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -37679,8 +37591,7 @@ "i18next-resources-to-backend": "^1.2.1", "luxon": "^3.5.0", "mjml": "^4.15.3", - "stripe": "^17.1.0", - "twilio": "^5.3.3" + "stripe": "^17.1.0" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/seed/storage_export/buckets.json b/seed/storage_export/buckets.json index 4ef41e309..ee4ac524c 100644 --- a/seed/storage_export/buckets.json +++ b/seed/storage_export/buckets.json @@ -1,16 +1,10 @@ { "buckets": [ { - "id": "demo-social-income.appspot.com" - }, - { - "id": "social-income-prod.appspot.com" + "id": "demo-social-income-local.appspot.com" }, { "id": "postfinance-payments-files" - }, - { - "id": "social-income-staging.appspot.com" } ] } \ No newline at end of file diff --git a/shared/.env.sample b/shared/.env.sample index 05781c806..fac4a219c 100644 --- a/shared/.env.sample +++ b/shared/.env.sample @@ -3,9 +3,4 @@ GCLOUD_PROJECT="demo-social-income-local" FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 -# For the Twilio related tests to work locally, configure the test credentials from Twilio in your .env file -TWILIO_SID=ACXXXXXXXXXXXXXXXXXXXX -TWILIO_TOKEN=yyyyyyyyyyyyyyyyyyyyy -TWILIO_SENDER_PHONE=+15005550006 - STRIPE_SECRET_KEY="sk_test_XXXXXXXXXXXXXXXXXXXXXXXX" diff --git a/shared/package.json b/shared/package.json index f6bdb65bc..307f472fc 100644 --- a/shared/package.json +++ b/shared/package.json @@ -27,7 +27,6 @@ "i18next-resources-to-backend": "^1.2.1", "luxon": "^3.5.0", "mjml": "^4.15.3", - "stripe": "^17.1.0", - "twilio": "^5.3.3" + "stripe": "^17.1.0" } } diff --git a/shared/src/firebase/admin/StorageAdmin.ts b/shared/src/firebase/admin/StorageAdmin.ts index 6b70aeced..a288df017 100644 --- a/shared/src/firebase/admin/StorageAdmin.ts +++ b/shared/src/firebase/admin/StorageAdmin.ts @@ -22,6 +22,6 @@ export class StorageAdmin { uploadFile = async ({ bucket, sourceFilePath, destinationFilePath }: UploadProps) => { const destinationBucket = bucket || this.storage.bucket(); - await destinationBucket.upload(sourceFilePath, { destination: destinationFilePath }); + return await destinationBucket.upload(sourceFilePath, { destination: destinationFilePath }); }; } diff --git a/shared/src/types/donation-certificate.ts b/shared/src/types/donation-certificate.ts index 6d4ccbcdc..9fdd7c50d 100644 --- a/shared/src/types/donation-certificate.ts +++ b/shared/src/types/donation-certificate.ts @@ -1,7 +1,7 @@ export const DONATION_CERTIFICATE_FIRESTORE_PATH = 'donation-certificates'; export type DonationCertificate = { - url: string; country: string; year: number; + storage_path: string; }; diff --git a/shared/src/types/language.ts b/shared/src/types/language.ts index 42ceff15c..17f799cc6 100644 --- a/shared/src/types/language.ts +++ b/shared/src/types/language.ts @@ -1,4 +1,4 @@ // TODO: use https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes and custom codes for languages that are not in that list, e.g. Krio -const LANGUAGE_CODES = ['en', 'de', 'it', 'fr', 'kri'] as const; +export const LANGUAGE_CODES = ['en', 'de', 'it', 'fr', 'kri'] as const; export type LanguageCode = (typeof LANGUAGE_CODES)[number]; diff --git a/shared/src/types/message.ts b/shared/src/types/message.ts index 824a977ea..ffaaa503e 100644 --- a/shared/src/types/message.ts +++ b/shared/src/types/message.ts @@ -1,5 +1,3 @@ -import { MessageDirection, MessageStatus } from 'twilio/lib/rest/api/v2010/account/message'; - export const MESSAGE_FIRESTORE_PATH = 'messages'; export enum MessageType { @@ -12,32 +10,6 @@ export interface Message { type: MessageType; } -export interface TwilioMessage extends Message { - type: MessageType.SMS | MessageType.WHATSAPP; - - // Twilio MessageInstance.toJSON() fields - body: string; - numSegments: string; - direction: MessageDirection; - from: string; - to: string; - dateUpdated: Date; - price: string; - errorMessage: string; - uri: string; - accountSid: string; - numMedia: string; - status: MessageStatus; - messagingServiceSid: string; - sid: string; - dateSent: Date; - dateCreated: Date; - errorCode: number; - priceUnit: string; - apiVersion: string; - subresourceUris: Record; -} - export interface Email extends Message { type: MessageType.EMAIL; subject: string; diff --git a/shared/src/types/payment.ts b/shared/src/types/payment.ts index 8b3f1589f..35d479e90 100644 --- a/shared/src/types/payment.ts +++ b/shared/src/types/payment.ts @@ -27,7 +27,6 @@ export enum PaymentProcessTaskType { GetRegistrationCSV = 'GetRegistrationCSV', GetPaymentCSV = 'GetPaymentCSV', CreatePayments = 'CreatePayments', - SendNotifications = 'SendNotifications', } export const PAYMENT_AMOUNT_SLE = 700; diff --git a/shared/src/types/recipient.ts b/shared/src/types/recipient.ts index c9c240cf1..07eb1f869 100644 --- a/shared/src/types/recipient.ts +++ b/shared/src/types/recipient.ts @@ -1,4 +1,6 @@ +import { DocumentReference } from 'firebase-admin/firestore'; import { DateTime } from 'luxon'; +import { PartnerOrganisation } from './partner-organisation'; import { Timestamp } from './timestamp'; export const RECIPIENT_FIRESTORE_PATH = 'recipients'; @@ -34,7 +36,7 @@ export type Recipient = { phone: number; has_whatsapp: boolean; }; - organisation: string; + organisation: DocumentReference; om_uid?: number; profession?: string; progr_status: RecipientProgramStatus; diff --git a/shared/src/utils/messaging/sms.ts b/shared/src/utils/messaging/sms.ts deleted file mode 100644 index bd0dc2a73..000000000 --- a/shared/src/utils/messaging/sms.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Twilio } from 'twilio'; -import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import { renderTemplate, RenderTemplateProps } from '../templates'; - -export interface SendSmsProps { - from: string; - to: string; - twilioConfig: { - sid: string; - token: string; - }; - templateProps: RenderTemplateProps; -} - -export const sendSms = async ({ to, from, twilioConfig, templateProps }: SendSmsProps): Promise => { - const body = await renderTemplate(templateProps); - const client = new Twilio(twilioConfig.sid, twilioConfig.token); - return client.messages.create({ body: body, from: from, to: to }); -}; diff --git a/shared/src/utils/messaging/whatsapp.ts b/shared/src/utils/messaging/whatsapp.ts deleted file mode 100644 index bde18e4ed..000000000 --- a/shared/src/utils/messaging/whatsapp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Twilio } from 'twilio'; -import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import { renderTemplate, RenderTemplateProps } from '../templates'; - -export interface SendWhatsappProps { - from: string; - to: string; - twilioConfig: { - sid: string; - token: string; - }; - templateProps: RenderTemplateProps; -} - -export const sendWhatsapp = async ({ - to, - from, - twilioConfig, - templateProps, -}: SendWhatsappProps): Promise => { - const client = new Twilio(twilioConfig.sid, twilioConfig.token); - const body = await renderTemplate(templateProps); - return client.messages.create({ body: body, from: `whatsapp:${from}`, to: `whatsapp:${to}` }); -}; diff --git a/shared/src/utils/sms.test.ts b/shared/src/utils/sms.test.ts deleted file mode 100644 index a768be050..000000000 --- a/shared/src/utils/sms.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TWILIO_SID, TWILIO_TOKEN } from '../../../functions/src/config'; -import { sendSms } from './messaging/sms'; - -const itif = (condition: boolean) => (condition ? test : test.skip); - -describe('send simple SMS', () => { - itif(TWILIO_SID != undefined && TWILIO_SID != 'ACXXXXXXXXXXXXXXXXXXXX')('send simple free text sms', async () => { - const message = await sendSms({ - from: '+15005550006', - to: '+41791234567', - twilioConfig: { - sid: TWILIO_SID, - token: TWILIO_TOKEN, - }, - templateProps: { - hbsTemplatePath: 'message/freetext.hbs', - context: { - content: 'This is a Test SMS', - }, - }, - }); - expect(message.status).toBe('queued'); - expect(message.sid.length).toBeGreaterThan(0); - }); -}); diff --git a/storage.rules b/storage.rules index e4d78e7d7..d2bfd22ce 100644 --- a/storage.rules +++ b/storage.rules @@ -1,7 +1,9 @@ -rules_version = '2'; service firebase.storage { match /b/{bucket}/o { - match /{allPaths=**} { + match /users/{userId}/{data=**} { + allow read, write: if request.auth.uid == userId; + } + match /{data=**} { allow read, write: if false; } } diff --git a/ui/src/components/typography/typography.tsx b/ui/src/components/typography/typography.tsx index 53a04f53b..4a11180e5 100644 --- a/ui/src/components/typography/typography.tsx +++ b/ui/src/components/typography/typography.tsx @@ -18,12 +18,13 @@ const FONT_SIZE_MAP: { [key in FontSize]: string } = { xs: 'text-xs', }; -const FONT_WEIGHTS = ['normal', 'medium', 'bold'] as const; +const FONT_WEIGHTS = ['normal', 'medium', 'bold', 'semibold'] as const; export type FontWeight = (typeof FONT_WEIGHTS)[number]; const FONT_WEIGHT_MAP: { [key in FontWeight]: string } = { normal: 'font-normal', medium: 'font-medium', bold: 'font-bold', + semibold: 'font-semibold', }; const FONT_COLOR_MAP: { [key in FontColor]: string } = { diff --git a/website/.env.development b/website/.env.development index eb27d5897..4206aaf8c 100644 --- a/website/.env.development +++ b/website/.env.development @@ -12,6 +12,9 @@ NEXT_PUBLIC_FIREBASE_FIRESTORE_EMULATOR_HOST="localhost" NEXT_PUBLIC_FIREBASE_FIRESTORE_EMULATOR_PORT="8080" NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_HOST="localhost" NEXT_PUBLIC_FIREBASE_FUNCTIONS_EMULATOR_PORT="5001" +NEXT_PUBLIC_FIREBASE_STORAGE_EMULATOR_HOST="localhost" +NEXT_PUBLIC_FIREBASE_STORAGE_EMULATOR_PORT="9199" +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="demo-social-income-local.appspot.com" BASE_URL="http://localhost:3001" diff --git a/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx b/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx index cae003c80..9e6928ff5 100644 --- a/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx @@ -3,7 +3,6 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { useDonationCertificates } from '@/app/[lang]/[region]/(website)/me/hooks'; import { - Button, SpinnerIcon, Table, TableBody, @@ -13,7 +12,9 @@ import { TableRow, Typography, } from '@socialincome/ui'; +import { ref } from 'firebase/storage'; import Link from 'next/link'; +import { useStorage, useStorageDownloadURL } from 'reactfire'; type ContributionsTableProps = { translations: { @@ -23,6 +24,13 @@ type ContributionsTableProps = { }; } & DefaultParams; +function FetchFileLink({ storagePath }: { storagePath: string }) { + const storage = useStorage(); + const { data } = useStorageDownloadURL(ref(storage, storagePath)); + if (!data) return null; + return Download PDF; +} + export function DonationCertificatesTable({ translations }: ContributionsTableProps) { const { donationCertificates, isLoading } = useDonationCertificates(); @@ -30,7 +38,7 @@ export function DonationCertificatesTable({ translations }: ContributionsTablePr return ; } - if (donationCertificates?.size === 0) { + if (donationCertificates === undefined || donationCertificates.size === 0) { return ; } else { return ( @@ -41,16 +49,16 @@ export function DonationCertificatesTable({ translations }: ContributionsTablePr - {donationCertificates?.docs.map((donationCertificateDoc, index) => { + {donationCertificates.docs.map((donationCertificateDoc, index) => { + const storagePath = donationCertificateDoc.get('storage_path'); + if (!storagePath) return; return ( - + - {donationCertificateDoc.get('year')} + {donationCertificateDoc.get('year')} - - - + ); diff --git a/website/src/app/[lang]/[region]/(website)/me/hooks.ts b/website/src/app/[lang]/[region]/(website)/me/hooks.ts index eb615b8ba..c0e484d01 100644 --- a/website/src/app/[lang]/[region]/(website)/me/hooks.ts +++ b/website/src/app/[lang]/[region]/(website)/me/hooks.ts @@ -1,8 +1,11 @@ import { UserContext } from '@/components/providers/user-context-provider'; import { useApi } from '@/hooks/useApi'; -import { orderBy } from '@firebase/firestore'; +import { orderBy, Query, QuerySnapshot } from '@firebase/firestore'; import { CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '@socialincome/shared/src/types/contribution'; -import { DONATION_CERTIFICATE_FIRESTORE_PATH } from '@socialincome/shared/src/types/donation-certificate'; +import { + DONATION_CERTIFICATE_FIRESTORE_PATH, + DonationCertificate, +} from '@socialincome/shared/src/types/donation-certificate'; import { Employer, EMPLOYERS_FIRESTORE_PATH } from '@socialincome/shared/src/types/employers'; import { USER_FIRESTORE_PATH } from '@socialincome/shared/src/types/user'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -56,21 +59,26 @@ export const useSubscriptions = () => { return { subscriptions, isLoading, error }; }; -export const useDonationCertificates = () => { +export const useDonationCertificates = (): { + donationCertificates: QuerySnapshot | undefined; + isLoading: boolean; + error: Error | null; +} => { const firestore = useFirestore(); const user = useUser(); + const { data: donationCertificates, isLoading, error, - } = useQuery({ + } = useQuery>({ queryKey: ['me', 'donation-certificates'], queryFn: () => getDocs( query( - collection(firestore, USER_FIRESTORE_PATH, user.id, DONATION_CERTIFICATE_FIRESTORE_PATH), + collection(firestore, user.ref.path, DONATION_CERTIFICATE_FIRESTORE_PATH), orderBy('year', 'desc'), - ), + ) as Query, ), }); return { donationCertificates, isLoading, error }; diff --git a/website/tailwind.config.ts b/website/tailwind.config.ts index 883e5862b..3af7698aa 100644 --- a/website/tailwind.config.ts +++ b/website/tailwind.config.ts @@ -1,8 +1,4 @@ export default { - content: [ - './src/**/*.{js,ts,jsx,tsx,mdx}', - '../ui/src/components/**/*.{js,jsx,ts,tsx,mdx,html}', - '../shared/locales/**/*.json', - ], + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}', '../ui/src/**/*.{js,jsx,ts,tsx,mdx,html}', '../shared/locales/**/*.json'], presets: [require('../ui/tailwind.config')], };