trypost/app/Support/Billing/FirstMonthCheckoutDiscount.php
Paulo Castellano 2c1cd4ec61 Ignore never-started subscriptions when detecting a first-time customer
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.
2026-07-09 16:03:25 -03:00

66 lines
2.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Billing;
use App\Models\Account;
use Laravel\Cashier\SubscriptionBuilder;
use RuntimeException;
use Stripe\Subscription as StripeSubscription;
final class FirstMonthCheckoutDiscount
{
/**
* Configure a subscription checkout to charge $1 for the first invoice via
* a `duration: once` Stripe coupon, so a real charge validates the card
* instead of a $0 trial authorization. Stripe rejects a Checkout Session
* that sets both `discounts` and `allow_promotion_codes`, so checkouts that
* don't qualify for the paid first month keep the promotion-code field.
*
* @throws RuntimeException when a qualifying checkout has the paid first
* month enabled but no coupon configured — failing
* loudly beats silently charging full price.
*/
public static function apply(SubscriptionBuilder $subscription, Account $account): SubscriptionBuilder
{
if (! self::qualifiesForPaidFirstMonth($account)) {
return $subscription->allowPromotionCodes();
}
$couponId = config('cashier.first_month_coupon_id');
if (! is_string($couponId) || $couponId === '') {
throw new RuntimeException(
'STRIPE_FIRST_MONTH_COUPON_ID must be set when REQUIRE_CARD_FOR_TRIAL is enabled, '
.'otherwise checkout would charge the full price instead of the $1 first month.'
);
}
return $subscription->withCoupon($couponId);
}
/**
* The fixed-amount first-month coupon only applies to a genuinely new
* customer checking out a single workspace: the fixed `amount_off` is only
* correct for a quantity of one, and the $1 offer is for first-time signups
* — not a returning account re-subscribing with workspaces it kept from a
* lapsed subscription. A subscription that never left `incomplete` never
* became real, so a new customer retrying after a failed first attempt
* still qualifies; any started subscription (even canceled) does not.
*/
private static function qualifiesForPaidFirstMonth(Account $account): bool
{
if (! (bool) config('trypost.billing.require_card_for_trial', true)) {
return false;
}
return $account->workspaces()->count() === 1
&& ! $account->subscriptions()
->whereNotIn('stripe_status', [
StripeSubscription::STATUS_INCOMPLETE,
StripeSubscription::STATUS_INCOMPLETE_EXPIRED,
])
->exists();
}
}