feat: add sent/received breakdown and billing period to usage emails#944
Conversation
The usage limit alert and limit exceeded emails now include a breakdown of sent vs received messages and the current billing period (e.g. 19 June 2026 to 19 July 2026) so users have clearer context on their usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 10 |
| Duplication | 4 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
| Intros: []string{ | ||
| fmt.Sprintf("You have exceeded your limit of %d messages on your %s plan.", user.SubscriptionName.Limit(), user.SubscriptionName), | ||
| fmt.Sprintf("You've reached your limit of %d messages on the %s plan, so new messages will not be processed until your usage resets.", user.SubscriptionName.Limit(), user.SubscriptionName), | ||
| fmt.Sprintf("Between %s and %s you sent %d messages and received %d, for a total of %d.", formatBillingDate(usage.StartTimestamp), formatBillingDate(usage.EndTimestamp), usage.SentMessages, usage.ReceivedMessages, usage.TotalMessages()), |
There was a problem hiding this comment.
Total in breakdown can be one less than the stated limit
IsEntitled uses a strict < limit check, so it fires when TotalMessages() + count >= limit. For the most common case (count = 1), the first limit-exceeded email is triggered when TotalMessages() = limit - 1. The email will therefore say "You've reached your limit of 500 messages" while the breakdown line below reads "for a total of 499" — a one-message gap that users are likely to question or find confusing. The cache key suppresses re-sends for 12 h, so this off-by-one copy is what most users will see on their first block.
Consider either adjusting the IsEntitled check to <= limit (if the intent is to allow exactly limit messages), or updating the copy to say "approaching your limit" rather than "reached your limit" when the total is below the plan ceiling.
There was a problem hiding this comment.
Fixed in 0c46667. Changed IsEntitled from (total + count) < limit to <= limit, so a plan grants the full N messages and the exceeded email now fires at otal = limit — the stated limit and the breakdown total match.
| func formatBillingDate(t time.Time) string { | ||
| return t.Format("2 January 2006") | ||
| } |
There was a problem hiding this comment.
Billing period dates formatted in whichever timezone the database returns
StartTimestamp and EndTimestamp are plain time.Time values stored in BillingUsage. If the database driver attaches UTC (or any other timezone) to them, the formatted date is correct for that zone but may differ from what the user expects. For billing period boundaries this is usually fine, but it's worth confirming that the repository always returns these timestamps in UTC (or consistently normalises them) so the rendered date matches the subscription anniversary the user sees in the billing UI.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed in 0c46667. computeBillingCycle builds the boundaries with ime.UTC, so ormatBillingDate now calls .UTC().Format(...) to render the date in the same zone the cycle is defined in, regardless of what timezone the driver attaches on read-back.
There was a problem hiding this comment.
Updated in 13ca142 to render the billing dates in the user's configured timezone via user.Location() (falls back to UTC on an invalid timezone), matching the existing PhoneDead / UserTimeString pattern, rather than forcing UTC.
Format billing period dates in UTC to match how billing cycle boundaries are computed, and change IsEntitled to <= limit so users get the full message allowance and the exceeded email total matches the stated limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Format the billing period using the user's configured timezone via user.Location() instead of UTC, so dates match what the user sees. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests for BillingUsage.IsEntitled at the exact limit boundary, and for the usage limit emails' sent/received breakdown, billing period rendering in the user's timezone, and formatBillingDate timezone conversion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
The usage limit alert and limit exceeded emails previously only mentioned the total plan limit. They now include:
19 June 2026to19 July 2026)The already-loaded BillingUsage is threaded through
handleLimitExceeded->sendLimitExceededEmail->UsageLimitExceeded, so no extra DB query is needed.Example copy
Limit exceeded:
Usage alert: