- Schema: plans, orders subscription cols + plan_id + period_started_at, billing_events log, users.stripe_id + sms_credits, products.price_aud (db/schema.sql + Laravel migration, idempotent) - Laravel/Cashier: Billable User, checkout (annual sub + one-off SKUs with Managed Payments tax_code), webhook controller (signature-verified, idempotent, BILLING_ENABLED kill-switch), account tag transitions, nightly reconcile, Stripe portal link, PlanResource + billing dashboard - Go frontend: account-level gating (paid sub required), SMS pool (included 50/yr + credits, drawn after included), plan caps replace constants, 60s plan cache (credits fresh), 25-tag cap (plan max_tags) - BillingSeeder: personal plan + 3 SKUs + dev paid orders - Verified test-mode e2e: subscribe/paid/active/alerts, pool exhaust + credits resume, lapsed/suspended, cancelled/closed, recover/active, webhook idempotency, 25-cap, one-off SKUs, replacement, kill-switch, invalid signature 400
79 lines
2.8 KiB
PHP
79 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Models\Order;
|
|
use App\Models\Plan;
|
|
use App\Models\Product;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Seeder;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Billing seed: personal plan + tag/credit SKUs + a paid order for dev accounts.
|
|
*
|
|
* - Personal plan: $10/yr, 50 SMS included, 25 max tags, caps 5/day + 10/hr.
|
|
* - SKUs: single tag $5, 10-pack $20, 100-SMS credit pack $10.
|
|
* - Dev accounts that own tags get a paid order so existing verify suites
|
|
* keep alerting under the new account-level gating (idempotent).
|
|
*/
|
|
class BillingSeeder extends Seeder
|
|
{
|
|
public function run(): void
|
|
{
|
|
// 1. Personal plan (idempotent by name).
|
|
$plan = Plan::firstOrCreate(
|
|
['plan_type' => 'personal', 'name' => 'Personal'],
|
|
[
|
|
'price_aud' => 10.00,
|
|
'billing_interval' => 'year',
|
|
'sms_included' => 50,
|
|
'max_tags' => 25,
|
|
'alerts_per_day' => 5,
|
|
'alerts_per_hour' => 10,
|
|
]
|
|
);
|
|
|
|
// 2. Tag + credit SKUs (idempotent by sku).
|
|
$skus = [
|
|
['sku' => 'TAG-SINGLE', 'name' => 'Where Woof Tag (single)', 'item_type' => 'other', 'price_aud' => 5.00],
|
|
['sku' => 'TAG-10PACK', 'name' => 'Where Woof Tags (10-pack)', 'item_type' => 'other', 'price_aud' => 20.00],
|
|
['sku' => 'SMS-CREDITS-100', 'name' => '100 SMS Credits', 'item_type' => 'other', 'price_aud' => 10.00],
|
|
];
|
|
foreach ($skus as $s) {
|
|
Product::firstOrCreate(['sku' => $s['sku']], $s);
|
|
}
|
|
|
|
// 3. Paid order for any account that owns tags (dev regression safety),
|
|
// plus the known dev accounts (admin + owner) so verify suites that
|
|
// bind tags after a DB reset still alert under account-level gating.
|
|
$tagOwners = DB::table('tags')
|
|
->whereNotNull('owner_id')
|
|
->distinct()
|
|
->pluck('owner_id');
|
|
|
|
$devEmails = ['admin@where-woof.com', 'owner@where-woof.com'];
|
|
foreach (User::whereIn('email', $devEmails)->get() as $dev) {
|
|
$tagOwners->push($dev->id);
|
|
}
|
|
$tagOwners = $tagOwners->unique();
|
|
|
|
foreach ($tagOwners as $ownerId) {
|
|
$existing = Order::where('account_id', $ownerId)->where('status', 'paid')->first();
|
|
if ($existing) {
|
|
continue;
|
|
}
|
|
Order::create([
|
|
'account_id' => $ownerId,
|
|
'status' => 'paid',
|
|
'plan_id' => $plan->id,
|
|
'period_started_at' => now(),
|
|
'renews_at' => now()->addYear(),
|
|
'amount' => 10.00,
|
|
]);
|
|
}
|
|
|
|
$this->command?->info('BillingSeeder: plan + '.count($skus).' SKUs seeded; '.$tagOwners->count().' dev accounts given paid orders.');
|
|
}
|
|
}
|