Skip to content

Commit 842bffa

Browse files
committed
convert to cashier with user creation upon purchase
1 parent e1fc573 commit 842bffa

22 files changed

Lines changed: 903 additions & 376 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Models\User;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Contracts\Queue\ShouldQueue;
8+
use Illuminate\Foundation\Bus\Dispatchable;
9+
use Illuminate\Queue\InteractsWithQueue;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Facades\Hash;
12+
use Illuminate\Support\Str;
13+
use Laravel\Cashier\Cashier;
14+
use Stripe\Customer;
15+
16+
class CreateUserFromStripeCustomer implements ShouldQueue
17+
{
18+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
19+
20+
public function __construct(public Customer $customer) {}
21+
22+
public function handle(): void
23+
{
24+
if (Cashier::findBillable($this->customer)) {
25+
$this->fail("A user already exists for Stripe customer [{$this->customer->id}].");
26+
27+
return;
28+
}
29+
30+
if (User::query()->where('email', $this->customer->email)->exists()) {
31+
$this->fail("A user already exists for email [{$this->customer->email}].");
32+
33+
return;
34+
}
35+
36+
$user = new User;
37+
$user->name = $this->customer->name;
38+
$user->email = $this->customer->email;
39+
$user->stripe_id = $this->customer->id;
40+
// We will create a random password for the user and expect them to reset it.
41+
$user->password = Hash::make(Str::random(72));
42+
43+
$user->save();
44+
}
45+
}

app/Jobs/StripeWebhooks/HandleCustomerSubscriptionCreatedJob.php renamed to app/Jobs/HandleCustomerSubscriptionCreatedJob.php

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
<?php
22

3-
namespace App\Jobs\StripeWebhooks;
3+
namespace App\Jobs;
44

5-
use App\Jobs\CreateAnystackLicenseJob;
65
use Illuminate\Bus\Queueable;
76
use Illuminate\Contracts\Queue\ShouldQueue;
87
use Illuminate\Foundation\Bus\Dispatchable;
98
use Illuminate\Queue\InteractsWithQueue;
109
use Illuminate\Queue\SerializesModels;
11-
use Spatie\WebhookClient\Models\WebhookCall;
12-
use Stripe\Event;
13-
use Stripe\StripeClient;
10+
use Laravel\Cashier\Cashier;
11+
use Laravel\Cashier\Events\WebhookHandled;
1412
use Stripe\Subscription;
1513

1614
class HandleCustomerSubscriptionCreatedJob implements ShouldQueue
1715
{
1816
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
1917

20-
public function __construct(public WebhookCall $webhook) {}
18+
public function __construct(public WebhookHandled $webhook) {}
2119

2220
public function handle(): void
2321
{
@@ -29,19 +27,17 @@ public function handle(): void
2927
return;
3028
}
3129

32-
$customer = app(StripeClient::class)
33-
->customers
34-
->retrieve($stripeSubscription->customer);
30+
$user = Cashier::findBillable($stripeSubscription->customer);
3531

36-
if (! $customer || ! ($email = $customer->email)) {
37-
$this->fail('Failed to retrieve customer information or customer has no email.');
32+
if (! $user || ! ($email = $user->email)) {
33+
$this->fail('Failed to find user from Stripe subscription customer.');
3834

3935
return;
4036
}
4137

4238
$subscriptionPlan = \App\Enums\Subscription::fromStripeSubscription($stripeSubscription);
4339

44-
$nameParts = explode(' ', $customer->name ?? '', 2);
40+
$nameParts = explode(' ', $user->name ?? '', 2);
4541
$firstName = $nameParts[0] ?: null;
4642
$lastName = $nameParts[1] ?? null;
4743

@@ -55,6 +51,6 @@ public function handle(): void
5551

5652
protected function constructStripeSubscription(): ?Subscription
5753
{
58-
return Event::constructFrom($this->webhook->payload)->data?->object;
54+
return Subscription::constructFrom($this->webhook->payload['data']['object']);
5955
}
6056
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Listeners;
4+
5+
use App\Jobs\HandleCustomerSubscriptionCreatedJob;
6+
use Illuminate\Support\Facades\Log;
7+
use Laravel\Cashier\Events\WebhookHandled;
8+
9+
class StripeWebhookHandledListener
10+
{
11+
public function handle(WebhookHandled $event): void
12+
{
13+
Log::debug('Webhook handled', $event->payload);
14+
15+
match ($event->payload['type']) {
16+
'customer.subscription.created' => dispatch(new HandleCustomerSubscriptionCreatedJob($event)),
17+
default => null,
18+
};
19+
}
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace App\Listeners;
4+
5+
use App\Jobs\CreateUserFromStripeCustomer;
6+
use Illuminate\Support\Facades\Log;
7+
use Laravel\Cashier\Events\WebhookReceived;
8+
use Stripe\Customer;
9+
10+
class StripeWebhookReceivedListener
11+
{
12+
public function handle(WebhookReceived $event): void
13+
{
14+
Log::debug('Webhook received', $event->payload);
15+
16+
match ($event->payload['type']) {
17+
// 'customer.created' must be dispatched sync so the user is
18+
// created before the cashier webhook handling is executed.
19+
'customer.created' => dispatch_sync(new CreateUserFromStripeCustomer(
20+
Customer::constructFrom($event->payload['data']['object'])
21+
)),
22+
default => null,
23+
};
24+
}
25+
}

app/Models/User.php

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,20 @@
66
use Illuminate\Database\Eloquent\Factories\HasFactory;
77
use Illuminate\Foundation\Auth\User as Authenticatable;
88
use Illuminate\Notifications\Notifiable;
9+
use Laravel\Cashier\Billable;
910
use Laravel\Sanctum\HasApiTokens;
1011

1112
class User extends Authenticatable
1213
{
13-
use HasApiTokens, HasFactory, Notifiable;
14+
use Billable, HasApiTokens, HasFactory, Notifiable;
1415

15-
/**
16-
* The attributes that are mass assignable.
17-
*
18-
* @var array<int, string>
19-
*/
20-
protected $fillable = [
21-
'name',
22-
'email',
23-
'password',
24-
];
16+
protected $guarded = [];
2517

26-
/**
27-
* The attributes that should be hidden for serialization.
28-
*
29-
* @var array<int, string>
30-
*/
3118
protected $hidden = [
3219
'password',
3320
'remember_token',
3421
];
3522

36-
/**
37-
* The attributes that should be cast.
38-
*
39-
* @var array<string, string>
40-
*/
4123
protected $casts = [
4224
'email_verified_at' => 'datetime',
4325
'password' => 'hashed',

app/Providers/EventServiceProvider.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace App\Providers;
44

5+
use App\Listeners\StripeWebhookHandledListener;
6+
use App\Listeners\StripeWebhookReceivedListener;
57
use Illuminate\Auth\Events\Registered;
68
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
79
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
810
use Illuminate\Support\Facades\Event;
11+
use Laravel\Cashier\Events\WebhookHandled;
12+
use Laravel\Cashier\Events\WebhookReceived;
913

1014
class EventServiceProvider extends ServiceProvider
1115
{
@@ -18,6 +22,12 @@ class EventServiceProvider extends ServiceProvider
1822
Registered::class => [
1923
SendEmailVerificationNotification::class,
2024
],
25+
WebhookReceived::class => [
26+
StripeWebhookReceivedListener::class,
27+
],
28+
WebhookHandled::class => [
29+
StripeWebhookHandledListener::class,
30+
],
2131
];
2232

2333
/**

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@
22
"name": "nativephp/nativephp.com",
33
"type": "project",
44
"description": "The NativePHP website",
5-
"keywords": ["laravel", "nativephp"],
5+
"keywords": [
6+
"laravel",
7+
"nativephp"
8+
],
69
"license": "MIT",
710
"require": {
811
"php": "^8.1",
912
"artesaos/seotools": "^1.2",
1013
"blade-ui-kit/blade-heroicons": "^2.3",
1114
"guzzlehttp/guzzle": "^7.2",
15+
"laravel/cashier": "^15.6",
1216
"laravel/framework": "^10.10",
1317
"laravel/sanctum": "^3.2",
1418
"laravel/tinker": "^2.8",
1519
"league/commonmark": "^2.4",
1620
"livewire/livewire": "^3.6",
1721
"spatie/laravel-menu": "^4.1",
18-
"spatie/laravel-stripe-webhooks": "^3.10",
1922
"spatie/yaml-front-matter": "^2.0",
20-
"stripe/stripe-php": "^17.1",
2123
"torchlight/torchlight-commonmark": "^0.5.5"
2224
},
2325
"require-dev": {

0 commit comments

Comments
 (0)