Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 120 additions & 172 deletions .cursor/rules/laravel-boost.mdc

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ STRIPE_MINI_PRICE_ID_EAP=
STRIPE_PRO_PRICE_ID=
STRIPE_PRO_PRICE_ID_EAP=
STRIPE_MAX_PRICE_ID=
STRIPE_MAX_PRICE_ID_MONTHLY=
STRIPE_MAX_PRICE_ID_EAP=
STRIPE_ULTRA_COMP_PRICE_ID=
STRIPE_EXTRA_SEAT_PRICE_ID=
STRIPE_EXTRA_SEAT_PRICE_ID_MONTHLY=
STRIPE_FOREVER_PRICE_ID=
STRIPE_TRIAL_PRICE_ID=
STRIPE_MINI_PAYMENT_LINK=
Expand Down
292 changes: 120 additions & 172 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

292 changes: 120 additions & 172 deletions .junie/guidelines.md

Large diffs are not rendered by default.

375 changes: 375 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

292 changes: 120 additions & 172 deletions CLAUDE.md

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions app/Console/Commands/CompUltraSubscription.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace App\Console\Commands;

use App\Enums\Subscription;
use App\Models\User;
use Illuminate\Console\Command;

class CompUltraSubscription extends Command
{
protected $signature = 'ultra:comp {email : The email address of the user to comp}';

protected $description = 'Create a comped Ultra subscription for a user using the dedicated $0 Stripe price';

public function handle(): int
{
$compedPriceId = config('subscriptions.plans.max.stripe_price_id_comped');

if (! $compedPriceId) {
$this->error('STRIPE_ULTRA_COMP_PRICE_ID is not configured.');

return self::FAILURE;
}

$email = $this->argument('email');
$user = User::where('email', $email)->first();

if (! $user) {
$this->error("User not found: {$email}");

return self::FAILURE;
}

$existingSubscription = $user->subscription('default');

if ($existingSubscription && $existingSubscription->active()) {
$currentPlan = 'unknown';

try {
$currentPlan = Subscription::fromStripePriceId(
$existingSubscription->items->first()?->stripe_price ?? $existingSubscription->stripe_price
)->name();
} catch (\Exception) {
}

$this->error("User already has an active {$currentPlan} subscription. Cancel it first or use swap.");

return self::FAILURE;
}

$user->createOrGetStripeCustomer();

$user->newSubscription('default', $compedPriceId)->create();

$this->info("Comped Ultra subscription created for {$email}.");

return self::SUCCESS;
}
}
125 changes: 125 additions & 0 deletions app/Console/Commands/MarkCompedSubscriptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;
use Laravel\Cashier\Subscription;

class MarkCompedSubscriptions extends Command
{
protected $signature = 'subscriptions:mark-comped
{file : Path to a CSV file containing email addresses (one per line or in an "email" column)}';

protected $description = 'Mark subscriptions as comped for email addresses in a CSV file';

public function handle(): int
{
$path = $this->argument('file');

if (! file_exists($path)) {
$this->error("File not found: {$path}");

return self::FAILURE;
}

$emails = $this->parseEmails($path);

if (empty($emails)) {
$this->error('No valid email addresses found in the file.');

return self::FAILURE;
}

$this->info('Found '.count($emails).' email(s) to process.');

$updated = 0;
$skipped = [];

foreach ($emails as $email) {
$user = User::where('email', $email)->first();

if (! $user) {
$skipped[] = "{$email} — user not found";

continue;
}

$subscription = Subscription::where('user_id', $user->id)
->where('stripe_status', 'active')
->first();

if (! $subscription) {
$skipped[] = "{$email} — no active subscription";

continue;
}

if ($subscription->is_comped) {
$skipped[] = "{$email} — already marked as comped";

continue;
}

$subscription->update(['is_comped' => true]);
$updated++;
$this->info("Marked {$email} as comped (subscription #{$subscription->id})");
}

if (count($skipped) > 0) {
$this->warn('Skipped:');
foreach ($skipped as $reason) {
$this->warn(" - {$reason}");
}
}

$this->info("Done. {$updated} subscription(s) marked as comped.");

return self::SUCCESS;
}

/**
* Parse email addresses from a CSV file.
* Supports: plain list (one email per line), or CSV with an "email" column header.
*
* @return array<string>
*/
private function parseEmails(string $path): array
{
$handle = fopen($path, 'r');

if (! $handle) {
return [];
}

$emails = [];
$emailColumnIndex = null;
$isFirstRow = true;

while (($row = fgetcsv($handle)) !== false) {
if ($isFirstRow) {
$isFirstRow = false;
$headers = array_map(fn ($h) => strtolower(trim($h)), $row);
$emailColumnIndex = array_search('email', $headers);

// If the first row looks like an email itself (no header), treat it as data
if ($emailColumnIndex === false && filter_var(trim($row[0]), FILTER_VALIDATE_EMAIL)) {
$emailColumnIndex = 0;
$emails[] = strtolower(trim($row[0]));
}

continue;
}

$value = trim($row[$emailColumnIndex] ?? '');

if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
$emails[] = strtolower($value);
}
}

fclose($handle);

return array_unique($emails);
}
}
59 changes: 59 additions & 0 deletions app/Console/Commands/SendMaxToUltraAnnouncement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Notifications\MaxToUltraAnnouncement;
use Illuminate\Console\Command;

