Skip to content

[SSF-146] handle recurring donations#114

Merged
amywng merged 15 commits intomainfrom
acw/SSF-146-recurring-emails
Mar 6, 2026
Merged

[SSF-146] handle recurring donations#114
amywng merged 15 commits intomainfrom
acw/SSF-146-recurring-emails

Conversation

@amywng
Copy link
Copy Markdown
Member

@amywng amywng commented Feb 22, 2026

ℹ️ Issue

Closes SSF-146

📝 Description

Implemented logic for processing recurring donations - every day, the cron job calls the service to check whether any scheduled donation dates have passed, send an email to the FM (placeholder log for now), and reschedule the next occurrence.
handleRecurringDonations():

  • fetches all donations and filters for ones with past dates in nextDonationDates
  • for each past date, log a placeholder for sending an automated email to the FM
  • decrements occurrencesRemaining and removes the date from nextDonationDates
  • calculates the next scheduled date using recurrence and recurrenceFreq (in a helper called calculateNextDate()) and adds it to nextDonationDates if there are remaining occurrences
  • handles cascading recalculation - if the replacement date is also in the past, it continues advancing and decrementing occurrencesRemaining until a future date is reached or occurrences run out

Added service tests to cover all cases

  • I added an insertDonation() helper to easily insert a new donation with custom recurrence info

✔️ Verification

Tests pass

@dburkhart07 dburkhart07 self-requested a review February 22, 2026 20:32
Copy link
Copy Markdown

@dburkhart07 dburkhart07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks really good so far! this function looked like it was kinda evil to do 😅

Comment thread apps/backend/src/foodRequests/request.controller.ts Outdated
Comment thread apps/backend/src/donations/donations.service.ts Outdated
Comment thread apps/backend/src/donations/donations.service.ts
Comment thread apps/backend/src/donations/donations.service.ts Outdated
Copy link
Copy Markdown

@dburkhart07 dburkhart07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a few small things for the tests ✈️ 🌵

Comment thread apps/backend/src/donations/donations.service.spec.ts Outdated
Comment thread apps/backend/src/donations/donations.service.spec.ts
Comment thread apps/backend/src/donations/donations.service.ts
Comment thread apps/backend/src/donations/donations.service.ts
Comment thread apps/backend/src/donations/donations.service.spec.ts
Comment thread apps/backend/src/donations/donations.service.spec.ts Outdated
Comment thread apps/backend/src/donations/donations.service.spec.ts
Comment thread apps/backend/src/donations/donations.service.spec.ts
Copy link
Copy Markdown
Collaborator

@Yurika-Kan Yurika-Kan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yayayya donations are being recurred. big things happening.

responded back to bigger comments & one test case!

thanks amy <3

Comment thread apps/backend/src/donations/donations.service.ts
Comment thread apps/backend/src/donations/donations.service.ts
@amywng amywng requested review from Yurika-Kan and dburkhart07 March 1, 2026 06:56
Copy link
Copy Markdown

@dburkhart07 dburkhart07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few small nits. Can you also verify that the Cron job is also actually working with this? I tried setting it to @Cron('* * * * * *'), and it seemed to run, but I did not see any database updates happening.

Comment thread apps/backend/src/donations/donations.service.ts Outdated
private calculateNextDate(
currentDate: Date,
recurrence: RecurrenceEnum,
recurrenceFreq: number | null = 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this default? If this is just a helper, similar to the previous comment, i dont think recurrenceFreq will never null when calculateNextDate is called, since its only called when the recurrence is not NONE.

Copy link
Copy Markdown
Member Author

@amywng amywng Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed for strict mode. alternative would be to throw an error in generateNextDonationDates if donation.recurrenceFreq is null before calling this helper, but we already know it's not null based on the way the db is set up (and the check that the recurrence isn't NONE) so i think this is the best way to handle strict mode on this

expect(donation.occurrencesRemaining).toEqual(1);
expect(donation.nextDonationDates).not.toBeNull();
expect(donation.nextDonationDates).toHaveLength(1);
if (donation.nextDonationDates?.[0]) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a conditional here? If nextDonationDates is not null and has a length of 1, then the first element of it exists. Think we can just do donation.nextDonationDates![0]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the linter complains when i try to do non-null assertion

const donation = await service.findOne(donationId);

expect(donation.nextDonationDates).not.toBeNull();
expect(donation.nextDonationDates?.[0].getDate()).toEqual(28);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make sure the month is also feb?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it technically is going to set it to whatever month is in the future from today, and pass all of the months from jan 31, 2024 to the current date (which is why i have the occurrencesRemaining set to 500 in case these tests matter like 40 years from now...) i've changed it to 1/31/2026 but it's not going to be deterministically february as the month. i think it's fine because this test is only validating the date clamping behavior and there are other tests that make sure the month is right

@amywng amywng requested a review from dburkhart07 March 2, 2026 00:55
Copy link
Copy Markdown

@dburkhart07 dburkhart07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!! 🌇 🌴

@Yurika-Kan
Copy link
Copy Markdown
Collaborator

Few small nits. Can you also verify that the Cron job is also actually working with this? I tried setting it to @Cron('* * * * * *'), and it seemed to run, but I did not see any database updates happening.

note here: my postgres had australia set as the time zone from reinstalling it during my dialogue, which affected the nextDonationDates array it generated - very small chance this would happen for client but good to flag this & might be nice to set explicit timezone in the cron job if it is a real concern

Copy link
Copy Markdown
Collaborator

@Yurika-Kan Yurika-Kan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTMMM so so awesome

tested cron via postman:

  • single past due date
  • multiple backlog past dates (cascade)
  • null constraints
  • cron daily + email logs

note: make sure to check postgres's time zone setting

@amywng amywng merged commit 0a559f3 into main Mar 6, 2026
4 checks passed
@amywng amywng deleted the acw/SSF-146-recurring-emails branch March 6, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants