259 lines
9.7 KiB
PHP
259 lines
9.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Academy;
|
|
|
|
use App\Mail\AcademyAccessIssue;
|
|
use App\Http\Middleware\ConditionalValidateCsrfToken;
|
|
use App\Models\StaffApplication;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Laravel\Cashier\SubscriptionBuilder;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
final class AcademyBillingCheckoutTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->withoutMiddleware(ConditionalValidateCsrfToken::class);
|
|
$this->configureBilling();
|
|
}
|
|
|
|
public function test_guest_cannot_start_checkout_and_is_redirected_to_login(): void
|
|
{
|
|
$this->post(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertRedirect(route('login'));
|
|
}
|
|
|
|
public function test_guest_can_view_pricing(): void
|
|
{
|
|
$this->get(route('academy.pricing'))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Academy/Billing/Pricing')
|
|
->where('currentTier', 'free')
|
|
->where('isSubscribed', false)
|
|
->where('activePlanKey', null)
|
|
->where('catalog.0.plans.0.price_display', '4.99 EUR')
|
|
->where('catalog.1.plans.0.price_display', '9.99 EUR'));
|
|
}
|
|
|
|
public function test_pricing_shows_active_plan_for_subscriber(): void
|
|
{
|
|
$user = User::factory()->create([
|
|
'email_verified_at' => now(),
|
|
'stripe_id' => 'cus_pricing_active_plan',
|
|
]);
|
|
|
|
$subscription = $user->subscriptions()->create([
|
|
'type' => 'academy',
|
|
'stripe_id' => 'sub_pricing_active_plan',
|
|
'stripe_status' => 'active',
|
|
'stripe_price' => 'price_1creatormonth',
|
|
'quantity' => 1,
|
|
]);
|
|
|
|
$subscription->items()->create([
|
|
'stripe_id' => 'si_pricing_active_plan',
|
|
'stripe_product' => 'prod_creator',
|
|
'stripe_price' => 'price_1creatormonth',
|
|
'quantity' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('academy.pricing'))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Academy/Billing/Pricing')
|
|
->where('currentTier', 'creator')
|
|
->where('isSubscribed', true)
|
|
->where('activePlanKey', 'creator_monthly')
|
|
->where('activePlanLabel', 'Creator Monthly'));
|
|
}
|
|
|
|
public function test_invalid_plan_returns_validation_error(): void
|
|
{
|
|
$user = User::factory()->create(['email_verified_at' => now()]);
|
|
|
|
$this->actingAs($user)
|
|
->from(route('academy.pricing'))
|
|
->post(route('academy.billing.checkout'), ['plan' => 'not_real'])
|
|
->assertRedirect(route('academy.pricing'))
|
|
->assertSessionHasErrors('plan');
|
|
}
|
|
|
|
public function test_missing_price_id_fails_safely(): void
|
|
{
|
|
config()->set('academy_billing.plans.creator_monthly.stripe_price_id', '');
|
|
|
|
$user = User::factory()->create(['email_verified_at' => now()]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertStatus(422)
|
|
->assertJsonPath('code', 'academy_billing_price_missing');
|
|
}
|
|
|
|
public function test_numeric_price_id_fails_safely_before_stripe(): void
|
|
{
|
|
config()->set('academy_billing.plans.creator_monthly.stripe_price_id', '9.99');
|
|
|
|
$user = User::factory()->create(['email_verified_at' => now()]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertStatus(422)
|
|
->assertJsonPath('code', 'academy_billing_price_invalid');
|
|
}
|
|
|
|
public function test_invalid_price_id_redirects_back_with_visible_flash_error(): void
|
|
{
|
|
config()->set('academy_billing.plans.creator_monthly.stripe_price_id', 'prod_not_a_price');
|
|
|
|
$user = User::factory()->create(['email_verified_at' => now()]);
|
|
|
|
$this->actingAs($user)
|
|
->from(route('academy.pricing'))
|
|
->post(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertRedirect(route('academy.pricing'))
|
|
->assertSessionHas('error', 'The selected Academy plan is misconfigured. Please contact support before continuing.');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([
|
|
'error' => 'The selected Academy plan is misconfigured. Please contact support before continuing.',
|
|
])
|
|
->get(route('academy.pricing'))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Academy/Billing/Pricing')
|
|
->where('flash.error', 'The selected Academy plan is misconfigured. Please contact support before continuing.'));
|
|
}
|
|
|
|
public function test_verified_user_can_start_checkout_for_configured_plan(): void
|
|
{
|
|
$user = User::factory()->create(['email_verified_at' => now()]);
|
|
$user = Mockery::mock($user)->makePartial();
|
|
|
|
$builder = Mockery::mock(new SubscriptionBuilder($user, 'academy', 'price_1creatormonth'))->makePartial();
|
|
$builder->shouldReceive('withMetadata')
|
|
->once()
|
|
->andReturnSelf();
|
|
$builder->shouldReceive('checkout')
|
|
->once()
|
|
->andReturn(redirect('https://checkout.stripe.test/session'));
|
|
|
|
$user->shouldReceive('subscription')->once()->with('academy')->andReturn(null);
|
|
$user->shouldReceive('newSubscription')->once()->with('academy', 'price_1creatormonth')->andReturn($builder);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertRedirect('https://checkout.stripe.test/session');
|
|
}
|
|
|
|
public function test_existing_subscriber_is_redirected_to_billing_portal(): void
|
|
{
|
|
$user = User::factory()->create([
|
|
'email_verified_at' => now(),
|
|
'stripe_id' => 'cus_checkout_redirect',
|
|
]);
|
|
|
|
$subscription = $user->subscriptions()->create([
|
|
'type' => 'academy',
|
|
'stripe_id' => 'sub_checkout_redirect',
|
|
'stripe_status' => 'active',
|
|
'stripe_price' => 'price_1creatormonth',
|
|
'quantity' => 1,
|
|
]);
|
|
|
|
$subscription->items()->create([
|
|
'stripe_id' => 'si_checkout_redirect',
|
|
'stripe_product' => 'prod_creator',
|
|
'stripe_price' => 'price_1creatormonth',
|
|
'quantity' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('academy.billing.checkout'), ['plan' => 'creator_monthly'])
|
|
->assertRedirect(route('academy.billing.portal'));
|
|
}
|
|
|
|
public function test_support_report_sends_mail_immediately_and_stores_record(): void
|
|
{
|
|
Mail::fake();
|
|
config()->set('mail.from.address', 'info@skinbase.org');
|
|
|
|
$user = User::factory()->create([
|
|
'email_verified_at' => now(),
|
|
'name' => 'Billing Tester',
|
|
'email' => 'tester@example.com',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->from(route('academy.billing.account'))
|
|
->post(route('academy.billing.report_issue'), [
|
|
'issue_type' => 'access',
|
|
'contact_email' => 'reply@example.com',
|
|
'session_id' => 'cs_test_123',
|
|
'message' => 'I paid but access did not update.',
|
|
])
|
|
->assertRedirect(route('academy.billing.account'))
|
|
->assertSessionHas('success', 'Support request sent — we will verify and activate your access shortly.');
|
|
|
|
Mail::assertSent(AcademyAccessIssue::class, function (AcademyAccessIssue $mail) use ($user): bool {
|
|
return $mail->user->is($user)
|
|
&& $mail->issueType === 'access'
|
|
&& $mail->contactEmail === 'reply@example.com'
|
|
&& $mail->sessionId === 'cs_test_123'
|
|
&& $mail->message === 'I paid but access did not update.';
|
|
});
|
|
|
|
$this->assertDatabaseHas('staff_applications', [
|
|
'topic' => 'contact',
|
|
'email' => 'reply@example.com',
|
|
'role' => 'academy_billing_support',
|
|
]);
|
|
|
|
$application = StaffApplication::query()->latest('created_at')->first();
|
|
|
|
$this->assertNotNull($application);
|
|
$this->assertSame('academy_billing', data_get($application?->payload, 'data.source'));
|
|
$this->assertSame('access', data_get($application?->payload, 'data.issue_type'));
|
|
}
|
|
|
|
private function configureBilling(): void
|
|
{
|
|
config()->set('academy.enabled', true);
|
|
config()->set('academy.payments_enabled', true);
|
|
config()->set('academy_billing.enabled', true);
|
|
config()->set('academy_billing.subscription_name', 'academy');
|
|
config()->set('academy_billing.plans', [
|
|
'creator_monthly' => [
|
|
'label' => 'Creator Monthly',
|
|
'tier' => 'creator',
|
|
'interval' => 'monthly',
|
|
'amount' => '4.99',
|
|
'currency' => 'EUR',
|
|
'stripe_price_id' => 'price_1creatormonth',
|
|
'featured' => false,
|
|
],
|
|
'pro_monthly' => [
|
|
'label' => 'Pro Monthly',
|
|
'tier' => 'pro',
|
|
'interval' => 'monthly',
|
|
'amount' => '9.99',
|
|
'currency' => 'EUR',
|
|
'stripe_price_id' => 'price_1promonth',
|
|
'featured' => true,
|
|
],
|
|
]);
|
|
}
|
|
}
|