HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/tests/Unit/Subscription/EndIfExpiredTest.php
<?php

namespace Tests\Unit\Subscription;

use App\Model\Customer;
use App\Model\Setting;
use App\Model\Subscription;
use App\Notifications\Admin\CustomerSubscriptionOverdue;
use App\Notifications\Customer\SubscriptionPaymentOverdue;
use App\Services\Notifications\Notifier;
use App\Services\Subscription\SubscriptionManagementService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Mockery;
use Tests\TestCase;

/**
 * Covers SubscriptionManagementService::endIfExpired — the three-branch decision
 * the cron makes per active local subscription:
 *
 *   - before current_period_ends_at        → no-op
 *   - past period_end, inside grace window → notify customer + admins (state=grace)
 *   - past period_end + grace_period       → end() the sub + notify both (state=expired)
 *
 * Plus the skip guards: remote-managed subs, null period_end, non-active.
 *
 * Uses real Setting writes (DB) so the grace_period value flows through the same
 * Setting::get() codepath production uses. Service + Notifier are partial mocks
 * so we can assert end()/dispatch() invocations without exercising their full
 * lifecycle stacks.
 */
class EndIfExpiredTest extends TestCase
{
    use DatabaseTransactions;

    protected function tearDown(): void
    {
        if ($container = Mockery::getContainer()) {
            $this->addToAssertionCount($container->mockery_getExpectationCount());
        }
        Mockery::close();
        parent::tearDown();
    }

    private function buildService(): SubscriptionManagementService
    {
        $service = Mockery::mock(SubscriptionManagementService::class)->makePartial();
        $service->shouldAllowMockingProtectedMethods();
        return $service;
    }

    private function bindNotifier(): Notifier
    {
        $notifier = Mockery::mock(Notifier::class);
        $this->app->instance(Notifier::class, $notifier);
        return $notifier;
    }

    private function buildSubscription(?Carbon $endsAt, bool $remote = false, string $status = Subscription::STATUS_ACTIVE): Subscription
    {
        $customer = Mockery::mock(Customer::class)->makePartial();
        $sub = Mockery::mock(Subscription::class)->makePartial();
        $sub->shouldReceive('hasRemoteSubscription')->andReturn($remote);
        $sub->status = $status;
        $sub->current_period_ends_at = $endsAt;
        $sub->customer = $customer;
        return $sub;
    }

    public function test_grace_zero_expired_subscription_is_ended_and_notifies_expired_state(): void
    {
        Setting::set('grace_period', '0');
        $sub = $this->buildSubscription(Carbon::now()->subSeconds(1));

        $service = $this->buildService();
        $service->shouldReceive('end')->once()->with($sub);

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')
            ->once()
            ->with(
                Mockery::on(fn ($e) => $e instanceof SubscriptionPaymentOverdue
                    && $e->state === SubscriptionPaymentOverdue::STATE_EXPIRED),
                $sub->customer,
            );
        $notifier->shouldReceive('dispatch')
            ->once()
            ->with(
                Mockery::on(fn ($e) => $e instanceof CustomerSubscriptionOverdue
                    && $e->state === CustomerSubscriptionOverdue::STATE_EXPIRED),
            );

        $service->endIfExpired($sub);
    }

    public function test_grace_three_one_day_overdue_stays_active_and_notifies_grace_state(): void
    {
        Setting::set('grace_period', '3');
        $sub = $this->buildSubscription(Carbon::now()->subDay());

        $service = $this->buildService();
        $service->shouldReceive('end')->never();

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')
            ->once()
            ->with(
                Mockery::on(fn ($e) => $e instanceof SubscriptionPaymentOverdue
                    && $e->state === SubscriptionPaymentOverdue::STATE_GRACE),
                $sub->customer,
            );
        $notifier->shouldReceive('dispatch')
            ->once()
            ->with(
                Mockery::on(fn ($e) => $e instanceof CustomerSubscriptionOverdue
                    && $e->state === CustomerSubscriptionOverdue::STATE_GRACE),
            );

        $service->endIfExpired($sub);
    }

    public function test_grace_three_four_days_overdue_is_ended_and_notifies_expired_state(): void
    {
        Setting::set('grace_period', '3');
        $sub = $this->buildSubscription(Carbon::now()->subDays(4));

        $service = $this->buildService();
        $service->shouldReceive('end')->once()->with($sub);

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')->twice();

        $service->endIfExpired($sub);
    }

    public function test_subscription_still_inside_paid_period_does_nothing(): void
    {
        Setting::set('grace_period', '3');
        $sub = $this->buildSubscription(Carbon::now()->addDays(2));

        $service = $this->buildService();
        $service->shouldReceive('end')->never();

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')->never();

        $service->endIfExpired($sub);
    }

    public function test_remote_subscription_is_skipped_no_notifications(): void
    {
        Setting::set('grace_period', '0');
        $sub = $this->buildSubscription(Carbon::now()->subDays(30), remote: true);

        $service = $this->buildService();
        $service->shouldReceive('end')->never();

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')->never();

        $service->endIfExpired($sub);
    }

    public function test_null_period_end_does_not_crash_or_end(): void
    {
        Setting::set('grace_period', '0');
        $sub = $this->buildSubscription(null);

        $service = $this->buildService();
        $service->shouldReceive('end')->never();

        $notifier = $this->bindNotifier();
        $notifier->shouldReceive('dispatch')->never();

        $service->endIfExpired($sub);
    }
}