B Blengi docs

Platform admin

One-time add-ons (Pro AI Setup, etc.)

Add-ons are one-time purchasable services that customers buy alongside (or after) a subscription plan. The first concrete add-on is the Professional AI Setup at €499 — a hands-on service where the team does the heavy lifting on onboarding, knowledge-base setup, AI configuration, and initial optimisation.

Add-ons live in their own table (plan_addons) next to plans. They share the global-catalog model: every workspace can see and buy them, and access to the admin CRUD is gated by the super_admin middleware (not by workspace scope).

What this card ships

  • The plan_addons schema + Eloquent model.
  • Admin CRUD at /admin/addons (super_admin only).
  • A seeded but disabled "Professional AI Setup" add-on (€499 EUR). Activate it from the admin once the delivery process is ready on your side.

What this card does NOT ship (yet)

  • The signup-time checkout integration (next Phase 2 card — attaches the addon as a one-time line item to the Stripe Checkout session).
  • The in-workspace "Services" page where existing customers can buy add-ons after signup (Phase 2 card after that).
  • The customer-facing addon card on the public /pricing page (lands with the signup checkout card).

These hang off the same schema, so creating add-ons now is safe even before the buying surfaces ship.

Creating an add-on

  1. Open /admin/addons and click New add-on.
  2. Fill in the display name, price (in major units), currency (EUR / USD / GBP / etc.), and a one-paragraph description.
  3. Add up to 8 bullet points describing what's included (max 200 chars each). Use the up/down arrows to reorder.
  4. Set a sort order — lower numbers render first when the public pricing surface ships.
  5. Toggle Active only after the delivery process is ready. Inactive add-ons stay in the DB but don't appear to customers.
  6. Save.

Field reference

ColumnTypeMeaning
slug string(64), unique URL-safe identifier. Auto-derived from the name on create. Locked on update so external Stripe metadata references stay stable.
name string(120) Display name shown on the addon card.
description text, nullable One-paragraph pitch shown under the name.
bullets json, nullable Array of strings rendered as the included-items list. Max 8 × 200 chars.
price_cents integer Price in minor units of the addon's own currency.
currency string(3) ISO 4217 currency code. Defaults to EUR.
stripe_price_id string, nullable Cached after first purchase. Empty until a customer actually checks out. Lazy provisioning lands with the next Phase 2 card.
is_active boolean, default false Visibility flag. Inactive add-ons stay in the DB but don't appear on any customer-facing surface.
sort_order integer, default 0 Lower numbers render first.

Deactivation behaviour

Deleting an add-on from the admin is a soft delete — the row is kept but is_active is flipped to false. The workspace_addon_purchases ledger resolves purchases by plan_addon_id; physically removing the row would orphan that history.

Webhook handling & refunds

Once a customer buys an add-on, the Stripe webhook handler at POST /billing/webhook drives the row lifecycle:

Stripe eventEffect on the purchase row
invoice.payment_succeeded Flips pendingpaid and dispatches the team notification job. Resolves the row via session id → subscription id → invoice id (in that order) so it works for both signup-bundle and in-app "Buy Services" 3DS flows.
invoice.payment_failed Flips pendingfailed. Will not regress a row that's already paid / delivered / refunded.
charge.refunded Flips a paid or delivered row to refunded with refunded_at stamped. Resolves the row via Stripe's payment_intent first, then invoice id. Dispatches NotifyBuyerOfAddonRefundJob which emails the workspace owner via AddonRefundedMail so they have a written record of the refund (Stripe's own dashboard email is operator-side only).
customer.subscription.created For bundled signup checkouts, stamps the row's stripe_subscription_id from the metadata so the subsequent invoice.payment_succeeded can find it even when Stripe omits checkout_session on the invoice payload.

Every handler short-circuits on Stripe event-id replay via WebhookIdempotency — a duplicate retry returns 200 without re-running the side effects.

Production webhook signature requirement

The Stripe webhook signature is enforced unconditionally outside local and testing environments. If a deploy ships with an empty STRIPE_WEBHOOK_SECRET, the VerifyWebhookSignature middleware still attaches and returns 403 on every request — fail-closed. This is intentional: Cashier's default gate (skip the middleware when no secret is set) is the wrong default in production. Set STRIPE_WEBHOOK_SECRET in your deploy environment before promoting any release that touches the webhook handler.

See also: Plans & Stripe sync for subscription plans, and the customer-facing Buy services page for the in-workspace purchase flow.