Throttle low-credit warning emails and add a UI opt-out #411

Open
opened 2026-08-06 18:22:55 +00:00 by caution-bot · 1 comment
caution-bot commented 2026-08-06 18:22:55 +00:00 (Migrated from codeberg.org)

Problem

When an organization is low on credits, the warning email should be useful without becoming noisy. If the balance remains below the threshold, users should not receive repeated low-credit warning emails more than once per day.

Users should also have a product-visible way to turn off this specific notification if they do not want low-credit warning emails.

Desired behavior

  • Send the low-credit warning email at most once per day for the same effective notification target while the account remains below the warning threshold.
  • Add a UI setting that lets a user disable low-credit warning emails.
  • Respect the UI preference before sending the warning email.
  • Keep the default behavior safe for billing/account health, e.g. existing users continue receiving the warning unless they opt out.

Acceptance criteria

  • Metering/email delivery persists enough state to enforce a daily cap for low-credit warning emails.
  • The cap prevents repeated sends from periodic balance checks within the same day.
  • The dashboard exposes a clear setting for disabling low-credit warning emails.
  • The backend stores and enforces that notification preference.
  • Focused tests cover both the daily throttle and the disabled-notification path.

Notes / open questions

  • The implementation should define the exact scope of the daily cap: per user, per organization, or per organization-recipient pair. Since existing low-credit warnings are sent to organization member emails, per organization-recipient is likely the least surprising behavior.
  • This is separate from the email template/copy fix for low-credit warnings; this issue is about send frequency and user preference control.
## Problem When an organization is low on credits, the warning email should be useful without becoming noisy. If the balance remains below the threshold, users should not receive repeated low-credit warning emails more than once per day. Users should also have a product-visible way to turn off this specific notification if they do not want low-credit warning emails. ## Desired behavior - Send the low-credit warning email at most once per day for the same effective notification target while the account remains below the warning threshold. - Add a UI setting that lets a user disable low-credit warning emails. - Respect the UI preference before sending the warning email. - Keep the default behavior safe for billing/account health, e.g. existing users continue receiving the warning unless they opt out. ## Acceptance criteria - Metering/email delivery persists enough state to enforce a daily cap for low-credit warning emails. - The cap prevents repeated sends from periodic balance checks within the same day. - The dashboard exposes a clear setting for disabling low-credit warning emails. - The backend stores and enforces that notification preference. - Focused tests cover both the daily throttle and the disabled-notification path. ## Notes / open questions - The implementation should define the exact scope of the daily cap: per user, per organization, or per organization-recipient pair. Since existing low-credit warnings are sent to organization member emails, per organization-recipient is likely the least surprising behavior. - This is separate from the email template/copy fix for low-credit warnings; this issue is about send frequency and user preference control.
caution-bot commented 2026-08-08 03:01:59 +00:00 (Migrated from codeberg.org)

Implementation notes from a quick code inspection/discussion:

  • The intended once-per-day behavior is already partially present in src/metering/src/balance.rs: low-balance warnings read billing_config.low_balance_warned_at, send only when it is missing or older than 24h, then update it to NOW().
  • A user receiving one email per hour is therefore not expected if the cooldown row is being persisted correctly.
  • Plausible root cause: some organizations may not have a billing_config row. In that case the current code treats the missing timestamp as eligible to send, then runs UPDATE billing_config SET low_balance_warned_at = NOW() WHERE organization_id = $1, which updates zero rows. The next hourly collection can then send again.
  • Current org creation paths should be checked/fixed as part of this issue. In particular, initialize_user_account and create_organization create organizations/members but do not appear to create a neutral billing_config row.

Suggested minimal fix shape:

  1. Backfill existing organizations with missing billing config rows. If this is addressing an active spam incident, consider setting low_balance_warned_at = NOW() during the backfill to avoid an immediate post-deploy notification wave; otherwise a neutral row with defaults is fine.
  2. Create a neutral billing_config row whenever a new organization is created:
    INSERT INTO billing_config (organization_id)
    VALUES ($1)
    ON CONFLICT (organization_id) DO NOTHING;
    
  3. Harden the low-balance send path with an atomic upsert/update claim instead of SELECT-then-UPDATE. Only send the email if the claim returns a row, e.g.:
    INSERT INTO billing_config (organization_id, low_balance_warned_at)
    VALUES ($1, NOW())
    ON CONFLICT (organization_id) DO UPDATE
    SET low_balance_warned_at = NOW()
    WHERE billing_config.low_balance_warned_at IS NULL
       OR billing_config.low_balance_warned_at <= NOW() - INTERVAL '24 hours'
    RETURNING low_balance_warned_at;
    
    This handles missing rows and reduces concurrent duplicate-send races.
  4. Then add the UI/backend opt-out. The lowest-risk product scope is probably an organization-level setting stored in existing organizations.settings JSONB, defaulting enabled. A true per-user notification preference would be a larger follow-up because send_dunning_email currently fans out to all organization member emails.

A neutral billing_config row means roughly: prepaid mode, no Paddle customer/subscription, no auto-topup, no payment method, no previous low-balance warning timestamp. The credit balance itself lives in ledger/balance tables, not in billing_config.

Implementation notes from a quick code inspection/discussion: - The intended once-per-day behavior is already partially present in `src/metering/src/balance.rs`: low-balance warnings read `billing_config.low_balance_warned_at`, send only when it is missing or older than 24h, then update it to `NOW()`. - A user receiving one email per hour is therefore not expected if the cooldown row is being persisted correctly. - Plausible root cause: some organizations may not have a `billing_config` row. In that case the current code treats the missing timestamp as eligible to send, then runs `UPDATE billing_config SET low_balance_warned_at = NOW() WHERE organization_id = $1`, which updates zero rows. The next hourly collection can then send again. - Current org creation paths should be checked/fixed as part of this issue. In particular, `initialize_user_account` and `create_organization` create organizations/members but do not appear to create a neutral `billing_config` row. Suggested minimal fix shape: 1. Backfill existing organizations with missing billing config rows. If this is addressing an active spam incident, consider setting `low_balance_warned_at = NOW()` during the backfill to avoid an immediate post-deploy notification wave; otherwise a neutral row with defaults is fine. 2. Create a neutral `billing_config` row whenever a new organization is created: ```sql INSERT INTO billing_config (organization_id) VALUES ($1) ON CONFLICT (organization_id) DO NOTHING; ``` 3. Harden the low-balance send path with an atomic upsert/update claim instead of SELECT-then-UPDATE. Only send the email if the claim returns a row, e.g.: ```sql INSERT INTO billing_config (organization_id, low_balance_warned_at) VALUES ($1, NOW()) ON CONFLICT (organization_id) DO UPDATE SET low_balance_warned_at = NOW() WHERE billing_config.low_balance_warned_at IS NULL OR billing_config.low_balance_warned_at <= NOW() - INTERVAL '24 hours' RETURNING low_balance_warned_at; ``` This handles missing rows and reduces concurrent duplicate-send races. 4. Then add the UI/backend opt-out. The lowest-risk product scope is probably an organization-level setting stored in existing `organizations.settings` JSONB, defaulting enabled. A true per-user notification preference would be a larger follow-up because `send_dunning_email` currently fans out to all organization member emails. A neutral `billing_config` row means roughly: prepaid mode, no Paddle customer/subscription, no auto-topup, no payment method, no previous low-balance warning timestamp. The credit balance itself lives in ledger/balance tables, not in `billing_config`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
public/platform#411
No description provided.