Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
release:
uses: saucebase-dev/saucebase/.github/workflows/semantic-release.yml@main
with:
version_file: 'module.json'
version_file: 'composer.json'
version_file_path: '.version'
prerelease: false
secrets:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ jobs:
test:
uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main
with:
module: Demo
dependencies: saucebase/auth saucebase/settings saucebase/billing saucebase/announcements saucebase/blog saucebase/roadmap saucebase/themes
module: demo
Comment thread
roble marked this conversation as resolved.
dependencies: saucebase/auth saucebase/billing saucebase/blog saucebase/roadmap
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Saucebase
Copyright (c) 2026 Saucebase

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
10 changes: 8 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ tasks:

test:e2e:
desc: Run E2E tests for Demo module
cmd: npx playwright test --project="@Demo*" {{.CLI_ARGS}}
cmd: npx playwright test --project="@demo*" {{.CLI_ARGS}}
interactive: true
Comment thread
roble marked this conversation as resolved.

# ── Database ──────────────────────────────────────────────────

db:seed:
desc: Seed the Demo module database
cmd: php artisan modules:seed --module=demo

# ── Code Generation ────────────────────────────────────────────

types:generate:
desc: Generate TypeScript types from PHP DTOs and enums
cmd: php artisan module:generate-types Demo
cmd: php artisan module:generate-types demo
Comment thread
roble marked this conversation as resolved.
35 changes: 0 additions & 35 deletions app/Http/Controllers/DemoController.php

This file was deleted.

16 changes: 0 additions & 16 deletions app/Providers/DemoServiceProvider.php

This file was deleted.

17 changes: 0 additions & 17 deletions app/Providers/RouteServiceProvider.php

This file was deleted.

13 changes: 9 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "saucebase/demo",
"description": "",
"description": "Demo module",
"type": "saucebase-module",
"license": "proprietary",
"version": "0.5.0",
Comment thread
roble marked this conversation as resolved.
"authors": [
{
"name": "Saucebase",
Expand All @@ -10,13 +12,15 @@
],
"extra": {
"laravel": {
"providers": [],
"providers": [
"Modules\\Demo\\Providers\\DemoServiceProvider"
],
"aliases": {}
}
},
"autoload": {
"psr-4": {
"Modules\\Demo\\": "app/",
"Modules\\Demo\\": "src/",
"Modules\\Demo\\Database\\Factories\\": "database/factories/",
"Modules\\Demo\\Database\\Seeders\\": "database/seeders/"
}
Expand All @@ -25,5 +29,6 @@
"psr-4": {
"Modules\\Demo\\Tests\\": "tests/"
}
}
},
"minimum-stability": "stable"
}
13 changes: 13 additions & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Modules\Demo\Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call(DemoDatabaseSeeder::class);
}
}
193 changes: 193 additions & 0 deletions database/seeders/DemoDatabaseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

namespace Modules\Demo\Database\Seeders;

use App\Enums\Role;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Modules\Billing\Database\Seeders\BillingDatabaseSeeder;
use Modules\Billing\Models\CheckoutSession;
use Modules\Billing\Models\Customer;
use Modules\Billing\Models\Invoice;
use Modules\Billing\Models\Payment;
use Modules\Billing\Models\PaymentMethod;
use Modules\Billing\Models\Price;
use Modules\Billing\Models\Subscription;
use Modules\Roadmap\Database\Seeders\RoadmapDatabaseSeeder;
use Modules\Roadmap\Enums\VoteType;
use Modules\Roadmap\Models\RoadmapItem;
use Modules\Roadmap\Models\RoadmapVote;

class DemoDatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
BillingDatabaseSeeder::class,
RoadmapDatabaseSeeder::class,
]);

$proMonthly = Price::where('provider_price_id', 'price_1SyadREx2sHJcHgwCt0ReZEJ')->firstOrFail();
$proYearly = Price::where('provider_price_id', 'price_1SyaCQEx2sHJcHgwEC9VmwSZ')->firstOrFail();
$teamMonthly = Price::where('provider_price_id', 'price_1SyaL0Ex2sHJcHgwWaaTGLgo')->firstOrFail();
$teamYearly = Price::where('provider_price_id', 'price_1SyaLWEx2sHJcHgw3fQdYV0J')->firstOrFail();

$users = User::factory()->count(30)->create();
foreach ($users as $user) {
$user->assignRole(Role::USER->value);
}

$customers = collect();
$paymentMethods = collect();
foreach ($users->take(25) as $user) {
$customer = Customer::factory()->withAddress()->create(['user_id' => $user->id]);
$paymentMethod = PaymentMethod::factory()->default()->visa()->create(['customer_id' => $customer->id]);
$customers->push($customer);
$paymentMethods->push($paymentMethod);
}

