Skip to content

Commit d086097

Browse files
committed
Merge remote-tracking branch 'origin/main' into ultra
# Conflicts: # app/Http/Controllers/CustomerLicenseController.php # app/Http/Controllers/CustomerPluginController.php # app/Jobs/HandleInvoicePaidJob.php # app/Livewire/MobilePricing.php # app/Models/User.php # resources/views/auth/register.blade.php # resources/views/components/navigation-bar.blade.php # routes/web.php
2 parents b116520 + 23b38c7 commit d086097

286 files changed

Lines changed: 13319 additions & 2523 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/Console/Commands/ExtendLicenseExpiryCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Console\Commands;
44

5+
use App\Jobs\UpdateAnystackLicenseExpiryJob;
56
use App\Models\License;
67
use Illuminate\Console\Command;
78

@@ -37,7 +38,7 @@ public function handle(): int
3738
}
3839

3940
// Dispatch the job to update the license expiry
40-
dispatch(new \App\Jobs\UpdateAnystackLicenseExpiryJob($license));
41+
dispatch(new UpdateAnystackLicenseExpiryJob($license));
4142

4243
$this->info("License expiry updated to {$license->expires_at->format('Y-m-d')}");
4344

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Plugin;
6+
use App\Models\PluginBundle;
7+
use App\Models\PluginLicense;
8+
use App\Models\User;
9+
use App\Notifications\BundlePluginAdded;
10+
use Illuminate\Console\Command;
11+
12+
class GrantPluginToBundleOwners extends Command
13+
{
14+
protected $signature = 'plugins:grant-to-bundle-owners
15+
{bundle : The bundle slug}
16+
{plugin : The plugin name (vendor/package)}
17+
{--dry-run : Preview what would happen without making changes}
18+
{--no-email : Grant the plugin without sending notification emails}';
19+
20+
protected $description = 'Grant a plugin to all users who have purchased a specific bundle and notify them via email';
21+
22+
public function handle(): int
23+
{
24+
$bundle = PluginBundle::where('slug', $this->argument('bundle'))->first();
25+
26+
if (! $bundle) {
27+
$this->error("Bundle not found: {$this->argument('bundle')}");
28+
29+
return Command::FAILURE;
30+
}
31+
32+
$plugin = Plugin::where('name', $this->argument('plugin'))->first();
33+
34+
if (! $plugin) {
35+
$this->error("Plugin not found: {$this->argument('plugin')}");
36+
37+
return Command::FAILURE;
38+
}
39+
40+
$dryRun = $this->option('dry-run');
41+
$noEmail = $this->option('no-email');
42+
43+
// Find all unique users who have purchased this bundle
44+
// (they have at least one active PluginLicense linked to this bundle)
45+
$userIds = PluginLicense::where('plugin_bundle_id', $bundle->id)
46+
->active()
47+
->distinct()
48+
->pluck('user_id');
49+
50+
$users = User::whereIn('id', $userIds)->get();
51+
52+
if ($users->isEmpty()) {
53+
$this->warn('No users found who have purchased this bundle.');
54+
55+
return Command::SUCCESS;
56+
}
57+
58+
$this->info("Bundle: {$bundle->name} (slug: {$bundle->slug})");
59+
$this->info("Plugin: {$plugin->name}");
60+
$this->info("Users found: {$users->count()}");
61+
62+
if ($dryRun) {
63+
$this->warn('[DRY RUN] No changes will be made.');
64+
}
65+
66+
$this->newLine();
67+
68+
$granted = 0;
69+
$skipped = 0;
70+
71+
foreach ($users as $user) {
72+
// Check if user already has an active license for this plugin
73+
$existingLicense = PluginLicense::where('user_id', $user->id)
74+
->where('plugin_id', $plugin->id)
75+
->active()
76+
->exists();
77+
78+
if ($existingLicense) {
79+
$this->line(" Skipped {$user->email} — already has an active license");
80+
$skipped++;
81+
82+
continue;
83+
}
84+
85+
if (! $dryRun) {
86+
PluginLicense::create([
87+
'user_id' => $user->id,
88+
'plugin_id' => $plugin->id,
89+
'plugin_bundle_id' => $bundle->id,
90+
'price_paid' => 0,
91+
'currency' => 'USD',
92+
'is_grandfathered' => true,
93+
'purchased_at' => now(),
94+
]);
95+
96+
if (! $noEmail) {
97+
$user->notify(
98+
(new BundlePluginAdded($plugin, $bundle))
99+
->delay(now()->addSeconds($granted * 2))
100+
);
101+
}
102+
}
103+
104+
$this->line(" Granted to {$user->email}");
105+
$granted++;
106+
}
107+
108+
$this->newLine();
109+
$this->info("Granted: {$granted}");
110+
$this->info("Skipped (already licensed): {$skipped}");
111+
112+
if ($dryRun) {
113+
$this->warn('This was a dry run. Run again without --dry-run to apply changes.');
114+
}
115+
116+
return Command::SUCCESS;
117+
}
118+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Product;
6+
use App\Models\ProductLicense;
7+
use App\Models\User;
8+
use App\Notifications\ProductGranted;
9+
use Illuminate\Console\Command;
10+
11+
class GrantProduct extends Command
12+
{
13+
protected $signature = 'products:grant
14+
{product : The product slug}
15+
{user : The user email}
16+
{--dry-run : Preview what would happen without making changes}
17+
{--no-email : Grant the product without sending a notification email}';
18+
19+
protected $description = 'Grant a product to a user by email';
20+
21+
public function handle(): int
22+
{
23+
$product = Product::where('slug', $this->argument('product'))->first();
24+
25+
if (! $product) {
26+
$this->error("Product not found: {$this->argument('product')}");
27+
28+
return Command::FAILURE;
29+
}
30+
31+
$user = User::where('email', $this->argument('user'))->first();
32+
33+
if (! $user) {
34+
$this->error("User not found: {$this->argument('user')}");
35+
36+
return Command::FAILURE;
37+
}
38+
39+
$dryRun = $this->option('dry-run');
40+
$noEmail = $this->option('no-email');
41+
42+
$this->info("Product: {$product->name} (slug: {$product->slug})");
43+
$this->info("User: {$user->email}");
44+
45+
if ($dryRun) {
46+
$this->warn('[DRY RUN] No changes will be made.');
47+
}
48+
49+
$this->newLine();
50+
51+
// Check if user already has a license for this product
52+
$existingLicense = ProductLicense::where('user_id', $user->id)
53+
->where('product_id', $product->id)
54+
->exists();
55+
56+
if ($existingLicense) {
57+
$this->warn("User {$user->email} already has a license for this product.");
58+
59+
return Command::SUCCESS;
60+
}
61+
62+
if (! $dryRun) {
63+
ProductLicense::create([
64+
'user_id' => $user->id,
65+
'product_id' => $product->id,
66+
'price_paid' => 0,
67+
'currency' => 'USD',
68+
'is_comped' => true,
69+
'purchased_at' => now(),
70+
]);
71+
72+
if (! $noEmail) {
73+
$user->notify(new ProductGranted($product));
74+
}
75+
}
76+
77+
$this->info("Granted to {$user->email}");
78+
79+
if ($dryRun) {
80+
$this->warn('This was a dry run. Run again without --dry-run to apply changes.');
81+
}
82+
83+
return Command::SUCCESS;
84+
}
85+
}

app/Console/Commands/MatchUsersWithStripeCustomers.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\User;
66
use Illuminate\Console\Command;
7+
use Illuminate\Support\Sleep;
78
use Stripe\Customer;
89
use Stripe\Exception\ApiErrorException;
910

@@ -82,7 +83,7 @@ public function handle(): int
8283
$progressBar->advance();
8384

8485
// Add a small delay to avoid rate limiting
85-
\Illuminate\Support\Sleep::usleep(100000); // 0.1 seconds
86+
Sleep::usleep(100000); // 0.1 seconds
8687
}
8788

