Looking to hire Laravel developers? Try LaraJobs

laravel-core-platform maintained by m0hammadgh92

Description
A production-grade, business-agnostic, multi-tenant Core platform for Laravel 11
Last update
2025/12/28 11:54 (dev-main)
License
Links
Downloads
3

Comments
comments powered by Disqus

Laravel 11 Core Platform

A production-grade, business-agnostic, multi-tenant Core platform for Laravel 11, designed to be extracted into a Composer package.

🎯 Overview

This Core platform provides a complete foundation for building multi-tenant SaaS applications with complex business logic. It handles:

  • Multi-tenancy with domain-based resolution
  • Customer & User separation (authentication ≠ commercial entity)
  • Order management with complete lifecycle
  • Billing & Invoicing with immutable financial records
  • Payment processing with gateway abstraction
  • Customer wallet system
  • Queue-driven architecture for all side-effects
  • Event-driven design for extensibility
  • Scoped settings with inheritance
  • Activity logging for audit trails
  • File management with polymorphic attachments

✨ Key Principles

1. Queue-First Architecture

ALL external operations, slow I/O, emails, notifications, PDFs, payment processing happen via queued jobs ONLY.

2. Event-Driven Core

Business operations emit domain events. Listeners (all queued) respond to events for side-effects.

3. Strict Financial Discipline

  • Money stored as INTEGER minor units (cents) - NO FLOATS
  • Currency always stored alongside amounts
  • Immutable invoice numbers via transactional sequences
  • Wallet ledger with balance snapshots

4. Multi-Tenancy by Design

  • Every tenant-owned table has tenant_id
  • Tenant resolved by request domain
  • Global Eloquent scopes prevent cross-tenant data leak
  • All queries automatically scoped

5. Customer ≠ User

  • Users: Authentication, activity causers
  • Customers: Commercial entities, own orders/invoices/payments/wallet
  • Orders, invoices, payments NEVER reference users directly

6. Package-Ready from Day One

  • No hardcoded business logic
  • No direct env() calls in logic
  • Everything configurable
  • Clear service contracts

📁 Structure

app/
├── Core/
│   ├── Tenancy/          # Multi-tenant infrastructure
│   ├── Identity/         # Users (in app/Models/User.php)
│   ├── CRM/              # Customer management
│   ├── Catalog/          # Products & Variants
│   ├── Sales/            # Orders
│   ├── Billing/          # Invoices, Payments, Wallet, Sequences
│   ├── Pricing/          # Tax Rates, Coupons
│   ├── Settings/         # Scoped settings
│   ├── Notifications/    # Templates & Outbound Messages
│   ├── Files/            # File management
│   └── Audit/            # (uses spatie/activitylog)
├── Support/
│   ├── Money/            # Money value object
│   ├── States/           # (To be created)
│   └── SettingsResolver/ # Settings inheritance resolver
├── Events/               # Domain events (to be created)
├── Listeners/            # Queued event listeners (to be created)
├── Jobs/                 # Async jobs (to be created)
└── Policies/             # Authorization (to be created)

modules/                  # Future vertical modules (empty)

🚀 Getting Started

Installation

This is already installed! The platform is ready to use.

Environment Setup

  1. Configure Database (already using SQLite for development)

  2. Configure Redis for queues (production):

QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
  1. Configure Mail (for notifications):
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

Running the Application

# Run migrations (already done)
php artisan migrate

# Start Horizon (queue worker)
php artisan horizon

# Start development server
php artisan serve

# Access Filament admin panel
open http://localhost:8000/admin

🏢 Multi-Tenancy

How It Works

  1. Request arrives with domain (e.g., acme.example.com)
  2. ResolveTenantMiddleware extracts the host
  3. TenantContext queries tenant_domains table
  4. Tenant bound to container as 'tenant'
  5. All models with BelongsToTenant trait auto-scope to this tenant

Adding a Tenant

php artisan tinker
// Create tenant
$tenant = \App\Core\Tenancy\Tenant::create([
    'name' => 'Acme Corporation',
    'slug' => 'acme',
    'status' => 'active',
    'metadata' => [],
]);

// Add domain
$tenant->domains()->create([
    'domain' => 'acme.test',
    'is_primary' => true,
]);

// Create default brand (optional)
$tenant->brands()->create([
    'name' => 'Acme Brand',
    'slug' => 'default',
    'is_default' => true,
]);

// Create admin user for this tenant
$user = \App\Models\User::create([
    'tenant_id' => $tenant->id,
    'name' => 'Admin User',
    'email' => 'admin@acme.test',
    'password' => bcrypt('password'),
    'role' => 'admin',
    'is_active' => true,
]);

Testing Multi-Tenancy Locally

Add to /etc/hosts:

127.0.0.1 acme.test
127.0.0.1 demo.test

Then access: http://acme.test:8000

💰 Money Handling

All monetary values use the Money value object:

use App\Support\Money\Money;

// From minor units (cents)
$price = Money::fromMinor(1999, 'USD'); // $19.99

// From major units (dollars)
$price = Money::fromMajor(19.99, 'USD'); // $19.99

// Operations
$total = $price->multiply(3); // $59.97
$discount = $price->percentage(2000); // 20% = $3.998 → $4.00
$final = $total->subtract($discount); // $55.97

// Display
echo $price->format(); // "$19.99"

📋 Order Lifecycle

draft
 ↓ (OrderSubmitted event)
pending_payment
 ↓ (PaymentCaptured event → OrderPaid event)
paid
 ↓ (items fulfilled)
closed

OR

canceled (from draft/pending_payment)
refunded (from paid)

Creating an Order

use App\Core\Sales\Order;
use App\Core\Billing\Sequence;

$order = Order::create([
    'tenant_id' => $tenant->id,
    'customer_id' => $customer->id,
    'order_number' => Sequence::nextFor($tenant->id, 'order', 'ORD-'),
    'status' => 'draft',
    'currency' => 'USD',
    // Amounts calculated by OrderCalculator service
]);

// Add items
$order->items()->create([
    'tenant_id' => $tenant->id,
    'product_variant_id' => $variant->id,
    'product_name' => $variant->product->name,
    'variant_name' => $variant->name,
    'quantity' => 1,
    'unit_price_minor' => $variant->price_minor,
    'currency' => 'USD',
    // Calculate totals
]);

📄 Invoice System

Invoices are immutable financial documents with:

  • Unique sequential numbers (thread-safe generation via Sequence)
  • Customer snapshot (for legal record)
  • Cannot be deleted (only voided)

Creating an Invoice

use App\Core\Billing\Invoice;
use App\Core\Billing\Sequence;

$invoice = Invoice::create([
    'tenant_id' => $tenant->id,
    'customer_id' => $customer->id,
    'order_id' => $order->id,
    'invoice_number' => Sequence::nextFor($tenant->id, 'invoice', 'INV-'),
    'status' => 'draft',
    'currency' => 'USD',
    'customer_snapshot' => $customer->toArray(), // Immutable record
]);

// Generate PDF via queued job
GenerateInvoicePdfJob::dispatch($invoice);

👛 Wallet System

Customers can have wallet balances (one per currency):

$wallet = $customer->walletAccounts()->firstOrCreate([
    'currency' => 'USD',
], [
    'balance_minor' => 0,
]);

// Credit wallet (via WalletService for transactional safety)
app(\App\Core\Billing\Services\WalletService::class)
    ->credit($wallet, Money::fromMajor(50.00, 'USD'), 'Initial credit');

// Debit wallet
app(\App\Core\Billing\Services\WalletService::class)
    ->debit($wallet, Money::fromMajor(10.00, 'USD'), 'Payment');

⚙️ Settings System

Settings follow this precedence: user → customer → brand → tenant → global

use App\Support\SettingsResolver\SettingsResolver;

$resolver = app(SettingsResolver::class);

// Get setting with context
$value = $resolver->get(
    key: 'theme.color',
    default: 'blue',
    userId: $user->id,
    customerId: $customer->id
);

// Set setting
$resolver->set(
    key: 'theme.color',
    value: 'red',
    scopeType: 'customer',
    scopeId: $customer->id
);

🔔 Notifications

All notifications are queued jobs that create OutboundMessage records:

// Listen to event
class OrderConfirmationListener
{
    public function handle(OrderSubmitted $event)
    {
        SendOrderConfirmationJob::dispatch($event->order);
    }
}

📦 Queue Lanes

Three priority lanes:

  1. critical: Payment callbacks, critical operations (3 workers, timeout 300s)
  2. default: Notifications, emails (5 workers, timeout 180s)
  3. low: PDFs, exports, background tasks (2 workers, timeout 600s)

Dispatch to specific queue:

GenerateInvoicePdfJob::dispatch($invoice)->onQueue('low');
PaymentCallbackJob::dispatch($payment)->onQueue('critical');

🧩 Extending with Modules

Future vertical modules for your specific business needs should:

  1. Live in modules/ModuleName/
  2. Have own Models, Services, Events, Jobs
  3. Can extend Core models (Order, Customer, Product)
  4. Listen to Core events for integration
  5. Never depend on other modules

Example:

// modules/YourModule/Models/CustomEntity.php
class CustomEntity extends Model
{
    public function order()
    {
        return $this->belongsTo(\App\Core\Sales\Order::class);
    }
}

// modules/YourModule/Listeners/HandleOrderPaid.php
class HandleOrderPaid
{
    public function handle(OrderPaid $event)
    {
        if ($event->order->hasProductType('your_product_type')) {
            YourCustomJob::dispatch($event->order);
        }
    }
}

🔐 Security

Policies (To Be Created)

All Filament resources should use policies:

  • Tenant scoping enforced
  • Role-based permissions
  • Super admin bypass for tenant management

Authentication

Users access Filament admin panel if:

  • is_active = true
  • role IN ('super_admin', 'admin')

🧪 Testing

# Run all tests
php artisan test

# Run specific test suite
php artisan test --testsuite=Feature

# Run with coverage
php artisan test --coverage

Critical test areas:

  • Tenant isolation (no cross-tenant data leakage)
  • Money calculations (precision)
  • Sequence generation (thread-safety)
  • Wallet transactions (balance integrity)
  • Event/listener flow

📊 Admin Panel (Filament)

Access at /admin with admin credentials.

Resources to create:

  • Tenants (super_admin only)
  • Users
  • Customers
  • Products & Variants
  • Orders
  • Invoices
  • Payments
  • Wallet Accounts
  • Coupons
  • Tax Rates
  • Settings
  • Files
  • Outbound Messages
  • Activity Log

🔄 Development Workflow

Creating a New Domain Feature

  1. Create migration in database/migrations/
  2. Create model in app/Core/{Domain}/
  3. Create event(s) in app/Events/
  4. Create listener(s) in app/Listeners/ (queued!)
  5. Create job(s) in app/Jobs/
  6. Create service/action in app/Core/{Domain}/Actions/
  7. Create Filament resource in app/Filament/Resources/
  8. Write tests in tests/Feature/{Domain}/

Example: Adding Refund Support

// 1. Update migrations (already has refunded_minor)

// 2. Create event
class RefundCreated
{
    public function __construct(public Payment $payment) {}
}

// 3. Create listener
class UpdateInvoiceAfterRefund
{
    public function handle(RefundCreated $event)
{
        $invoice = $event->payment->invoice;
        $invoice->increment('refunded_minor', $event->payment->amount_minor);
        $invoice->save();
    }
}

// 4. Create job
class ProcessRefundJob implements ShouldQueue
{
    public $queue = 'critical';
    
    public function handle()
    {
        // Process refund via gateway
        // Fire RefundCreated event
    }
}

🎯 Roadmap

Completed ✅

  • Database schema (all 24 migrations)
  • Multi-tenancy infrastructure
  • Core models (Tenant, Customer, User, Product, Order, Invoice, Payment, Wallet)
  • Money value object
  • Settings resolver
  • Filament & Horizon integration

In Progress 🔨

  • Domain events
  • Queued listeners
  • Background jobs
  • Filament resources
  • Policies

Planned 📋

  • API layer (optional)
  • Webhook system
  • PDF generation service
  • Payment gateway drivers
  • Fulfillment system abstraction
  • Test suite
  • Package extraction

📚 Resources

📄 License

Proprietary. Not for redistribution.

🙏 Credits

Built with:

  • Laravel 11
  • Filament 3
  • Laravel Horizon
  • Spatie Laravel Activity Log

Built for production. Designed for extensibility. Ready for extraction.

Testing webhook