// 12 month-start dates, oldest first, for backdating
$months = collect(range(11, 0))->map(fn ($i) => now()->subMonths($i)->startOfMonth());

// [customerIndex, price, state]
$assignments = [
[0, $proMonthly, 'active'],
[1, $proMonthly, 'active'],
[2, $proMonthly, 'active'],
[3, $proMonthly, 'active'],
[4, $proMonthly, 'active'],
[5, $proMonthly, 'active'],
[6, $proMonthly, 'active'],
[7, $proMonthly, 'active'],
[8, $proMonthly, 'active'],
[9, $proMonthly, 'active'],
[10, $proMonthly, 'active'],
[11, $proMonthly, 'active'],
[12, $proYearly, 'active'],
[13, $proYearly, 'active'],
[14, $proYearly, 'active'],
[15, $proYearly, 'active'],
[16, $proYearly, 'active'],
[17, $teamMonthly, 'active'],
[18, $teamMonthly, 'active'],
[19, $teamMonthly, 'active'],
[20, $teamMonthly, 'active'],
[21, $teamYearly, 'active'],
[22, $teamYearly, 'active'],
[23, $proMonthly, 'onTrial'],
[24, $proMonthly, 'onTrial'],
// Cancelled prior subscriptions reusing the same customers
[0, $proMonthly, 'cancelled'],
[1, $teamMonthly, 'cancelled'],
[2, $proYearly, 'cancelled'],
// Past due
[3, $teamMonthly, 'pastDue'],
[4, $proMonthly, 'pastDue'],
];

$activeSubscriptions = collect();
$monthIndex = 0;

foreach ($assignments as [$customerIndex, $price, $state]) {
$customer = $customers->get($customerIndex);
$paymentMethod = $paymentMethods->get($customerIndex);
$createdAt = $months[$monthIndex % 12];
$monthIndex++;

$factory = Subscription::factory()->state([
'customer_id' => $customer->id,
'price_id' => $price->id,
'payment_method_id' => $paymentMethod->id,
]);

$factory = match ($state) {
'cancelled' => $factory->cancelled(),
'pastDue' => $factory->pastDue(),
'onTrial' => $factory->onTrial(),
default => $factory,
};

$subscription = $factory->create(['created_at' => $createdAt]);

if (in_array($state, ['active', 'onTrial', 'pastDue'])) {
$customer->user->assignRole(Role::SUBSCRIBER->value);
}

if ($state === 'active') {
$activeSubscriptions->push($subscription);
}
}

// Payments: one per month per active subscription since its creation.
// This produces a naturally growing monthly revenue trend.
$activeSubscriptions = Subscription::whereIn('id', $activeSubscriptions->pluck('id'))->with('price')->get();

foreach ($activeSubscriptions as $subscription) {
$cursor = Carbon::parse($subscription->created_at)->startOfMonth();
$nowMonth = now()->startOfMonth();

while ($cursor <= $nowMonth) {
$succeeded = fake()->boolean(90);

$paymentFactory = Payment::factory()->state([
'customer_id' => $subscription->customer_id,
'subscription_id' => $subscription->id,
'price_id' => $subscription->price_id,
'amount' => $subscription->price->amount,
]);

if (! $succeeded) {
$paymentFactory = $paymentFactory->failed();
}

$payment = $paymentFactory->create(['created_at' => $cursor->copy()]);

if ($succeeded) {
Invoice::factory()->create([
'customer_id' => $subscription->customer_id,
'subscription_id' => $subscription->id,
'payment_id' => $payment->id,
'subtotal' => $subscription->price->amount,
'total' => $subscription->price->amount,
'paid_at' => $cursor->copy(),
'created_at' => $cursor->copy(),
]);
}

$cursor->addMonth();
}
}

// Checkout sessions spread across 12 months for the conversion chart.
// ~2-3 completed, 1 abandoned, 0-1 expired per month → ~67% conversion rate.
foreach ($months as $month) {
CheckoutSession::factory()
->completed()
->count(fake()->numberBetween(2, 3))
->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);

CheckoutSession::factory()
->abandoned()
->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);

if (fake()->boolean(50)) {
CheckoutSession::factory()
->expired()
->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);
}
}

// Roadmap votes: each demo user votes on 3-6 random items.
$roadmapItems = RoadmapItem::all();
foreach ($users as $user) {
$itemsToVote = $roadmapItems->random(fake()->numberBetween(3, 6));
foreach ($itemsToVote as $item) {
RoadmapVote::firstOrCreate(
['roadmap_item_id' => $item->id, 'user_id' => $user->id],
['type' => fake()->randomElement([VoteType::Up, VoteType::Down])]
);
}
}
}
}
15 changes: 0 additions & 15 deletions module.json

This file was deleted.

Binary file added resources/assets/images/dashboard-admin-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading