Skip to content

Commit ff6ba97

Browse files
committed
Merge branch 'v3' of https://github.com/NativePHP/nativephp.com into v3
2 parents aaf825c + 845da14 commit ff6ba97

214 files changed

Lines changed: 17096 additions & 870 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.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\User;
6+
use Illuminate\Console\Command;
7+
8+
class GeneratePluginLicenseKeys extends Command
9+
{
10+
/**
11+
* The name and signature of the console command.
12+
*
13+
* @var string
14+
*/
15+
protected $signature = 'plugins:generate-license-keys
16+
{--force : Regenerate keys for all users, even those who already have one}';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'Generate plugin license keys for all users who do not have one';
24+
25+
/**
26+
* Execute the console command.
27+
*/
28+
public function handle(): int
29+
{
30+
$force = $this->option('force');
31+
32+
$query = User::query();
33+
34+
if (! $force) {
35+
$query->whereNull('plugin_license_key');
36+
}
37+
38+
$count = $query->count();
39+
40+
if ($count === 0) {
41+
$this->info('All users already have plugin license keys.');
42+
43+
return self::SUCCESS;
44+
}
45+
46+
$this->info("Generating plugin license keys for {$count} users...");
47+
48+
$bar = $this->output->createProgressBar($count);
49+
$bar->start();
50+
51+
$query->chunkById(100, function ($users) use ($bar) {
52+
foreach ($users as $user) {
53+
$user->getPluginLicenseKey();
54+
$bar->advance();
55+
}
56+
});
57+
58+
$bar->finish();
59+
$this->newLine();
60+
61+
$this->info("Generated plugin license keys for {$count} users.");
62+
63+
return self::SUCCESS;
64+
}
65+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\PayoutStatus;
6+
use App\Models\PluginPayout;
7+
use App\Services\StripeConnectService;
8+
use Illuminate\Console\Command;
9+
10+
class RetryFailedPayouts extends Command
11+
{
12+
protected $signature = 'payouts:retry-failed {--payout-id= : Retry a specific payout}';
13+
14+
protected $description = 'Retry failed plugin payouts';
15+
16+
public function handle(StripeConnectService $stripeConnectService): int
17+
{
18+
$payoutId = $this->option('payout-id');
19+
20+
if ($payoutId) {
21+
$payout = PluginPayout::find($payoutId);
22+
23+
if (! $payout) {
24+
$this->error("Payout #{$payoutId} not found.");
25+
26+
return self::FAILURE;
27+
}
28+
29+
if (! $payout->isFailed()) {
30+
$this->error("Payout #{$payoutId} is not in failed status.");
31+
32+
return self::FAILURE;
33+
}
34+
35+
return $this->retryPayout($payout, $stripeConnectService);
36+
}
37+
38+
$failedPayouts = PluginPayout::failed()
39+
->with(['pluginLicense', 'developerAccount'])
40+
->get();
41+
42+
if ($failedPayouts->isEmpty()) {
43+
$this->info('No failed payouts to retry.');
44+
45+
return self::SUCCESS;
46+
}
47+
48+
$this->info("Found {$failedPayouts->count()} failed payout(s) to retry.");
49+
50+
$succeeded = 0;
51+
$failed = 0;
52+
53+
foreach ($failedPayouts as $payout) {
54+
// Reset status to pending before retrying
55+
$payout->update(['status' => PayoutStatus::Pending]);
56+
57+
if ($stripeConnectService->processTransfer($payout)) {
58+
$this->info("Payout #{$payout->id} succeeded.");
59+
$succeeded++;
60+
} else {
61+
$this->error("Payout #{$payout->id} failed again.");
62+
$failed++;
63+
}
64+
}
65+
66+
$this->newLine();
67+
$this->info("Results: {$succeeded} succeeded, {$failed} failed.");
68+
69+
return $failed > 0 ? self::FAILURE : self::SUCCESS;
70+
}
71+
72+
protected function retryPayout(PluginPayout $payout, StripeConnectService $stripeConnectService): int
73+
{
74+
$this->info("Retrying payout #{$payout->id}...");
75+
76+
// Reset status to pending before retrying
77+
$payout->update(['status' => PayoutStatus::Pending]);
78+
79+
if ($stripeConnectService->processTransfer($payout)) {
80+
$this->info('Payout succeeded!');
81+
82+
return self::SUCCESS;
83+
}
84+
85+
$this->error('Payout failed again.');
86+
87+
return self::FAILURE;
88+
}
89+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Services\SatisService;
6+
use Illuminate\Console\Command;
7+
8+
class SatisBuild extends Command
9+
{
10+
/**
11+
* The name and signature of the console command.
12+
*
13+
* @var string
14+
*/
15+
protected $signature = 'satis:build
16+
{--plugin= : Build only a specific plugin by name (e.g., vendor/package)}';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'Trigger a Satis repository build';
24+
25+
/**
26+
* Execute the console command.
27+
*/
28+
public function handle(SatisService $satisService): int
29+
{
30+
$pluginName = $this->option('plugin');
31+
32+
if ($pluginName) {
33+
$plugin = \App\Models\Plugin::where('name', $pluginName)->first();
34+
35+
if (! $plugin) {
36+
$this->error("Plugin '{$pluginName}' not found.");
37+
38+
return self::FAILURE;
39+
}
40+
41+
if (! $plugin->isApproved()) {
42+
$this->error("Plugin '{$pluginName}' is not approved.");
43+
44+
return self::FAILURE;
45+
}
46+
47+
$this->info("Triggering Satis build for: {$pluginName}");
48+
$result = $satisService->build([$plugin]);
49+
} else {
50+
$this->info('Triggering Satis build for all approved plugins...');
51+
$result = $satisService->buildAll();
52+
}
53+
54+
if ($result['success']) {
55+
$this->info('Build triggered successfully!');
56+
$this->line("Job ID: {$result['job_id']}");
57+
58+
if (isset($result['plugins_count'])) {
59+
$this->line("Plugins: {$result['plugins_count']}");
60+
}
61+
62+
return self::SUCCESS;
63+
}
64+
65+
$this->error('Build trigger failed: '.$result['error']);
66+
67+
if (isset($result['status'])) {
68+
$this->line("HTTP Status: {$result['status']}");
69+
}
70+
71+
$this->line('API URL: '.config('services.satis.url'));
72+
$this->line('API Key configured: '.(config('services.satis.api_key') ? 'Yes' : 'No'));
73+
74+
return self::FAILURE;
75+
}
76+
}

app/Console/Commands/SendLicenseExpiryWarnings.php

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@
88

99
class SendLicenseExpiryWarnings extends Command
1010
{
11-
protected $signature = 'licenses:send-expiry-warnings';
11+
protected $signature = 'licenses:send-expiry-warnings {--catch-up : Send missed warnings for licenses within warning windows}';
1212

1313
protected $description = 'Send expiry warning emails for licenses that are expiring soon';
1414

1515
public function handle(): int
1616
{
1717
$warningDays = [30, 7, 1];
1818
$totalSent = 0;
19+
$catchUp = $this->option('catch-up');
20+
21+
if ($catchUp) {
22+
$this->info('Running in catch-up mode - sending missed warnings...');
23+
}
1924

2025
foreach ($warningDays as $days) {
21-
$sent = $this->sendWarningsForDays($days);
26+
$sent = $this->sendWarningsForDays($days, $catchUp);
2227
$totalSent += $sent;
2328

2429
$this->info("Sent {$sent} warning emails for licenses expiring in {$days} day(s)");
@@ -29,24 +34,38 @@ public function handle(): int
2934
return Command::SUCCESS;
3035
}
3136

32-
private function sendWarningsForDays(int $days): int
37+
private function sendWarningsForDays(int $days, bool $catchUp = false): int
3338
{
34-
$targetDate = now()->addDays($days)->startOfDay();
3539
$sent = 0;
3640

37-
// Find licenses that:
38-
// 1. Expire on the target date
39-
// 2. Don't have an active subscription (legacy licenses)
40-
// 3. Haven't been sent a warning for this specific day count recently
41-
$licenses = License::query()
42-
->whereDate('expires_at', $targetDate)
41+
$query = License::query()
4342
->whereNull('subscription_item_id') // Legacy licenses without subscriptions
44-
->whereDoesntHave('expiryWarnings', function ($query) use ($days) {
45-
$query->where('warning_days', $days)
46-
->where('sent_at', '>=', now()->subHours(23)); // Prevent duplicate emails within 23 hours
47-
})
48-
->with('user')
49-
->get();
43+
->with('user');
44+
45+
if ($catchUp) {
46+
// Catch-up mode: find licenses that are within the warning window but haven't received this warning yet
47+
// For 30-day: expires within 30 days (but more than 7 days to avoid overlap)
48+
// For 7-day: expires within 7 days (but more than 1 day)
49+
// For 1-day: expires within 1 day (but hasn't expired yet)
50+
$warningThresholds = [30 => 7, 7 => 1, 1 => 0];
51+
$lowerBound = $warningThresholds[$days] ?? 0;
52+
53+
$query->where('expires_at', '>', now()->addDays($lowerBound)->startOfDay())
54+
->where('expires_at', '<=', now()->addDays($days)->endOfDay())
55+
->whereDoesntHave('expiryWarnings', function ($q) use ($days) {
56+
$q->where('warning_days', $days);
57+
});
58+
} else {
59+
// Normal mode: only licenses expiring on the exact target date
60+
$targetDate = now()->addDays($days)->startOfDay();
61+
$query->whereDate('expires_at', $targetDate)
62+
->whereDoesntHave('expiryWarnings', function ($q) use ($days) {
63+
$q->where('warning_days', $days)
64+
->where('sent_at', '>=', now()->subHours(23)); // Prevent duplicate emails within 23 hours
65+
});
66+
}
67+
68+
$licenses = $query->get();
5069

5170
foreach ($licenses as $license) {
5271
if ($license->user) {
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\SyncPlugin;
6+
use App\Models\Plugin;
7+
use Illuminate\Console\Command;
8+
9+
class SyncPlugins extends Command
10+
{
11+
protected $signature = 'plugins:sync';
12+
13+
protected $description = 'Dispatch jobs to sync all plugins from their repositories';
14+
15+
public function handle(): int
16+
{
17+
$plugins = Plugin::all();
18+
19+
$count = $plugins->count();
20+
21+
if ($count === 0) {
22+
$this->info('No plugins to sync.');
23+
24+
return self::SUCCESS;
25+
}
26+
27+
$this->info("Dispatching sync jobs for {$count} plugins...");
28+
29+
$plugins->each(fn (Plugin $plugin) => SyncPlugin::dispatch($plugin));
30+
31+
$this->info('Done. Jobs have been dispatched to the queue.');
32+
33+
return self::SUCCESS;
34+
}
35+
}

app/Console/Kernel.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ protected function schedule(Schedule $schedule): void
2323
->dailyAt('10:30')
2424
->onOneServer()
2525
->runInBackground();
26+
27+
// Remove GitHub access for users with expired Max licenses
28+
$schedule->command('github:remove-expired-access')
29+
->dailyAt('10:00')
30+
->onOneServer()
31+
->runInBackground();
32+
33+
// Remove Discord Max role for users with expired Max licenses
34+
$schedule->command('discord:remove-expired-roles')
35+
->dailyAt('10:30')
36+
->onOneServer()
37+
->runInBackground();
2638
}
2739

2840
/**

app/Enums/PayoutStatus.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum PayoutStatus: string
6+
{
7+
case Pending = 'pending';
8+
case Transferred = 'transferred';
9+
case Failed = 'failed';
10+
11+
public function label(): string
12+
{
13+
return match ($this) {
14+
self::Pending => 'Pending',
15+
self::Transferred => 'Transferred',
16+
self::Failed => 'Failed',
17+
};
18+
}
19+
20+
public function color(): string
21+
{
22+
return match ($this) {
23+
self::Pending => 'yellow',
24+
self::Transferred => 'green',
25+
self::Failed => 'red',
26+
};
27+
}
28+
}

0 commit comments

Comments
 (0)