m0hammadgh92 / laravel-core-platform
A production-grade, business-agnostic, multi-tenant Core platform for Laravel 11
Package info
github.com/m0hammadgh/Multi-Vendor-Sale-System
pkg:composer/m0hammadgh92/laravel-core-platform
Requires
- php: ^8.2
- filament/filament: ^3.0
- laravel/framework: ^11.0
- laravel/horizon: ^5.0
- spatie/laravel-activitylog: ^4.0
Requires (Dev)
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- phpunit/phpunit: ^11.0
README
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
-
Configure Database (already using SQLite for development)
-
Configure Redis for queues (production):
QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379
- 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
- Request arrives with domain (e.g.,
acme.example.com) ResolveTenantMiddlewareextracts the hostTenantContextqueriestenant_domainstable- Tenant bound to container as
'tenant' - All models with
BelongsToTenanttrait 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:
critical: Payment callbacks, critical operations (3 workers, timeout 300s)default: Notifications, emails (5 workers, timeout 180s)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:
- Live in
modules/ModuleName/ - Have own Models, Services, Events, Jobs
- Can extend Core models (Order, Customer, Product)
- Listen to Core events for integration
- 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 = truerole 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
- Create migration in
database/migrations/ - Create model in
app/Core/{Domain}/ - Create event(s) in
app/Events/ - Create listener(s) in
app/Listeners/(queued!) - Create job(s) in
app/Jobs/ - Create service/action in
app/Core/{Domain}/Actions/ - Create Filament resource in
app/Filament/Resources/ - 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.