class SendMaxToUltraAnnouncement extends Command
{
protected $signature = 'ultra:send-announcement
{--dry-run : Show what would be sent without actually sending}';

protected $description = 'Send a one-time announcement email to paying Max subscribers about the Ultra upgrade';

public function handle(): int
{
$dryRun = $this->option('dry-run');

if ($dryRun) {
$this->info('DRY RUN - No emails will be sent');
}

$maxPriceIds = array_filter([
config('subscriptions.plans.max.stripe_price_id'),
config('subscriptions.plans.max.stripe_price_id_monthly'),
config('subscriptions.plans.max.stripe_price_id_eap'),
config('subscriptions.plans.max.stripe_price_id_discounted'),
]);

$users = User::query()
->whereHas('subscriptions', function ($query) use ($maxPriceIds) {
$query->where('stripe_status', 'active')
->where('is_comped', false)
->whereIn('stripe_price', $maxPriceIds);
})
->get();

$this->info("Found {$users->count()} paying Max subscriber(s)");

$sent = 0;

foreach ($users as $user) {
if ($dryRun) {
$this->line("Would send to: {$user->email}");
} else {
$user->notify(new MaxToUltraAnnouncement);
$this->line("Sent to: {$user->email}");
}

$sent++;
}

$this->newLine();
$this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)");

return Command::SUCCESS;
}
}
74 changes: 74 additions & 0 deletions app/Console/Commands/SendUltraUpgradePromotion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Console\Commands;

use App\Enums\Subscription;
use App\Models\User;
use App\Notifications\UltraUpgradePromotion;
use Illuminate\Console\Command;

class SendUltraUpgradePromotion extends Command
{
protected $signature = 'ultra:send-upgrade-promo
{--dry-run : Show what would be sent without actually sending}';

protected $description = 'Send a promotional email to Mini and Pro subscribers encouraging them to upgrade to Ultra';

public function handle(): int
{
$dryRun = $this->option('dry-run');

if ($dryRun) {
$this->info('DRY RUN - No emails will be sent');
}

$miniPriceIds = array_filter([
config('subscriptions.plans.mini.stripe_price_id'),
config('subscriptions.plans.mini.stripe_price_id_eap'),
]);

$proPriceIds = array_filter([
config('subscriptions.plans.pro.stripe_price_id'),
config('subscriptions.plans.pro.stripe_price_id_eap'),
config('subscriptions.plans.pro.stripe_price_id_discounted'),
]);

$eligiblePriceIds = array_merge($miniPriceIds, $proPriceIds);

$users = User::query()
->whereHas('subscriptions', function ($query) use ($eligiblePriceIds) {
$query->where('stripe_status', 'active')
->where('is_comped', false)
->whereIn('stripe_price', $eligiblePriceIds);
})
->get();

$this->info("Found {$users->count()} eligible subscriber(s)");

$sent = 0;

foreach ($users as $user) {
$priceId = $user->subscriptions()
->where('stripe_status', 'active')
->where('is_comped', false)
->whereIn('stripe_price', $eligiblePriceIds)
->value('stripe_price');

$planName = Subscription::fromStripePriceId($priceId)->name();

if ($dryRun) {
$this->line("Would send to: {$user->email} ({$planName})");
} else {
$user->notify(new UltraUpgradePromotion($planName));
$this->line("Sent to: {$user->email} ({$planName})");
}

$sent++;
}

$this->newLine();
$this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)");

return Command::SUCCESS;
}
}
Loading
Loading