Skip to content

Updated gift subscription reminder email copy and layout#27509

Merged
minimaluminium merged 3 commits intomainfrom
sodo/gift-reminder-email-tweaks
Apr 23, 2026
Merged

Updated gift subscription reminder email copy and layout#27509
minimaluminium merged 3 commits intomainfrom
sodo/gift-reminder-email-tweaks

Conversation

@minimaluminium
Copy link
Copy Markdown
Member

@minimaluminium minimaluminium commented Apr 22, 2026

no issues

Follow-up to #27436 with a few design tweaks to the new gift reminder email. Also aligns the CTA with the in-progress "Continue subscription" button on the Portal account page.

  • Rewrote the body copy and renamed the CTA "Manage subscription""Continue subscription" to match the in-progress Portal account button
  • Removed the conditional "Hi {name}," greeting so the h1 leads directly
  • Replaced the cadence row with a "Price after gift ends" row, and wired tier currency + monthly/yearly price through the reminder flow

- rewrote body copy to "Continue with a paid subscription to keep reading", making the next step explicit and matching the new CTA
- renamed CTA from "Manage subscription" to "Continue subscription" to match the button being added on the Portal account page and to reflect that a gift member has no subscription to manage yet
- removed the conditional "Hi {name}," greeting so the h1 leads directly; most gift redeemers don't have a name on file so the conditional was branching for an edge case
- the cadence row (e.g. "Bronze • 3 months") described the gift that was already received, not what the redeemer would pay to continue; the end date already communicates the gift length, so it was replaced with a "Price after gift ends" row
- added a "Price after gift ends" row so the redeemer can see the ongoing subscription price without clicking through to Portal — reduces surprise at checkout and helps the decision
- wired tier currency and monthly/yearly price through the reminder flow and picked the price matching the gift cadence
- if pricing has been nulled between purchase and reminder, fail fast before marking the gift as reminded so the next run recovers once an admin restores pricing
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Walkthrough

Removed member name personalization and cadence label from gift reminder templates and data. Introduced post-gift pricing fields (gift.priceAfter, tierPrice, tierCurrency) and extended Tier with currency, monthlyPrice, and yearlyPrice. Updated template text and CTA from "Manage subscription" to "Continue subscription". Service logic now validates tier pricing before sending reminders and avoids marking gifts as reminded when pricing is missing. Unit tests and reminder payload shapes were updated to reflect these changes.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: updating the gift subscription reminder email copy and layout, which encompasses the CTA rename, greeting removal, and cadence row replacement.
Description check ✅ Passed The description is directly related to the changeset, providing context about design tweaks, CTA alignment, greeting removal, and the price display changes across the affected files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sodo/gift-reminder-email-tweaks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
ghost/core/core/server/services/gifts/gift-service.ts (1)

495-503: Tighten the error message to reflect both failure modes.

The guard fires when either the cadence-specific price or the tier's currency is missing, but the message only mentions "pricing". A currency-only failure will emit a misleading Tier missing yearly pricing for gift: ... log line that points an on-call admin at the wrong field. Including which field is actually null makes the recovery path (restore pricing vs. restore currency) obvious from the log.

