Energy Bundle — Customer Portal "Add Energy Bundle" CTA
Status: Planned Issue: #155Depends on: #149 (Energy Bundle Stripe SKU + entitlement helpers — shipped)
Price (live in Stripe as of 2026-05-20, re-priced to round numbers 2026-05-21): $100 CAD/seat/month or $1,000 CAD/seat/year (2 months free). Product: prod_UYQu6Drc1obzVY. Resolve price IDs via getVerticalBundlePriceId("energy_bundle", interval) from server/utils/pricing.js.
Goal
Self-serve add-on upgrade flow inside the customer portal for Business-plan customers. A single "Add Energy Bundle" CTA card in the account page that opens a Stripe-hosted checkout for the add-on. On success, metadata.energy_bundle = true lands on the subscription and the energy-specific features unlock immediately.
CTA placement
The CTA card lives on /app/account (the customer portal page) in a new "Add-ons" section below the existing plan/invoice/payment sections. Visibility rules:
- Hidden when the user is not on Business
- Hidden when
metadata.energy_bundleis alreadytrue - Visible — primary when the user is on Business and not yet on the bundle
For Starter and Pro customers, no CTA appears. Instead, the existing tier-upgrade path takes them to Business first. Once they're on Business, the Energy Bundle add-on becomes available — a one-click upgrade rather than a side-stream.
Component
app/components/billing/EnergyBundleCard.vue:
- Card layout consistent with the existing billing/account UI (
UCard+UButtonfrom Nuxt UI) - Title: "Energy Bundle"
- Subtitle: short benefits list (AER wells filters, CCS layers, asset history, Indigenous consultation overlay, Crown dispositions overlay — surfaces from the Energy Bundle docs)
- Price line: pulled from
STRIPE_ENERGY_BUNDLE_DISPLAY_PRICEso the dollar value isn't hardcoded - Primary action: "Add Energy Bundle" → POSTs to
/api/billing/energy-bundle/checkoutwhich returns a Stripe Checkout URL - Secondary action: "Learn more" → opens
/use-cases/energy(once that route is wired — see follow-up task)
API
POST /api/billing/energy-bundle/checkout
body: {}
→ { url: "https://checkout.stripe.com/c/pay/..." }
Implementation:
requireUser(event)+ check the user is on Business (requireBusiness(event))- Verify they don't already have the bundle (idempotent — clicking twice doesn't open two checkout sessions)
- Create a Stripe Checkout session with the price ID from
STRIPE_ENERGY_BUNDLE_PRICE_ID,client_reference_idset to the user ID, and the subscription set to modify the existing Business subscription (not create a parallel one) - Return the checkout URL
Webhook handler (server/api/webhooks/stripe.post.js) on the checkout.session.completed event for mode: subscription with the energy bundle price ID:
- Update
app.subscriptions.metadata.energy_bundle = truefor the user - Send a confirmation email
- Trigger a Telegram notification for the founder-led sales motion
Trial promo code
The 10 existing energy customers get a trial promo code (90-day or full-comp — TBD per operations runbook). The Stripe Checkout session includes allow_promotion_codes: true so they can paste the code at checkout. The promo code itself is configured in Stripe directly — no code change needed.
Acceptance criteria progress
- Energy Bundle entitlement helpers shipped via #149
- "Add Energy Bundle" CTA card component
- CTA visible only to Business customers not yet on the bundle
-
/api/billing/energy-bundle/checkoutendpoint - Stripe Checkout session creation with the bundle price ID
- Webhook handler setting
metadata.energy_bundle = trueon success - Entitlement applied immediately (energy-gated features unlock without re-login)
- Trial promo code path for 10 existing energy customers
- Confirmation email + Telegram notification
UI consistency
The card follows the existing customer-portal design patterns. Reference components on /app/account:
app/components/billing/PlanCard.vue— current plan summary cardapp/components/billing/PaymentMethodCard.vue— payment method cardapp/components/billing/InvoiceList.vue— invoice history
The new EnergyBundleCard.vue matches their visual treatment (padding, border, header style) so the add-on doesn't read as a different surface.
Related features
- Energy Bundle Stripe SKU (#149) — entitlement helpers
- AER Wells data layer — bundle-gated when expanded with abandoned/orphan/operator filters
- Alberta CCS / Pore Space Tenure — bundle-gated
- Asset/operator history — bundle-gated
- Indigenous consultation overlay — bundle-gated
- Crown dispositions — bundle-gated