Repository navigation
feat(billing): email users when their first-purchase bonus window opens and before it closes - #4516
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
|
Warning Review limit reached
This review includes 25 billable files and costs up to $6.25.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 13 minutes for your next included review. View limit details
Comment |
There was a problem hiding this comment.
ℹ️ No critical issues — one minor suggestion inline.
Reviewed changes
- Window-open detection on the settle path:
FirstPurchaseBonusService.findWindowOpenedBydecides, for a payment that earned no bonus, whether it was a personal account's first card payment, andsettleSucceededTransactionreturns that decision soStripeWebhookServicecan publishFirstPurchaseBonusWindowOpenedpost-commit (idempotent across webhook retries via the settled-status guard). - Opened-email handler: re-checks live eligibility (window must still be the open one for the event's reason), tracks Amplitude events with stable
insertIds, schedules ashort-policy reminder per user+reason withstartAfter= close − 2 days, and skips the reminder when less than 2 days remain. - Reminder handler: re-checks eligibility and the live close date before sending the "2 days left" email, so bonus earned since, closed windows, and late unsubscribes all suppress it.
PromotionalNotificationService: attaches the signed unsubscribe links from #4514, and refuses to send for unsubscribed users or whenEMAIL_UNSUBSCRIBE_SECRETis unset (warn log) — the email terms derive fromFirstPurchaseBonusService's constants, so copy cannot drift from the rule.- Tests: unit specs for both handlers, templates, promotional service, bonus service and webhook service; integration specs running both handlers through the real pg-boss worker; a webhook functional spec covering the small first purchase. I ran the new suites locally — 111 unit, 75 integration, and 18 functional tests all pass, and
tsc --noEmit/ eslint are clean.
The stacking on #4514 is clean: this diff only consumes the unsubscribe-token and preference plumbing that already exists on the base branch, and the PR body's release-order note (notifications must ship #4514 first) matches the code — an older notifications service strips the unknown unsubscribe field and would send without the headers.
GLM-5.3 | 𝕏
There was a problem hiding this comment.
✅ No new issues found — the prior review's one finding is cleanly fixed.
Reviewed changes
This run covered the delta since the initial review: commit 573bc0d9 ("lock the buyer before opening a first-purchase bonus window").
- Locked the buyer before opening a window:
findWindowOpenedBynow goes through the same#lockPayingAccountThenPersonas the bonus path — paying organization first, then the person, bothno key update— so concurrent small first purchases serialize and the loser sees the winner's committed payment and opens nothing.#isPaidByTeamis gone; the team check reads the locked organization row, preserving the missing-org-treated-as-personal semantics, and lock ordering matches every other org/person locker in the codebase (no inversions). - Regression and unit coverage: a new integration spec settles one person's two small first purchases in parallel and asserts exactly one window; unit specs assert org→person lock ordering for both entry points and the user-not-lockable 500. The settle-path integration spec now builds its persisted bonus service with real
UserRepository/OrganizationRepositoryso the locks actually run against Postgres. - Verified locally: 54 unit + 76 integration tests pass on real Postgres, and the base-checkout check (new test against the pre-fix service) fails with two windows, so the test genuinely catches the race.
tsc --noEmitand eslint on the changed files are clean.
GLM-5.3 | 𝕏
|
@claude review |
There was a problem hiding this comment.
An organization admin can view or raise the monthly spending cap at claude.ai/admin-settings/usage, on the Claude Code Review row under Services. The cap resets at the start of the next billing period.
Once the cap resets or is raised, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
573bc0d to
eed98d9
Compare
c742923 to
4e5cf67
Compare
eed98d9 to
2793a97
Compare
…ns and before it closes A personal account's first card payment that earns no bonus now opens the 7-day window for the bonus, auto-reload charges included. The settle path returns the opening and the webhook publishes FirstPurchaseBonusWindowOpened after commit, next to the bonus grant. The event handler re-reads the live eligibility, tracks the opening, schedules a single reminder 2 days before the window closes and sends the opening email. The reminder runs on a short queue keyed per person and window, and checks eligibility and the opt-out again when it is due. Both emails go through PromotionalNotificationService. It sends nothing to users who opted out of product update emails or when EMAIL_UNSUBSCRIBE_SECRET is unset. Otherwise it signs the unsubscribe page and one-click links, which the notifications service turns into the footer link and the List-Unsubscribe headers. Part of CON-1165
…ndow Two small first purchases settling at once each missed the other's payment. Both opened a window, so the person got two emails.
2793a97 to
842e173
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4516 +/- ##
==========================================
+ Coverage 94.05% 94.27% +0.22%
==========================================
Files 1759 1658 -101
Lines 50638 47926 -2712
Branches 12388 11844 -544
==========================================
- Hits 47627 45182 -2445
+ Misses 2708 2457 -251
+ Partials 303 287 -16
*This pull request uses carry forward flags. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
✅ No new issues found — this push is a clean rebase onto main with #4514 merged, and the PR's own code is byte-for-byte what the prior reviews covered.
Reviewed changes
This run covered the delta since the prior pullfrog review (573bc0d): the branch was rebased onto current main, with no change to the billing code itself.
- Dropped the stacked notifications commits: the branch previously carried the three #4514 commits (unsubscribe footer, header-safe URL validation, postal-address line breaks) beneath the billing work; their content squash-merged into main as
85b9b43, so the PR diff is now the 25apps/apifiles alone. - Rebased the two billing commits onto the new base unchanged:
git range-diffagainst the pre-push head shows both the feature commit and the buyer-lock commit applying byte-for-byte identically (=), so the settle path, both handlers, the templates,PromotionalNotificationServiceand every spec are exactly the code reviewed at573bc0d, including the lock fix for the duplicate-email race. - Verified the composition with main's new commits: #4512's additions to
jobs.provider.tscoexist with this PR's two handler registrations;CreateNotificationInput'sunsubscribefield now resolves from the shared schema #4514 landed inpackages/console-api-types; #4515 and #4530 touched deploy-web only; and the rebased head's full check suite passes, including api tests, mutation testing and test-build.
GLM-5.3 | 𝕏

Why
#4513 keeps the first-purchase bonus open for 7 days after a first purchase under $100, but people only find out on the Add Credits screen. This PR emails them when the window opens and again 2 days before it closes.
Part of CON-1165
What
FirstPurchaseBonusWindowOpenedafter commit.shortpolicy, so one per person and window).PromotionalNotificationServiceattaches the signed unsubscribe links from feat(notifications): add an unsubscribe footer and one-click headers to promotional emails #4514. It skips people who opted out of product updates, and sends nothing whenEMAIL_UNSUBSCRIBE_SECRETis unset.first_purchase_bonus_window_openedandfirst_purchase_bonus_window_email_sent.Release: notifications with #4514 (merged) must be live in prod before this API ships. Otherwise these emails go out without the unsubscribe headers and footer.
Testing