8889
$progressBar->finish();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Jobs\ProcessPayoutTransfer;
6+
use App\Models\PluginPayout;
7+
use Illuminate\Console\Command;
8+
9+
class ProcessEligiblePayouts extends Command
10+
{
11+
protected $signature = 'payouts:process-eligible';
12+
13+
protected $description = 'Dispatch transfer jobs for pending payouts that have passed the 15-day holding period';
14+
15+
public function handle(): int
16+
{
17+
$eligiblePayouts = PluginPayout::pending()
18+
->where('eligible_for_payout_at', '<=', now())
19+
->get();
20+
21+
if ($eligiblePayouts->isEmpty()) {
22+
$this->info('No eligible payouts to process.');
23+
24+
return self::SUCCESS;
25+
}
26+
27+
foreach ($eligiblePayouts as $payout) {
28+
ProcessPayoutTransfer::dispatch($payout);
29+
}
30+
31+
$this->info("Dispatched {$eligiblePayouts->count()} payout transfer job(s).");
32+
33+
return self::SUCCESS;
34+
}
35+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Lead;
6+
use App\Notifications\NewLeadSubmitted;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Support\Carbon;
9+
use Illuminate\Support\Facades\Notification;
10+
11+
class ResendLeadNotifications extends Command
12+
{
13+
protected $signature = 'app:resend-lead-notifications {date : The date to resend leads from (Y-m-d)}';
14+
15+
protected $description = 'Re-send lead notifications to the sales email for leads submitted on or after a given date.';
16+
17+
public function handle(): void
18+
{
19+
$date = Carbon::createFromFormat('Y-m-d', $this->argument('date'))->startOfDay();
20+
21+
$leads = Lead::query()
22+
->where('created_at', '>=', $date)
23+
->orderBy('created_at')
24+
->get();
25+
26+
if ($leads->isEmpty()) {
27+
$this->info('No leads found from '.$date->toDateString().' onwards.');
28+
29+
return;
30+
}
31+
32+
$this->info("Found {$leads->count()} lead(s) from {$date->toDateString()} onwards.");
33+
34+
foreach ($leads as $lead) {
35+
Notification::route('mail', 'sales@nativephp.com')
36+
->notify(new NewLeadSubmitted($lead));
37+
38+
$this->line("Sent notification for lead: {$lead->company} ({$lead->email})");
39+
}
40+
41+
$this->info('Done.');
42+
}
43+
}

app/Console/Commands/SatisBuild.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Console\Commands;
44

5+
use App\Models\Plugin;
56
use App\Services\SatisService;
67
use Illuminate\Console\Command;
78

@@ -30,7 +31,7 @@ public function handle(SatisService $satisService): int
3031
$pluginName = $this->option('plugin');
3132

3233
if ($pluginName) {
33-
$plugin = \App\Models\Plugin::where('name', $pluginName)->first();
34+
$plugin = Plugin::where('name', $pluginName)->first();
3435

3536
if (! $plugin) {
3637
$this->error("Plugin '{$pluginName}' not found.");

app/Console/Commands/SendLegacyLicenseThankYou.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\License;
66
use App\Notifications\LegacyLicenseThankYou;
77
use Illuminate\Console\Command;
8+
use Illuminate\Support\Facades\Date;
89

910
class SendLegacyLicenseThankYou extends Command
1011
{
@@ -25,7 +26,7 @@ public function handle(): int
2526
// that have a user and haven't been converted to a subscription
2627
$legacyLicenses = License::query()
2728
->whereNull('subscription_item_id')
28-
->where('created_at', '<', \Illuminate\Support\Facades\Date::create(2025, 5, 8))
29+
->where('created_at', '<', Date::create(2025, 5, 8))
2930
->whereHas('user')
3031
->with('user')
3132
->get();

app/Console/Commands/SyncPlugins.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Console\Commands;
44

5+
use App\Jobs\SyncPlugin;
56
use App\Models\Plugin;
67
use Illuminate\Console\Command;
78

@@ -25,7 +26,7 @@ public function handle(): int
2526

2627
$this->info("Dispatching sync jobs for {$count} plugins...");
2728

28-
$plugins->each(fn (Plugin $plugin) => dispatch(new \App\Jobs\SyncPlugin($plugin)));
29+
$plugins->each(fn (Plugin $plugin) => dispatch(new SyncPlugin($plugin)));
2930

3031
$this->info('Done. Jobs have been dispatched to the queue.');
3132

app/Console/Kernel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ protected function schedule(Schedule $schedule): void
2323
->dailyAt('10:30')
2424
->onOneServer()
2525
->runInBackground();
26+
27+
// Process developer payouts that have passed the 15-day holding period
28+
$schedule->command('payouts:process-eligible')
29+
->dailyAt('11:00')
30+
->onOneServer();
2631
}
2732

2833
/**

0 commit comments

Comments
 (0)