Skip to content

Charge $1 for the first month instead of a free trial#176

Merged
paulocastellano merged 4 commits into
mainfrom
feat/paid-first-month-checkout
Jul 9, 2026
Merged

Charge $1 for the first month instead of a free trial#176
paulocastellano merged 4 commits into
mainfrom
feat/paid-first-month-checkout

Conversation

@paulocastellano

Copy link
Copy Markdown
Contributor

What

Replaces the 7/8-day free trial at signup checkout with a real $1 charge on the first invoice, so a genuine card authorization+capture validates the card up front instead of a $0 trial authorization that lets invalid cards slip through until the real charge fires days later.

How it's built

  • A Stripe Coupon (amount_off=1100, currency=usd, duration=once) is applied to the checkout session instead of trialDays(). It discounts only the first invoice ($12 → $1); the next invoice bills the full $12 automatically — no manual swap/cron job needed.
  • app/Support/Billing/FirstMonthCheckoutDiscount.php — applies the coupon when require_card_for_trial=true (the default, card-required flow). The separate no-card generic-trial flow (require_card_for_trial=false) is untouched and keeps allowPromotionCodes() instead, since Stripe rejects a Checkout Session that sets both discounts and allow_promotion_codes.
  • config/cashier.php / .env.example — new STRIPE_FIRST_MONTH_COUPON_ID config.
  • Audited every isOnTrial()/trial-dependent consumer (AccountPolicy, EnsureAccountReady, BillingCycle, the PostHog usage sync job) — no changes needed, they all react dynamically to the subscription no longer entering a trialing state.

Testing

Unit test covering both branches of FirstMonthCheckoutDiscount (coupon applied vs. promotion codes allowed), exercised against a real SubscriptionBuilder with no network calls. Full backend suite green (2476 passed).

Deploy note

The coupon (TRIAL1USD, $11 off, once, USD) exists in Stripe test mode already. The equivalent live coupon has been created — set STRIPE_FIRST_MONTH_COUPON_ID to its live ID in the production .env before this ships.

A $0 trial only runs a weak card authorization, so invalid cards slip
through and only fail once the real charge fires days later. Applying a
$11-off, duration=once Stripe coupon at checkout charges $1 for real on
the first invoice instead, which validates the card immediately, then
reverts to the full monthly price on the next invoice with no manual
swap needed.
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Two hardening fixes for the paid first month:

- FirstMonthCheckoutDiscount throws when the paid first month is enabled
  but STRIPE_FIRST_MONTH_COUPON_ID is unset, instead of silently charging
  every new customer the full price with no discount.
- Guard workspace store() with the same active-subscription check create()
  already applies, so a direct POST can't bootstrap a second billable
  workspace and inflate checkout quantity past the fixed first-month coupon.
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

The fixed amount_off coupon only nets $1 for a quantity of one, and the
offer is meant for genuinely new signups. A lapsed account that kept
several workspaces and re-subscribes through onboarding would otherwise
be charged N*price - amount_off (not $1), and a returning customer could
whittle down to one workspace to claim the discount again.

Apply the coupon only when the paid first month is enabled, the account
bills a single workspace, and it has never subscribed before; every other
checkout falls back to allowing promotion codes at the full price.
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

A raw subscriptions()->exists() check treated a leftover incomplete /
incomplete_expired row — which Cashier persists when a first payment
fails or a 3DS challenge is abandoned — as a prior subscription, so a
genuinely new customer retrying after a failed first attempt lost the
$1 coupon and was charged full price. Exclude those never-started
statuses so only a subscription that actually started (active, canceled,
etc.) marks the account as a returning customer.
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@paulocastellano paulocastellano merged commit eee0a4a into main Jul 9, 2026
5 checks passed
@paulocastellano paulocastellano deleted the feat/paid-first-month-checkout branch July 9, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant