Skip to content

Commit a5f1c7b

Browse files
committed
Merge remote-tracking branch 'origin/main' into v2
2 parents a84edf3 + 265b002 commit a5f1c7b

30 files changed

+1443
-242
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+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource\Pages;
6+
use App\Models\WallOfLoveSubmission;
7+
use Filament\Forms;
8+
use Filament\Forms\Form;
9+
use Filament\Resources\Resource;
10+
use Filament\Tables;
11+
use Filament\Tables\Table;
12+
use Illuminate\Database\Eloquent\Builder;
13+
14+
class WallOfLoveSubmissionResource extends Resource
15+
{
16+
protected static ?string $model = WallOfLoveSubmission::class;
17+
18+
protected static ?string $navigationIcon = 'heroicon-o-heart';
19+
20+
protected static ?string $navigationLabel = 'Wall of Love';
21+
22+
protected static ?string $pluralModelLabel = 'Wall of Love Submissions';
23+
24+
public static function form(Form $form): Form
25+
{
26+
return $form
27+
->schema([
28+
Forms\Components\Section::make('Submission Details')
29+
->schema([
30+
Forms\Components\TextInput::make('name')
31+
->required()
32+
->maxLength(255),
33+
34+
Forms\Components\TextInput::make('company')
35+
->maxLength(255),
36+
37+
Forms\Components\FileUpload::make('photo_path')
38+
->label('Photo')
39+
->image()
40+
->disk('public')
41+
->directory('wall-of-love-photos'),
42+
43+
Forms\Components\TextInput::make('url')
44+
->label('Website/Social URL')
45+
->url()
46+
->maxLength(255),
47+
48+
Forms\Components\Textarea::make('testimonial')
49+
->maxLength(1000)
50+
->rows(4),
51+
]),
52+
53+
Forms\Components\Section::make('Review Information')
54+
->schema([
55+
Forms\Components\Select::make('user_id')
56+
->relationship('user', 'name')
57+
->required()
58+
->disabled(),
59+
60+
Forms\Components\DateTimePicker::make('approved_at')
61+
->label('Approved At'),
62+
63+
Forms\Components\Select::make('approved_by')
64+
->relationship('approvedBy', 'name')
65+
->label('Approved By'),
66+
67+
Forms\Components\Placeholder::make('created_at')
68+
->label('Submitted At')
69+
->content(fn (WallOfLoveSubmission $record): ?string => $record->created_at?->diffForHumans()),
70+
]),
71+
]);
72+
}
73+
74+
public static function table(Table $table): Table
75+
{
76+
return $table
77+
->columns([
78+
Tables\Columns\TextColumn::make('name')
79+
->searchable()
80+
->sortable(),
81+
82+
Tables\Columns\TextColumn::make('company')
83+
->searchable()
84+
->toggleable(),
85+
86+
Tables\Columns\TextColumn::make('user.name')
87+
->label('Submitted By')
88+
->searchable()
89+
->sortable(),
90+
91+
Tables\Columns\ImageColumn::make('photo_path')
92+
->label('Photo')
93+
->disk('public')
94+
->height(40)
95+
->toggleable(),
96+
97+
Tables\Columns\IconColumn::make('approved_at')
98+
->label('Status')
99+
->boolean()
100+
->trueIcon('heroicon-o-check-circle')
101+
->falseIcon('heroicon-o-clock')
102+
->trueColor('success')
103+
->falseColor('warning')
104+
->sortable(),
105+
106+
Tables\Columns\TextColumn::make('approvedBy.name')
107+
->label('Approved By')
108+
->toggleable(),
109+
110+
Tables\Columns\TextColumn::make('created_at')
111+
->label('Submitted')
112+
->dateTime()
113+
->sortable()
114+
->toggleable(),
115+
])
116+
->filters([
117+
Tables\Filters\TernaryFilter::make('approved_at')
118+
->label('Status')
119+
->placeholder('All submissions')
120+
->trueLabel('Approved')
121+
->falseLabel('Pending')
122+
->queries(
123+
true: fn (Builder $query) => $query->whereNotNull('approved_at'),
124+
false: fn (Builder $query) => $query->whereNull('approved_at'),
125+
),
126+
])
127+
->actions([
128+
Tables\Actions\Action::make('approve')
129+
->icon('heroicon-o-check')
130+
->color('success')
131+
->visible(fn (WallOfLoveSubmission $record) => $record->isPending())
132+
->action(fn (WallOfLoveSubmission $record) => $record->update([
133+
'approved_at' => now(),
134+
'approved_by' => auth()->id(),
135+
]))
136+
->requiresConfirmation()
137+
->modalHeading('Approve Submission')
138+
->modalDescription('Are you sure you want to approve this submission for the Wall of Love?'),
139+
140+
Tables\Actions\Action::make('unapprove')
141+
->icon('heroicon-o-x-mark')
142+
->color('warning')
143+
->visible(fn (WallOfLoveSubmission $record) => $record->isApproved())
144+
->action(fn (WallOfLoveSubmission $record) => $record->update([
145+
'approved_at' => null,
146+
'approved_by' => null,
147+
]))
148+
->requiresConfirmation()
149+
->modalHeading('Unapprove Submission')
150+
->modalDescription('Are you sure you want to unapprove this submission?'),
151+
152+
Tables\Actions\EditAction::make(),
153+
Tables\Actions\DeleteAction::make(),
154+
])
155+
->bulkActions([
156+
Tables\Actions\BulkActionGroup::make([
157+
Tables\Actions\BulkAction::make('approve')
158+
->icon('heroicon-o-check')
159+
->color('success')
160+
->action(function ($records) {
161+
$records->each(fn (WallOfLoveSubmission $record) => $record->update([
162+
'approved_at' => now(),
163+
'approved_by' => auth()->id(),
164+
]));
165+
})
166+
->requiresConfirmation()
167+
->modalHeading('Approve Selected Submissions')
168+
->modalDescription('Are you sure you want to approve all selected submissions?'),
169+
170+
Tables\Actions\DeleteBulkAction::make(),
171+
]),
172+
])
173+
->defaultSort('created_at', 'desc');
174+
}
175+
176+
public static function getRelations(): array
177+
{
178+
return [
179+
//
180+
];
181+
}
182+
183+
public static function getPages(): array
184+
{
185+
return [
186+
'index' => Pages\ListWallOfLoveSubmissions::route('/'),
187+
// 'create' => Pages\CreateWallOfLoveSubmission::route('/create'),
188+
'edit' => Pages\EditWallOfLoveSubmission::route('/{record}/edit'),
189+
];
190+
}
191+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\WallOfLoveSubmissionResource\Pages;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource;
6+
use Filament\Actions;
7+
use Filament\Resources\Pages\EditRecord;
8+
9+
class EditWallOfLoveSubmission extends EditRecord
10+
{
11+
protected static string $resource = WallOfLoveSubmissionResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
Actions\DeleteAction::make(),
17+
];
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\WallOfLoveSubmissionResource\Pages;
4+
5+
use App\Filament\Resources\WallOfLoveSubmissionResource;
6+
use Filament\Resources\Pages\ListRecords;
7+
8+
class ListWallOfLoveSubmissions extends ListRecords
9+
{
10+
protected static string $resource = WallOfLoveSubmissionResource::class;
11+
12+
protected function getHeaderActions(): array
13+
{
14+
return [
15+
];
16+
}
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
class WallOfLoveSubmissionController extends Controller
6+
{
7+
public function create()
8+
{
9+
// Check if user is eligible (has early adopter license)
10+
$hasEarlyAdopterLicense = auth()->user()
11+
->licenses()
12+
->where('created_at', '<', '2025-06-01')
13+
->exists();
14+
15+
if (! $hasEarlyAdopterLicense) {
16+
abort(404);
17+
}
18+
19+
// Check if user already has a submission
20+
$hasExistingSubmission = auth()->user()->wallOfLoveSubmissions()->exists();
21+
22+
if ($hasExistingSubmission) {
23+
return redirect()->route('customer.licenses')->with('info', 'You have already submitted your story to the Wall of Love.');
24+
}
25+
26+
return view('customer.wall-of-love.create');
27+
}
28+
}

0 commit comments

Comments
 (0)