From 12dd4d63eed90fbd68178ddb3928bfdb21bb6742 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 21:14:32 +0100 Subject: [PATCH 1/3] Bugfix: Make sure amount displayed on campaign page is dynamically calculated --- admin/src/collections/Campaigns.ts | 22 +++++-------------- shared/src/stripe/StripeEventHandler.ts | 21 ------------------ shared/src/types/campaign.ts | 2 -- shared/src/types/contribution.ts | 9 +++++--- .../(website)/campaign/[campaign]/page.tsx | 10 ++++++--- 5 files changed, 19 insertions(+), 45 deletions(-) diff --git a/admin/src/collections/Campaigns.ts b/admin/src/collections/Campaigns.ts index 41d3fbf56..6d1fe6617 100644 --- a/admin/src/collections/Campaigns.ts +++ b/admin/src/collections/Campaigns.ts @@ -69,45 +69,35 @@ export const campaignsCollection = buildAuditedCollection({ multiline: true, }, link_website: { - title: 'Website Link', + name: 'Website Link', dataType: 'string', validation: { required: false }, description: 'The link to the website (optional)', }, link_instagram: { - title: 'Instagram Link', + name: 'Instagram Link', dataType: 'string', validation: { required: false }, description: 'The link to the Instagram profile (optional)', }, link_tiktok: { - title: 'TikTok Link', + name: 'TikTok Link', dataType: 'string', validation: { required: false }, description: 'The link to the TikTok profile (optional)', }, link_facebook: { - title: 'Facebook Link', + name: 'Facebook Link', dataType: 'string', validation: { required: false }, description: 'The link to the Facebook profile (optional)', }, link_x: { - title: 'X (formerly Twitter) Link', + name: 'X (formerly Twitter) Link', dataType: 'string', validation: { required: false }, description: 'The link to the X profile (optional)', }, - amount_collected_chf: { - dataType: 'number', - name: 'Collected amount in CHF', - readOnly: true, - }, - contributions: { - dataType: 'number', - name: 'Contributions', - readOnly: true, - }, goal: { dataType: 'number', name: 'Optional Fundraising Goal', @@ -152,7 +142,7 @@ export const campaignsCollection = buildAuditedCollection({ validation: { required: true, matches: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, - matchMessage: 'Slug must contain only lowercase letters, numbers, and hyphens', + matchesMessage: 'Slug must contain only lowercase letters, numbers, and hyphens', }, description: 'URL-friendly version of the title. Must be unique and contain only lowercase letters, numbers, and hyphens.', diff --git a/shared/src/stripe/StripeEventHandler.ts b/shared/src/stripe/StripeEventHandler.ts index d1c755d7f..33c3a97da 100644 --- a/shared/src/stripe/StripeEventHandler.ts +++ b/shared/src/stripe/StripeEventHandler.ts @@ -139,26 +139,6 @@ export class StripeEventHandler { } }; - /** - * Increments the total donations of a campaign if the charge is associated with a campaignId. - */ - maybeUpdateCampaign = async (contribution: StripeContribution): Promise => { - if (contribution.campaign_path) { - try { - const campaign = await contribution.campaign_path.get(); - const current_contributions = campaign.data()?.contributions ?? 0; - const current_amount_chf = campaign.data()?.amount_collected_chf ?? 0; - await contribution.campaign_path.update({ - contributions: current_contributions + 1, - amount_collected_chf: current_amount_chf + contribution.amount_chf, - }); - console.log(`Campaign amount ${contribution.campaign_path} updated.`); - } catch (error) { - console.error(`Error updating campaign amount ${contribution.campaign_path}.`, error); - } - } - }; - constructStatus = (status: Stripe.Charge.Status) => { switch (status) { case 'succeeded': @@ -207,7 +187,6 @@ export class StripeEventHandler { ).doc(charge.id); await contributionRef.set(contribution, { merge: true }); console.info(`Updated contribution document: ${contributionRef.path}`); - await this.maybeUpdateCampaign(contribution); return contributionRef; }; } diff --git a/shared/src/types/campaign.ts b/shared/src/types/campaign.ts index 90c98ef8c..cdac19c9f 100644 --- a/shared/src/types/campaign.ts +++ b/shared/src/types/campaign.ts @@ -17,8 +17,6 @@ export type Campaign = { link_tiktok?: string; link_facebook?: string; link_x?: string; - amount_collected_chf: number; // automatically updated by incoming payments. - contributions: number; // automatically updated by incoming payments. goal?: number; goal_currency?: Currency; end_date: Timestamp; diff --git a/shared/src/types/contribution.ts b/shared/src/types/contribution.ts index 56b70a6b7..0a1818a76 100644 --- a/shared/src/types/contribution.ts +++ b/shared/src/types/contribution.ts @@ -22,13 +22,16 @@ export enum StatusKey { export type Contribution = StripeContribution | BankWireContribution; +/** + * Represents a contribution to Social Income. The amount that ends up on our account is amount_chf - fees_chf. + */ type BaseContribution = { source: ContributionSourceKey; status: StatusKey; created: Timestamp; amount: number; - amount_chf: number; - fees_chf: number; + amount_chf: number; // Amount donated in CHF, including fees + fees_chf: number; // Transaction fees in CHF currency: Currency; campaign_path?: DocumentReference; }; @@ -36,7 +39,7 @@ type BaseContribution = { export type StripeContribution = BaseContribution & { source: ContributionSourceKey.STRIPE; monthly_interval: number; - reference_id: string; // stripe charge id + reference_id: string; // The stripe charge id, see: https://docs.stripe.com/api/charges }; export type BankWireContribution = BaseContribution & { diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index ec209e8ec..e862db3cc 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -6,6 +6,7 @@ import { firestoreAdmin } from '@/firebase-admin'; import { WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { getMetadata } from '@/metadata'; import { Campaign, CAMPAIGN_FIRESTORE_PATH, CampaignStatus } from '@socialincome/shared/src/types/campaign'; +import { Contribution } from '@socialincome/shared/src/types/contribution'; import { daysUntilTs } from '@socialincome/shared/src/utils/date'; import { getLatestExchangeRate } from '@socialincome/shared/src/utils/exchangeRates'; import { Translator } from '@socialincome/shared/src/utils/i18n'; @@ -85,8 +86,11 @@ export default async function Page({ params }: CampaignPageProps) { ? await getLatestExchangeRate(firestoreAdmin, campaign.goal_currency) : 1.0; - const contributions = campaign.contributions ?? 0; - const amountCollected = Math.round((campaign.amount_collected_chf ?? 0) * exchangeRate); + const contributions = await firestoreAdmin + .collectionGroup('contributions') + .where('campaign_path', '==', firestoreAdmin.firestore.doc([CAMPAIGN_FIRESTORE_PATH, params.campaign].join('/'))) + .get(); + const amountCollected = (contributions.docs.reduce((sum, c) => sum + c.data().amount_chf, 0) ?? 0) * exchangeRate; const percentageCollected = campaign.goal ? Math.round((amountCollected / campaign.goal) * 100) : undefined; const daysLeft = daysUntilTs(campaign.end_date.toDate()); @@ -122,7 +126,7 @@ export default async function Page({ params }: CampaignPageProps) { {translator?.t('campaign.without-goal.collected', { context: { - count: contributions, + count: contributions.size, amount: amountCollected, currency: campaign.goal_currency, total: campaign.goal, From 219495dc5ba0ab915edbf4fe64273f1ebc9817ae Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 21:53:37 +0100 Subject: [PATCH 2/3] small fix --- .../[lang]/[region]/(website)/campaign/[campaign]/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index e862db3cc..033ab8e2e 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -6,7 +6,7 @@ import { firestoreAdmin } from '@/firebase-admin'; import { WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { getMetadata } from '@/metadata'; import { Campaign, CAMPAIGN_FIRESTORE_PATH, CampaignStatus } from '@socialincome/shared/src/types/campaign'; -import { Contribution } from '@socialincome/shared/src/types/contribution'; +import { Contribution, CONTRIBUTION_FIRESTORE_PATH } from '@socialincome/shared/src/types/contribution'; import { daysUntilTs } from '@socialincome/shared/src/utils/date'; import { getLatestExchangeRate } from '@socialincome/shared/src/utils/exchangeRates'; import { Translator } from '@socialincome/shared/src/utils/i18n'; @@ -87,7 +87,7 @@ export default async function Page({ params }: CampaignPageProps) { : 1.0; const contributions = await firestoreAdmin - .collectionGroup('contributions') + .collectionGroup(CONTRIBUTION_FIRESTORE_PATH) .where('campaign_path', '==', firestoreAdmin.firestore.doc([CAMPAIGN_FIRESTORE_PATH, params.campaign].join('/'))) .get(); const amountCollected = (contributions.docs.reduce((sum, c) => sum + c.data().amount_chf, 0) ?? 0) * exchangeRate; @@ -126,7 +126,7 @@ export default async function Page({ params }: CampaignPageProps) { {translator?.t('campaign.without-goal.collected', { context: { - count: contributions.size, + count: contributions.docs.length, amount: amountCollected, currency: campaign.goal_currency, total: campaign.goal, From 4932dffb4d61949609294a1b4ee33e08f816d468 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 22:00:34 +0100 Subject: [PATCH 3/3] small fix --- .../app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index 033ab8e2e..b3fe7ed36 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -161,7 +161,7 @@ export default async function Page({ params }: CampaignPageProps) { {translator.t('campaign.with-goal.collected-amount', { context: { - count: contributions, + count: contributions.docs.length, amount: amountCollected, currency: campaign.goal_currency, },