Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.00% covered (warning)
84.00%
105 / 125
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayWebhookController
84.00% covered (warning)
84.00%
105 / 125
50.00% covered (danger)
50.00%
1 / 2
32.44
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
 stripe
83.87% covered (warning)
83.87%
104 / 124
0.00% covered (danger)
0.00%
0 / 1
31.29
1<?php
2
3namespace App\Http\Controllers\v2\RolePlay;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\AddOns;
7use App\Http\Models\Auth\User;
8use App\Http\Models\Invoices;
9use App\Http\Models\UserAddOns;
10use App\Traits\SubscriptionTrait;
11use Illuminate\Http\JsonResponse;
12use Illuminate\Http\Request;
13use Illuminate\Support\Facades\Log;
14use Stripe\Exception\SignatureVerificationException;
15use Stripe\Webhook;
16
17class RolePlayWebhookController extends Controller
18{
19    use SubscriptionTrait;
20
21    public function __construct() {}
22
23    public function stripe(Request $request): JsonResponse
24    {
25        $webhookSecret = config('app.stripe.role_play_webhook');
26
27        $signature = $request->header('Stripe-Signature');
28
29        try {
30            $event = Webhook::constructEvent(
31                $request->getContent(),
32                $signature,
33                $webhookSecret
34            );
35        } catch (SignatureVerificationException $e) {
36            return response()->json(['status' => 'Webhook Failed'], 400);
37        }
38
39        $addonProductIds = AddOns::where('product', 'RolePlay')->select('stripe_id')->get()->pluck('stripe_id')->toArray();
40
41        $eventType = $event->type;
42        $eventObject = $event->data->object;
43
44        $isAddonEvent = false;
45
46        if (in_array($eventType, ['customer.subscription.created', 'customer.subscription.updated', 'customer.subscription.deleted'])) {
47            if (isset($eventObject->items->data[0]->price->product) && in_array($eventObject->items->data[0]->price->product, $addonProductIds)) {
48                $isAddonEvent = true;
49            }
50        } elseif (in_array($eventType, ['invoice.paid', 'invoice.payment_failed'])) {
51            foreach ($eventObject->lines->data as $lineItem) {
52                if (isset($lineItem->price->product) && in_array($lineItem->price->product, $addonProductIds)) {
53                    $isAddonEvent = true;
54                    break;
55                }
56            }
57        }
58
59        if (! $isAddonEvent) {
60            return response()->json(['status' => 'Webhook Ignored'], 200);
61        }
62
63        $user = User::where('stripe_id', $eventObject->customer)->first();
64
65        if (! $user) {
66            Log::warning('User not found for Stripe customer ID: ', ['data' => $eventObject->customer]);
67
68            return response()->json(['status' => 'User not found.'], 404);
69        }
70
71        switch ($eventType) {
72            case 'customer.subscription.updated':
73                $subscription = $event->data->object;
74                $userAddon = UserAddOns::where('stripe_id', $subscription->id)->first();
75
76                // Corporate addons are inert to Stripe webhooks â€” they are not
77                // billed via Stripe. Guard against touching them in any branch.
78                if ($userAddon && $userAddon->source === UserAddOns::SOURCE_COMPANY) {
79                    Log::info('Ignoring Stripe webhook for corporate UserAddOn', ['id' => $userAddon->id]);
80                    break;
81                }
82
83                if (! $userAddon) {
84                    $user = User::where('stripe_id', $subscription->customer)->first();
85                    if ($user) {
86                        $stripeProductId = $eventObject->items->data[0]->price->product ?? null;
87                        $addOn = AddOns::where('stripe_id', $stripeProductId)->first();
88
89                        UserAddOns::create([
90                            'user_id' => $user->id,
91                            'add_on_id' => $addOn->id,
92                            'product' => 'RolePlay',
93                            'stripe_price_id' => $subscription->items->data[0]->price->id,
94                            'status' => $subscription->status,
95                            'starts_at' => now(),
96                            'ends_at' => \Carbon\Carbon::createFromTimestamp($subscription->current_period_end),
97                            'quantity' => 1,
98                            'stripe_id' => $subscription->id,
99                        ]);
100                    }
101                } else {
102                    $userAddon->update([
103                        'status' => $subscription->status,
104                        'stripe_price_id' => $subscription->items->data[0]->price->id,
105                        'ends_at' => \Carbon\Carbon::createFromTimestamp($subscription->current_period_end),
106                    ]);
107                }
108                break;
109
110            case 'customer.subscription.created':
111                $subscription = $event->data->object;
112                $stripeProductId = $eventObject->items->data[0]->price->product ?? null;
113                $addOn = AddOns::where('stripe_id', $stripeProductId)->first();
114
115                UserAddOns::create([
116                    'user_id' => $user->id,
117                    'add_on_id' => $addOn->id,
118                    'product' => 'RolePlay',
119                    'status' => $subscription->status,
120                    'stripe_price_id' => $subscription->items->data[0]->price->id ?? null,
121                    'starts_at' => now(),
122                    'ends_at' => \Carbon\Carbon::createFromTimestamp($subscription->current_period_end),
123                    'quantity' => 1,
124                    'stripe_id' => $subscription->id,
125                ]);
126                break;
127
128            case 'invoice.paid':
129                $invoice = $event->data->object;
130                $subscriptionId = $invoice->subscription;
131                $paidAt = \Carbon\Carbon::createFromTimestamp($invoice->created);
132
133                Invoices::create([
134                    'user_id' => $user->id,
135                    'stripe_id' => $invoice->id,
136                    'stripe_price_id' => $eventObject->lines->data[0]->price->id ?? null,
137                    'stripe_subscription_id' => $subscriptionId,
138                    'stripe_product_id' => $eventObject->lines->data[0]->price->product ?? null,
139                    'amount' => $invoice->amount_paid / 100,
140                    'currency' => strtoupper($invoice->currency),
141                    'paid_at' => $paidAt,
142                    'status' => 'paid',
143                ]);
144
145                $userAddon = UserAddOns::where('stripe_id', $subscriptionId)->first();
146
147                if ($userAddon && $userAddon->source !== UserAddOns::SOURCE_COMPANY) {
148                    $userAddon->update([
149                        'status' => 'active',
150                        'ends_at' => \Carbon\Carbon::createFromTimestamp($invoice->period_end),
151                    ]);
152                }
153                break;
154
155            case 'invoice.payment_failed':
156                $invoice = $event->data->object;
157                $subscriptionId = $invoice->subscription;
158
159                Invoices::create([
160                    'user_id' => $user->id,
161                    'stripe_id' => $invoice->id,
162                    'stripe_price_id' => $eventObject->lines->data[0]->price->id ?? null,
163                    'stripe_subscription_id' => $subscriptionId,
164                    'stripe_product_id' => $eventObject->lines->data[0]->price->product ?? null,
165                    'amount' => $invoice->amount_due / 100,
166                    'currency' => strtoupper($invoice->currency),
167                    'paid_at' => null,
168                    'status' => 'failed',
169                ]);
170
171                $userAddon = UserAddOns::where('stripe_id', $subscriptionId)->first();
172
173                if ($userAddon && $userAddon->source !== UserAddOns::SOURCE_COMPANY) {
174                    $userAddon->update(['status' => 'payment_failed']);
175                }
176                break;
177
178            case 'customer.subscription.deleted':
179                $subscription = $event->data->object;
180                $subscriptionId = $subscription->id;
181
182                $userAddon = UserAddOns::where('stripe_id', $subscriptionId)->first();
183
184                if ($userAddon && $userAddon->source === UserAddOns::SOURCE_COMPANY) {
185                    Log::info('Ignoring Stripe subscription.deleted for corporate UserAddOn', ['id' => $userAddon->id]);
186                    break;
187                }
188
189                if ($userAddon) {
190                    if ($subscription->cancel_at_period_end) {
191                        $userAddon->update(['status' => 'canceled_at_period_end']);
192                    } else {
193                        $userAddon->update(['status' => 'canceled']);
194                    }
195                }
196                break;
197        }
198
199        return response()->json(['status' => 'Webhook Handled'], 200);
200    }
201}