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.
This commit is contained in:
parent
3dd4621f57
commit
2c1cd4ec61
2 changed files with 25 additions and 5 deletions
|
|
@ -7,6 +7,7 @@
|
|||
use App\Models\Account;
|
||||
use Laravel\Cashier\SubscriptionBuilder;
|
||||
use RuntimeException;
|
||||
use Stripe\Subscription as StripeSubscription;
|
||||
|
||||
final class FirstMonthCheckoutDiscount
|
||||
{
|
||||
|
|
@ -44,7 +45,9 @@ public static function apply(SubscriptionBuilder $subscription, Account $account
|
|||
* 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.
|
||||
* 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
|
||||
{
|
||||
|
|
@ -53,6 +56,11 @@ private static function qualifiesForPaidFirstMonth(Account $account): bool
|
|||
}
|
||||
|
||||
return $account->workspaces()->count() === 1
|
||||
&& ! $account->subscriptions()->exists();
|
||||
&& ! $account->subscriptions()
|
||||
->whereNotIn('stripe_status', [
|
||||
StripeSubscription::STATUS_INCOMPLETE,
|
||||
StripeSubscription::STATUS_INCOMPLETE_EXPIRED,
|
||||
])
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ function firstMonthSubscription(Account $account): SubscriptionBuilder
|
|||
return $account->newSubscription(Account::SUBSCRIPTION_NAME, 'price_monthly_test');
|
||||
}
|
||||
|
||||
function givePriorSubscription(Account $account): void
|
||||
function givePriorSubscription(Account $account, string $status = 'canceled'): void
|
||||
{
|
||||
$account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_'.fake()->uuid(),
|
||||
'stripe_status' => 'canceled',
|
||||
'stripe_status' => $status,
|
||||
'stripe_price' => 'price_monthly_test',
|
||||
]);
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ function givePriorSubscription(Account $account): void
|
|||
->and($subscription->couponId)->toBeNull();
|
||||
});
|
||||
|
||||
test('skips the coupon when the account has subscribed before', function () {
|
||||
test('skips the coupon when the account has a prior canceled subscription', function () {
|
||||
Workspace::factory()->create(['account_id' => $this->account->id]);
|
||||
givePriorSubscription($this->account);
|
||||
|
||||
|
|
@ -81,6 +81,18 @@ function givePriorSubscription(Account $account): void
|
|||
->and($subscription->couponId)->toBeNull();
|
||||
});
|
||||
|
||||
test('still applies the coupon when the only prior subscription never left incomplete', function () {
|
||||
Workspace::factory()->create(['account_id' => $this->account->id]);
|
||||
givePriorSubscription($this->account, 'incomplete_expired');
|
||||
|
||||
$subscription = firstMonthSubscription($this->account);
|
||||
|
||||
FirstMonthCheckoutDiscount::apply($subscription, $this->account);
|
||||
|
||||
expect($subscription->couponId)->toBe('TRIAL1USD')
|
||||
->and($subscription->allowPromotionCodes)->toBeFalse();
|
||||
});
|
||||
|
||||
test('throws instead of charging full price when a qualifying checkout has no coupon', function () {
|
||||
config(['cashier.first_month_coupon_id' => '']);
|
||||
Workspace::factory()->create(['account_id' => $this->account->id]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue