Skip to content

Commit 0f197cd

Browse files
committed
Match users with Stripe customers
1 parent 9a9394f commit 0f197cd

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\User;
6+
use Illuminate\Console\Command;
7+
use Stripe\Customer;
8+
use Stripe\Exception\ApiErrorException;
9+
10+
class MatchUsersWithStripeCustomers extends Command
11+
{
12+
protected $signature = 'users:match-stripe-customers
13+
{--limit= : Limit the number of users to process}
14+
{--dry-run : Show what would be updated without making changes}';
15+
16+
protected $description = 'Match users without Stripe IDs to their Stripe customer records';
17+
18+
public function handle(): int
19+
{
20+
$query = User::whereNull('stripe_id');
21+
22+
$totalUsers = $query->count();
23+
24+
if ($totalUsers === 0) {
25+
$this->info('No users found without Stripe IDs.');
26+
27+
return self::SUCCESS;
28+
}
29+
30+
$this->info("Found {$totalUsers} users without Stripe IDs.");
31+
32+
$limit = $this->option('limit');
33+
if ($limit) {
34+
$query->limit((int) $limit);
35+
$this->info("Processing first {$limit} users...");
36+
}
37+
38+
$dryRun = $this->option('dry-run');
39+
if ($dryRun) {
40+
$this->warn('DRY RUN MODE - No changes will be made');
41+
}
42+
43+
$users = $query->get();
44+
45+
$matched = 0;
46+
$notFound = 0;
47+
$errors = 0;
48+
49+
$progressBar = $this->output->createProgressBar($users->count());
50+
$progressBar->start();
51+
52+
/** @var User $user */
53+
foreach ($users as $user) {
54+
try {
55+
/** @var Customer $customer */
56+
$customer = $user->findStripeCustomerRecords()->first(fn (Customer $result) => $result->next_invoice_sequence === 1);
57+
58+
if ($customer) {
59+
$matched++;
60+
61+
if (! $dryRun) {
62+
$user->update(['stripe_id' => $customer->id]);
63+
}
64+
65+
$this->newLine();
66+
$this->line(" ✓ Matched: {$user->email}{$customer->id}");
67+
} else {
68+
$notFound++;
69+
$this->newLine();
70+
$this->line(" - No match: {$user->email}");
71+
}
72+
} catch (ApiErrorException $e) {
73+
$errors++;
74+
$this->newLine();
75+
$this->error(" ✗ Error for {$user->email}: {$e->getMessage()}");
76+
} catch (\Exception $e) {
77+
$errors++;
78+
$this->newLine();
79+
$this->error(" ✗ Unexpected error for {$user->email}: {$e->getMessage()}");
80+
}
81+
82+
$progressBar->advance();
83+
84+
// Add a small delay to avoid rate limiting
85+
usleep(100000); // 0.1 seconds
86+
}
87+
88+
$progressBar->finish();
89+
$this->newLine(2);
90+
91+
// Summary
92+
$this->info('Summary:');
93+
$this->table(
94+
['Status', 'Count'],
95+
[
96+
['Matched', $matched],
97+
['Not Found', $notFound],
98+
['Errors', $errors],
99+
['Total Processed', $users->count()],
100+
]
101+
);
102+
103+
if ($dryRun) {
104+
$this->warn('This was a dry run. Run without --dry-run to apply changes.');
105+
}
106+
107+
return self::SUCCESS;
108+
}
109+
}

app/Models/User.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use Illuminate\Database\Eloquent\Relations\HasMany;
1010
use Illuminate\Foundation\Auth\User as Authenticatable;
1111
use Illuminate\Notifications\Notifiable;
12+
use Illuminate\Support\Collection;
1213
use Laravel\Cashier\Billable;
1314
use Laravel\Sanctum\HasApiTokens;
15+
use Stripe\Customer;
1416

1517
class User extends Authenticatable implements FilamentUser
1618
{
@@ -67,4 +69,13 @@ public function getLastNameAttribute(): ?string
6769

6870
return $nameParts[1] ?? null;
6971
}
72+
73+
public function findStripeCustomerRecords(): Collection
74+
{
75+
$search = static::stripe()->customers->search([
76+
'query' => 'email:"'.$this->email.'"',
77+
]);
78+
79+
return collect($search->data);
80+
}
7081
}

0 commit comments

Comments
 (0)