feat(packages): implement package-based business model
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->enum('type', ['endcustomer', 'reseller']);
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->integer('max_photos')->nullable();
|
||||
$table->integer('max_guests')->nullable();
|
||||
$table->integer('gallery_days')->nullable();
|
||||
$table->integer('max_tasks')->nullable();
|
||||
$table->boolean('watermark_allowed')->default(true);
|
||||
$table->boolean('branding_allowed')->default(false);
|
||||
$table->integer('max_events_per_year')->nullable();
|
||||
$table->timestamp('expires_after')->nullable();
|
||||
$table->json('features')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['type', 'price']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('packages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('event_packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('purchased_price', 8, 2);
|
||||
$table->timestamp('purchased_at');
|
||||
$table->integer('used_photos')->default(0);
|
||||
$table->timestamps();
|
||||
$table->index('event_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('event_packages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tenant_packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->timestamp('purchased_at');
|
||||
$table->timestamp('expires_at');
|
||||
$table->integer('used_events')->default(0);
|
||||
$table->boolean('active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tenant_packages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('package_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained();
|
||||
$table->foreignId('event_id')->nullable()->constrained();
|
||||
$table->foreignId('package_id')->constrained();
|
||||
$table->string('provider_id');
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->enum('type', ['endcustomer_event', 'reseller_subscription']);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('user_agent')->nullable();
|
||||
$table->boolean('refunded')->default(false);
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'purchased_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('package_purchases');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::dropIfExists('event_credits_ledger');
|
||||
Schema::dropIfExists('purchase_history');
|
||||
Schema::dropIfExists('event_purchases');
|
||||
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'event_credits_balance',
|
||||
'subscription_tier',
|
||||
'subscription_expires_at',
|
||||
'free_event_granted_at',
|
||||
'total_revenue'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->integer('event_credits_balance')->default(1);
|
||||
$table->string('subscription_tier')->nullable();
|
||||
$table->timestamp('subscription_expires_at')->nullable();
|
||||
$table->timestamp('free_event_granted_at')->nullable();
|
||||
$table->decimal('total_revenue', 10, 2)->default(0.00);
|
||||
});
|
||||
|
||||
Schema::create('event_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('credits_added')->default(0);
|
||||
$table->decimal('price', 10, 2)->default(0);
|
||||
$table->string('provider_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('purchase_history', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('package_id', 255);
|
||||
$table->integer('credits_added')->default(0);
|
||||
$table->decimal('price', 10, 2)->default(0);
|
||||
$table->string('provider_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('credits_change');
|
||||
$table->string('reason');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Event;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Ensure packages table has data (seed if empty)
|
||||
if (DB::table('packages')->count() == 0) {
|
||||
// Insert standard packages if not seeded
|
||||
DB::table('packages')->insert([
|
||||
[
|
||||
'name' => 'Free/Test',
|
||||
'type' => 'endcustomer',
|
||||
'price' => 0.00,
|
||||
'max_photos' => 30,
|
||||
'max_guests' => 10,
|
||||
'gallery_days' => 3,
|
||||
'max_tasks' => 1,
|
||||
'watermark_allowed' => true,
|
||||
'branding_allowed' => false,
|
||||
'max_events_per_year' => null,
|
||||
'expires_after' => null,
|
||||
'features' => json_encode([]),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'Starter',
|
||||
'type' => 'endcustomer',
|
||||
'price' => 19.00,
|
||||
'max_photos' => 300,
|
||||
'max_guests' => 50,
|
||||
'gallery_days' => 14,
|
||||
'max_tasks' => 5,
|
||||
'watermark_allowed' => true,
|
||||
'branding_allowed' => false,
|
||||
'max_events_per_year' => null,
|
||||
'expires_after' => null,
|
||||
'features' => json_encode([]),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
// Add more standard packages as per plan
|
||||
[
|
||||
'name' => 'Reseller S',
|
||||
'type' => 'reseller',
|
||||
'price' => 149.00,
|
||||
'max_photos' => null,
|
||||
'max_guests' => null,
|
||||
'gallery_days' => null,
|
||||
'max_tasks' => null,
|
||||
'watermark_allowed' => true,
|
||||
'branding_allowed' => true,
|
||||
'max_events_per_year' => 5,
|
||||
'expires_after' => now()->addYear(),
|
||||
'features' => json_encode(['limited_branding']),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
// ... other reseller packages
|
||||
]);
|
||||
}
|
||||
|
||||
// Migrate tenant credits to tenant_packages (Free package)
|
||||
DB::table('tenants')->where('event_credits_balance', '>', 0)->chunk(100, function ($tenants) {
|
||||
foreach ($tenants as $tenant) {
|
||||
$freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
|
||||
DB::table('tenant_packages')->insert([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $freePackageId,
|
||||
'price' => 0.00,
|
||||
'purchased_at' => $tenant->free_event_granted_at ?? now(),
|
||||
'expires_at' => now()->addDays(30), // or based on credits
|
||||
'used_events' => min($tenant->event_credits_balance, 1), // e.g. 1 free event
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Create purchase ledger entry
|
||||
DB::table('package_purchases')->insert([
|
||||
'tenant_id' => $tenant->id,
|
||||
'event_id' => null,
|
||||
'package_id' => $freePackageId,
|
||||
'provider_id' => 'migration_free',
|
||||
'price' => 0.00,
|
||||
'type' => 'reseller_subscription',
|
||||
'metadata' => json_encode(['migrated_from_credits' => $tenant->event_credits_balance]),
|
||||
'ip_address' => null,
|
||||
'user_agent' => null,
|
||||
'refunded' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Migrate event purchases to event_packages (if any existing events)
|
||||
DB::table('events')->chunk(100, function ($events) {
|
||||
foreach ($events as $event) {
|
||||
if ($event->tenant->event_credits_balance > 0) { // or check if event was created with credits
|
||||
$freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
|
||||
DB::table('event_packages')->insert([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $freePackageId,
|
||||
'purchased_price' => 0.00,
|
||||
'purchased_at' => $event->created_at,
|
||||
'used_photos' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Ledger entry
|
||||
DB::table('package_purchases')->insert([
|
||||
'tenant_id' => $event->tenant_id,
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $freePackageId,
|
||||
'provider_id' => 'migration_free',
|
||||
'price' => 0.00,
|
||||
'type' => 'endcustomer_event',
|
||||
'metadata' => json_encode(['migrated_from_credits' => true]),
|
||||
'ip_address' => null,
|
||||
'user_agent' => null,
|
||||
'refunded' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user