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,