Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.49% covered (success)
91.49%
43 / 47
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SyncSubscriptionFromWebhookAction
91.49% covered (success)
91.49%
43 / 47
25.00% covered (danger)
25.00%
1 / 4
20.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
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
10
 extractPreapprovalId
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 mapStatus
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
6.84
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Billing\Application\Actions;
6
7use ImovelZapAi\Billing\Application\Contracts\MercadoPagoClient;
8use ImovelZapAi\Billing\Infrastructure\Persistence\Eloquent\BillingPaymentEventModel;
9use ImovelZapAi\Organizations\Domain\Enums\SubscriptionStatus;
10use ImovelZapAi\Organizations\Infrastructure\Persistence\Eloquent\OrganizationSubscriptionModel;
11
12final class SyncSubscriptionFromWebhookAction
13{
14    public function __construct(
15        private readonly MercadoPagoClient $client,
16    ) {}
17
18    /**
19     * @param  array<string, mixed>  $payload
20     */
21    public function execute(array $payload): ?OrganizationSubscriptionModel
22    {
23        $externalId = $this->extractPreapprovalId($payload);
24        $eventType = (string) ($payload['type'] ?? $payload['action'] ?? 'unknown');
25
26        $subscription = null;
27        if ($externalId !== null) {
28            $subscription = OrganizationSubscriptionModel::query()
29                ->where('mp_preapproval_id', $externalId)
30                ->first();
31
32            if ($subscription === null) {
33                $remote = $this->client->getPreapproval($externalId);
34                $ref = $remote['external_reference'] ?? null;
35                if (is_string($ref) && $ref !== '') {
36                    $subscription = OrganizationSubscriptionModel::query()->find($ref);
37                }
38            }
39
40            if ($subscription !== null) {
41                $remote = $this->client->getPreapproval($externalId);
42                $status = $this->mapStatus((string) ($remote['status'] ?? ''));
43
44                $subscription->forceFill([
45                    'mp_preapproval_id' => $externalId,
46                    'status' => $status,
47                    'canceled_at' => $status === SubscriptionStatus::Canceled ? now() : null,
48                    'grace_ends_at' => $status === SubscriptionStatus::PastDue
49                        ? now()->addDays((int) config('billing.grace_days', 3))
50                        : null,
51                    'current_period_ends_at' => $status === SubscriptionStatus::Active
52                        ? ((string) ($subscription->billing_cycle ?? 'monthly') === 'yearly' ? now()->addYear() : now()->addMonth())
53                        : $subscription->current_period_ends_at,
54                ])->save();
55            }
56        }
57
58        BillingPaymentEventModel::query()->create([
59            'tenant_id' => $subscription?->tenant_id,
60            'organization_subscription_id' => $subscription?->id,
61            'provider' => 'mercadopago',
62            'event_type' => $eventType,
63            'external_id' => $externalId,
64            'status' => $subscription?->status?->value,
65            'payload' => $payload,
66        ]);
67
68        return $subscription?->fresh(['plan']);
69    }
70
71    /**
72     * @param  array<string, mixed>  $payload
73     */
74    private function extractPreapprovalId(array $payload): ?string
75    {
76        $dataId = $payload['data']['id'] ?? $payload['id'] ?? null;
77        if (is_string($dataId) || is_numeric($dataId)) {
78            return (string) $dataId;
79        }
80
81        return null;
82    }
83
84    private function mapStatus(string $mpStatus): SubscriptionStatus
85    {
86        return match (strtolower($mpStatus)) {
87            'authorized', 'active' => SubscriptionStatus::Active,
88            'pending' => SubscriptionStatus::Trialing,
89            'paused' => SubscriptionStatus::PastDue,
90            'cancelled', 'canceled' => SubscriptionStatus::Canceled,
91            default => SubscriptionStatus::PastDue,
92        };
93    }
94}