Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.13% covered (warning)
89.13%
41 / 46
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangePlanAction
89.13% covered (warning)
89.13%
41 / 46
33.33% covered (danger)
33.33%
1 / 3
14.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
89.19% covered (warning)
89.19%
33 / 37
0.00% covered (danger)
0.00%
0 / 1
8.08
 assertDowngradeAllowed
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Billing\Application\Actions;
6
7use App\Models\User;
8use ImovelZapAi\Billing\Application\Contracts\MercadoPagoClient;
9use ImovelZapAi\Billing\Domain\Enums\BillingCycle;
10use ImovelZapAi\Billing\Domain\Exceptions\BillingException;
11use ImovelZapAi\Organizations\Domain\Enums\SubscriptionStatus;
12use ImovelZapAi\Organizations\Infrastructure\Persistence\Eloquent\OrganizationSubscriptionModel;
13use ImovelZapAi\Organizations\Infrastructure\Persistence\Eloquent\SubscriptionPlanModel;
14use ImovelZapAi\Properties\Infrastructure\Persistence\Eloquent\PropertyModel;
15use ImovelZapAi\Tenancy\Domain\Enums\OrganizationType;
16use ImovelZapAi\Tenancy\Infrastructure\Persistence\Eloquent\TenantModel;
17
18final class ChangePlanAction
19{
20    public function __construct(
21        private readonly MercadoPagoClient $client,
22        private readonly SyncMercadoPagoPlanCatalogAction $syncCatalog,
23    ) {}
24
25    public function execute(
26        string $tenantId,
27        string $planCode,
28        ?BillingCycle $cycle = null,
29    ): OrganizationSubscriptionModel {
30        $this->syncCatalog->execute();
31
32        $subscription = OrganizationSubscriptionModel::query()
33            ->with('plan')
34            ->where('tenant_id', $tenantId)
35            ->first();
36
37        if ($subscription === null) {
38            throw BillingException::planNotFound($planCode);
39        }
40
41        $newPlan = SubscriptionPlanModel::query()
42            ->where('code', $planCode)
43            ->where('is_active', true)
44            ->first();
45
46        if ($newPlan === null) {
47            throw BillingException::planNotFound($planCode);
48        }
49
50        $this->assertDowngradeAllowed($tenantId, $newPlan);
51
52        $resolvedCycle = $cycle ?? BillingCycle::tryFrom((string) ($subscription->billing_cycle ?? 'monthly'))
53            ?? BillingCycle::Monthly;
54
55        $subscription->forceFill([
56            'subscription_plan_id' => $newPlan->id,
57            'billing_cycle' => $resolvedCycle->value,
58            'status' => $subscription->status === SubscriptionStatus::Canceled
59                ? SubscriptionStatus::Trialing
60                : $subscription->status,
61        ])->save();
62
63        TenantModel::query()->whereKey($tenantId)->update([
64            'type' => (int) $newPlan->max_users === 1
65                ? OrganizationType::Solo
66                : OrganizationType::Agency,
67        ]);
68
69        if (filled($subscription->mp_preapproval_id)) {
70            $mpPlanId = $resolvedCycle === BillingCycle::Yearly
71                ? $newPlan->mp_preapproval_plan_id_yearly
72                : $newPlan->mp_preapproval_plan_id_monthly;
73
74            if (filled($mpPlanId)) {
75                $this->client->updatePreapproval((string) $subscription->mp_preapproval_id, [
76                    'preapproval_plan_id' => $mpPlanId,
77                ]);
78            }
79        }
80
81        return $subscription->fresh(['plan']) ?? $subscription;
82    }
83
84    private function assertDowngradeAllowed(string $tenantId, SubscriptionPlanModel $plan): void
85    {
86        $users = User::query()->where('tenant_id', $tenantId)->count();
87        $properties = PropertyModel::query()->withoutGlobalScopes()->where('tenant_id', $tenantId)->count();
88
89        $maxUsers = (int) $plan->max_users;
90        $maxProperties = (int) $plan->max_properties;
91
92        if ($maxUsers > 0 && $users > $maxUsers) {
93            throw BillingException::downgradeBlocked('usuário(s)', $users, $maxUsers);
94        }
95
96        if ($maxProperties > 0 && $properties > $maxProperties) {
97            throw BillingException::downgradeBlocked('imóvel(is)', $properties, $maxProperties);
98        }
99    }
100}