♻️ Proposed message tweak
-        // Throw before the transaction so the gift isn't marked as reminded;
-        // the next run recovers after an admin restores pricing.
-        const tierPrice = gift.cadence === 'month' ? tier.monthlyPrice : tier.yearlyPrice;
-
-        if (tierPrice === null || tier.currency === null) {
-            throw new errors.NotFoundError({
-                message: `Tier missing ${gift.cadence}ly pricing for gift: ${gift.tierId}`
-            });
-        }
+        // Throw before the transaction so the gift isn't marked as reminded;
+        // the next run recovers after an admin restores pricing.
+        const tierPrice = gift.cadence === 'month' ? tier.monthlyPrice : tier.yearlyPrice;
+
+        if (tierPrice === null || tier.currency === null) {
+            const missing = [
+                tierPrice === null ? `${gift.cadence}ly price` : null,
+                tier.currency === null ? 'currency' : null
+            ].filter(Boolean).join(' and ');
+            throw new errors.NotFoundError({
+                message: `Tier missing ${missing} for gift: ${gift.tierId}`
+            });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/services/gifts/gift-service.ts` around lines 495 -
503, The thrown NotFoundError in gift-service.ts currently only mentions missing
pricing even though the guard checks both cadence-specific price (tierPrice) and
tier.currency; update the error construction (the throw new errors.NotFoundError
call) to include which field(s) are null (e.g., "missing monthly/yearly pricing"
and/or "missing currency") by inspecting tierPrice and tier.currency and
composing a clear message that references gift.tierId so operators know whether
to restore pricing or currency.
ghost/core/test/unit/server/services/gifts/gift-service.test.ts (1)

914-936: Consider also covering the currency === null branch.

sendReminderForGift throws when either tierPrice or tier.currency is null, but this suite only exercises the missing-price branch. A sibling case with currency: null (and valid prices) would lock in both halves of the guard and protect against future regressions where one side is relaxed independently.

♻️ Proposed additional test
+        it('does not mark the gift as reminded when tier currency is missing', async function () {
+            const gift = buildRedeemedGift({cadence: 'year'});
+
+            giftRepository.findPendingReminder.resolves([gift]);
+            giftRepository.getByToken.resolves(gift);
+            tiersService.api.read.resolves({
+                id: 'tier_1',
+                name: 'Bronze',
+                currency: null,
+                monthlyPrice: 1000,
+                yearlyPrice: 10000
+            });
+
+            const service = createService();
+            const result = await service.processReminders();
+
+            assert.equal(result.remindedCount, 0);
+            assert.equal(result.skippedCount, 0);
+            assert.equal(result.failedCount, 1);
+
+            sinon.assert.notCalled(giftRepository.update);
+            sinon.assert.notCalled(giftEmailService.sendReminder);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/server/services/gifts/gift-service.test.ts` around lines
914 - 936, Add a sibling unit test to cover the currency === null branch: in
gift-service.test.ts create a new it(...) similar to the existing "does not mark
the gift as reminded when tier pricing is missing for the gift cadence" but have
tiersService.api.read resolve with a valid price (e.g., monthlyPrice or
yearlyPrice non-null matching the gift cadence) and currency: null; keep
giftRepository.findPendingReminder and getByToken resolving the gift and call
service.processReminders(), then assert remindedCount is 0, skippedCount is 0,
failedCount is 1 and that giftRepository.update and
giftEmailService.sendReminder were not called to ensure sendReminderForGift’s
guard on tier.currency is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/core/server/services/gifts/gift-service.ts`:
- Around line 495-503: The thrown NotFoundError in gift-service.ts currently
only mentions missing pricing even though the guard checks both cadence-specific
price (tierPrice) and tier.currency; update the error construction (the throw
new errors.NotFoundError call) to include which field(s) are null (e.g.,
"missing monthly/yearly pricing" and/or "missing currency") by inspecting
tierPrice and tier.currency and composing a clear message that references
gift.tierId so operators know whether to restore pricing or currency.

In `@ghost/core/test/unit/server/services/gifts/gift-service.test.ts`:
- Around line 914-936: Add a sibling unit test to cover the currency === null
branch: in gift-service.test.ts create a new it(...) similar to the existing
"does not mark the gift as reminded when tier pricing is missing for the gift
cadence" but have tiersService.api.read resolve with a valid price (e.g.,
monthlyPrice or yearlyPrice non-null matching the gift cadence) and currency:
null; keep giftRepository.findPendingReminder and getByToken resolving the gift
and call service.processReminders(), then assert remindedCount is 0,
skippedCount is 0, failedCount is 1 and that giftRepository.update and
giftEmailService.sendReminder were not called to ensure sendReminderForGift’s
guard on tier.currency is exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 029cfd48-a8ee-4443-a44a-5b4a3306c5f2

📥 Commits

Reviewing files that changed from the base of the PR and between d0e813c and 2418691.

📒 Files selected for processing (6)
  • ghost/core/core/server/services/gifts/email-templates/gift-reminder.hbs
  • ghost/core/core/server/services/gifts/email-templates/gift-reminder.ts
  • ghost/core/core/server/services/gifts/gift-email-service.ts
  • ghost/core/core/server/services/gifts/gift-service.ts
  • ghost/core/test/unit/server/services/gifts/gift-email-service.test.js
  • ghost/core/test/unit/server/services/gifts/gift-service.test.ts

@minimaluminium minimaluminium requested a review from sagzy April 23, 2026 05:00
throw new errors.NotFoundError({
message: `Tier missing ${gift.cadence}ly pricing for gift: ${gift.tierId}`
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: currency/price would be missing only happen if the tier was "Free". We should probably do that check on the gift purchase rather than here, but it also doesn't harm to have an extra check here 👍

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.

👍 fair point, leaving in as a safety net for now

Comment thread ghost/core/test/unit/server/services/gifts/gift-email-service.test.js Outdated
Comment thread ghost/core/test/unit/server/services/gifts/gift-email-service.test.js Outdated
- removed the "does not include a greeting" test: it asserted the absence of something deleted in the previous commit, which is low-signal
- renamed the CTA test to "renders a 'Continue subscription' CTA" for clarity, dropping "new" since it ages out after merge
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/gifts/gift-email-service.test.js (1)

177-187: Mirror these formatting assertions against the text body too.

The default price test covers both bodies, but the monthly and EUR edge cases only check HTML. Adding text assertions keeps the plain-text reminder from drifting.

🧪 Proposed test coverage tweak
         it('formats month cadence in the post-gift price', async function () {
             await service.sendReminder({...reminderData, cadence: 'month', tierPrice: 1000});
 
             sinon.assert.calledWith(mailer.send, sinon.match.has('html', sinon.match('$10.00/month')));
+            sinon.assert.calledWith(mailer.send, sinon.match.has('text', sinon.match('$10.00/month')));
         });
 
         it('formats non-USD currency correctly in the post-gift price', async function () {
             await service.sendReminder({...reminderData, tierCurrency: 'eur', tierPrice: 1500});
 
             sinon.assert.calledWith(mailer.send, sinon.match.has('html', sinon.match('€15.00/year')));
+            sinon.assert.calledWith(mailer.send, sinon.match.has('text', sinon.match('€15.00/year')));
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/server/services/gifts/gift-email-service.test.js` around
lines 177 - 187, The tests for monthly cadence and non-USD currency only assert
the formatted price appears in the HTML body; update the two it blocks that call
service.sendReminder (the tests referencing cadence: 'month' and tierCurrency:
'eur' / tierPrice) to also assert the same formatted string appears in the
plain-text body by adding sinon.assert.calledWith(mailer.send,
sinon.match.has('text', sinon.match(...))) mirroring the existing
sinon.match.has('html', ...) assertions for '$10.00/month' and '€15.00/year'
respectively.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/test/unit/server/services/gifts/gift-email-service.test.js`:
- Around line 177-187: The tests for monthly cadence and non-USD currency only
assert the formatted price appears in the HTML body; update the two it blocks
that call service.sendReminder (the tests referencing cadence: 'month' and
tierCurrency: 'eur' / tierPrice) to also assert the same formatted string
appears in the plain-text body by adding sinon.assert.calledWith(mailer.send,
sinon.match.has('text', sinon.match(...))) mirroring the existing
sinon.match.has('html', ...) assertions for '$10.00/month' and '€15.00/year'
respectively.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c931597b-8192-4914-b528-1da25eddf813

📥 Commits

Reviewing files that changed from the base of the PR and between 2418691 and abb2596.

📒 Files selected for processing (1)
  • ghost/core/test/unit/server/services/gifts/gift-email-service.test.js

@sonarqubecloud
Copy link
Copy Markdown

@minimaluminium minimaluminium merged commit bbc4a95 into main Apr 23, 2026
43 checks passed
@minimaluminium minimaluminium deleted the sodo/gift-reminder-email-tweaks branch April 23, 2026 10:16
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.

2 participants