Dedicated usage invoice email, and show meter credits on usage lines

Merged
PI
15d ago
+344-18

Send a separate subscription_usage_invoice email when metered usage is billed outside of a subscription cycle.


Summary

Related Issue: N/A

Meter-cycle (usage) orders now send customers their own "usage invoice" email instead of reusing the subscription_cycled renewal email. Usage periods fully covered by meter credits no longer send a $0.00 email, and metered usage lines now show credited units.

What

Usage invoice email

  • New email template subscription_usage_invoice (server/emails/src/emails/subscription_usage_invoice.tsx), plus SubscriptionUsageInvoiceProps / SubscriptionUsageInvoiceEmail in server/polar/email/schemas.py. Regenerated server/emails/src/types/openapi.ts.
  • send_confirmation_email uses the new template for subscription_meter_cycle orders.
  • The email stays under the existing subscription_cycled customer email setting, the same way subscription_final_invoice already does. The dashboard description for that setting now reads "Sent when a subscription renews or is invoiced for usage".

No email for fully-credited periods

  • send_confirmation_email skips meter-cycle orders whose subtotal_amount is 0. The subtotal is the sum of the usage lines after meter credits but before discounts, so only credits trigger the skip. The order is still created. A usage period that a 100% discount reduces to $0, or that the customer balance pays for, still gets the email.

Credited units on usage lines

  • get_amount_and_label on metered prices (server/polar/models/product_price.py) now takes consumed and credited units separately. It used to receive consumed minus credited, so a customer who used 42 units with 30 credited saw "(12 consumed units)". The label now reads "(42 consumed units - 30 credited units) × $0.25"; tiered prices read "(500 consumed units - 100 credited units, volume pricing)". The label is stored on the order item, so the PDF invoice, customer portal and API show the new wording for new orders; existing orders keep their labels.
  • Credits are now subtracted in Decimal, not floating point. The float subtraction, which this PR moved from the billing entry service into the price model, could land just under a rounding threshold: 100.1 units minus 100 credits at 5¢ per unit billed 0¢ instead of 1¢. Apart from that rounding edge case, amounts are unchanged.

Tests

  • Meter-cycle orders send SubscriptionUsageInvoiceEmail; turning off subscription_cycled suppresses it; a $0 subtotal sends nothing; a fully discounted usage order still sends.
  • Label and amount with credits, for both unit and tiered metered prices, and the 100.1 − 100 rounding case.

Screenshots

Neither screenshot uses preview data. A yearly "API Pro" subscription with a monthly meter interval recorded 42 usage events at $0.25 per unit. order_service.settle_meter_cycle_order created the order, send_confirmation_email built the email, and the production renderer binary (render_email_template) produced the HTML. The only test values are the 20% tax (the test suite's mocked tax service) and the invoice number prefix (the test organization's).

No credits:

Usage invoice email

30 units credited:

Usage invoice email with credited units

Why

  • The subject already said "usage invoice", but the body said "Your subscription renewed" / "subscription has just renewed". Customers on yearly plans with monthly usage billing got a "renewed" email every month.
  • A customer whose meter credits covered all their usage got a $0.00 "usage invoice" email every period.
  • Credits were invisible on the invoice, and the usage it did show was lower than what the customer actually used.

How

The template follows subscription_final_invoice: the same attachment, receipt link, "Manage subscription" button and order summary, with usage-specific copy. Unlike the renewal emails, it does not show the "Billing has moved to Polar" migration notice. The template's preview props are a metered usage order, so the react-email dev preview matches the real email. Merchants are unaffected: they get no notification for usage invoices, either before or after this change.

Checklist

  • This PR addresses a single concern (one bug fix, one feature, one refactor)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (uv run task lint && uv run task lint_types)
  • No unrelated changes or drive-by fixes are included

The screenshots are hosted at commits on this branch; the commit after each deletes it, so the PR diff doesn't include them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QBmXDedqXQkcGy2adkwiig