Reseller Architecture
This document covers the technical architecture of the multi-level reseller platform, including tenant hierarchy, database design, and data flow patterns.
Tenant Hierarchy
Dynamic Role System
The platform uses a capability-based role system instead of hardcoded tenant types. See the Tenant Roles Architecture for complete details.
// Roles are dynamic - these are the default system roles
'platform_owner' // Platform owner, master wholesaler (legacy: super_admin)
'distributor' // Distributor, wholesaler (legacy: reseller)
'retailer' // End-level seller (legacy: standard)
Hierarchy Fields
Added to the tenants table:
| Field | Type | Description |
|---|---|---|
tenant_role_id |
bigint | FK to tenant_roles table (capability system) |
parent_tenant_id |
uuid | Reference to parent tenant |
depth |
integer | Level in hierarchy (0 = root tenant) |
reseller_settings |
json | Margin configs, fulfillment preferences |
Note: The legacy
typecolumn has been removed. All type checks now go throughtenant_role_id.
Tenant Model Extensions
// app/Models/Tenant.php
use App\Models\Tenant\TenantRole;
class Tenant extends BaseTenant
{
// Role relationship
public function role(): BelongsTo
{
return $this->belongsTo(TenantRole::class, 'tenant_role_id');
}
// Hierarchy relationships
public function parentTenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'parent_tenant_id');
}
public function childTenants(): HasMany
{
return $this->hasMany(Tenant::class, 'parent_tenant_id');
}
// Get all descendants (recursive CTE query)
public function allDescendants(): Collection
{
// Uses efficient recursive CTE - see full implementation in Tenant.php
}
// Capability-based checks (string keys)
public function hasCapability(string $capability): bool
{
return $this->role?->hasCapability($capability) ?? false;
}
// Helper type checks (use string capability keys)
public function isSuperAdmin(): bool
{
return $this->hasCapability('access_all_tenants')
&& $this->hasCapability('bypass_permissions');
}
public function isReseller(): bool
{
return $this->hasCapability('create_sub_tenants')
&& !$this->hasCapability('access_all_tenants');
}
public function canAddSubResellers(): bool
{
// Checks both capability AND license limit
return $this->hasCapability('create_sub_tenants')
&& $this->hasLicenseForSubTenants();
}
}
Database Architecture
Central vs Tenant Database
The reseller platform uses a hybrid approach:
┌─────────────────────────────────────────────────────────────┐
│ CENTRAL DATABASE │
│ (Shared across all tenants - PostgreSQL) │
├─────────────────────────────────────────────────────────────┤
│ • tenants (with hierarchy fields) │
│ • master_products │
│ • reseller_product_pricing │
│ • chain_orders │
│ • chain_order_status_history │
│ • tenant_wallets │
│ • wallet_transactions │
│ • cod_collections │
│ • cod_remittances │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ TENANT DB A │ │ TENANT DB B │ │ TENANT DB C │
│ (Isolated) │ │ (Isolated) │ │ (Isolated) │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ • orders │ │ • orders │ │ • orders │
│ • customers │ │ • customers │ │ • customers │
│ • products │ │ • products │ │ • products │
│ • settings │ │ • settings │ │ • settings │
└───────────────┘ └───────────────┘ └───────────────┘
Why This Split?
Central Database for data that:
- Needs to be queried across tenants
- Represents relationships between tenants
- Must maintain referential integrity across the chain
Tenant Database for data that:
- Is tenant-specific and isolated
- Doesn't need cross-tenant queries
- Benefits from complete isolation
Context Switching
How It Works
Super admins can "operate as" any descendant tenant:
┌──────────────────────────────────────────────────────────┐
│ API Request │
│ Headers: │
│ Authorization: Bearer <token> │
│ X-Tenant: super-admin-id │
│ X-Effective-Tenant: reseller-123 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ HandleSuperAdminContext Middleware │
│ 1. Validate user is super_admin │
│ 2. Validate target tenant is descendant │
│ 3. Set effective_tenant on request │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Controller │
│ $tenant = $request->attributes->get('effective_tenant') │
│ ?? tenant(); │
│ // Now operating as reseller-123 │
└──────────────────────────────────────────────────────────┘
TenantContextService
// app/Services/TenantContextService.php
class TenantContextService
{
public function canSwitchContext(User $user, Tenant $currentTenant): bool
{
// Check if tenant has context switching capability
if (!$currentTenant->hasCapability('can_switch_context')) {
return false;
}
// Tenant with access_all_tenants can access all tenants
if ($currentTenant->hasCapability('access_all_tenants')) {
return Tenant::count() > 1;
}
// Tenant with access_descendants can access their descendants
if ($currentTenant->hasCapability('access_descendants')) {
return $currentTenant->childTenants()->exists();
}
return false;
}
public function canAccessTenant(User $user, Tenant $currentTenant, string $targetTenantId): bool
{
if ($currentTenant->id === $targetTenantId) {
return true;
}
if ($currentTenant->hasCapability('access_all_tenants')) {
return true;
}
if ($currentTenant->hasCapability('access_descendants')) {
$targetTenant = Tenant::find($targetTenantId);
return $targetTenant && $targetTenant->isDescendantOf($currentTenant);
}
return false;
}
public function getAccessibleTenants(User $user, Tenant $currentTenant): Collection
{
if ($currentTenant->hasCapability('access_all_tenants')) {
return Tenant::orderBy('name')->get();
}
if ($currentTenant->hasCapability('access_descendants')) {
return $currentTenant->allDescendants()->prepend($currentTenant);
}
return new Collection([$currentTenant]);
}
}
Middleware Implementation
// app/Http/Middleware/HandleSuperAdminContext.php
class HandleSuperAdminContext
{
public function handle(Request $request, Closure $next)
{
$effectiveTenantId = $request->header('X-Effective-Tenant');
$originalTenant = tenant();
if ($effectiveTenantId && $originalTenant && $effectiveTenantId !== $originalTenant->id) {
$contextService = app(TenantContextService::class);
// Use capability-based check instead of type check
if (!$contextService->canAccessTenant(
$request->user(),
$originalTenant,
$effectiveTenantId
)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$effectiveTenant = Tenant::find($effectiveTenantId);
$request->attributes->set('effective_tenant', $effectiveTenant);
$request->attributes->set('original_tenant', $originalTenant);
$request->attributes->set('is_context_switched', true);
}
return $next($request);
}
}
Order Flow Architecture
Chain Order Creation
When an order is placed at a reseller's store:
┌─────────────────────────────────────────────────────────────┐
│ 1. Customer places order at Sub-Reseller's Shopify store │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Shopify webhook → Sub-Reseller's tenant database │
│ Order created with customer's price (₹155) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. OrderForwardingService creates ChainOrder │
│ - origin_tenant_id = Sub-Reseller │
│ - chain_path = [Super, Distributor, Sub-Reseller] │
│ - margin_breakdown = calculated margins per level │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Order visible to all tenants in chain at THEIR cost │
│ - Super Admin sees: ₹100 │
│ - Distributor sees: ₹120 │
│ - Sub-Reseller sees: ₹155 (customer price) │
└─────────────────────────────────────────────────────────────┘
Chain Path Structure
// ChainOrder model
[
'chain_path' => [
'tenant-super-admin-uuid', // Index 0 - Top of chain
'tenant-distributor-uuid', // Index 1
'tenant-sub-reseller-uuid', // Index 2 - Origin
],
'margin_breakdown' => [
[
'tenant_id' => 'tenant-super-admin-uuid',
'cost' => 100,
'selling_price' => 120,
'margin_amount' => 20,
'margin_percent' => 20,
],
[
'tenant_id' => 'tenant-distributor-uuid',
'cost' => 120,
'selling_price' => 138,
'margin_amount' => 18,
'margin_percent' => 15,
],
[
'tenant_id' => 'tenant-sub-reseller-uuid',
'cost' => 138,
'selling_price' => 155,
'margin_amount' => 17,
'margin_percent' => 12.3,
],
]
]
Settlement Flow Architecture
Upstream Flow (Super Admin Fulfills)
┌─────────────────────────────────────────────────────────────┐
│ 1. Super Admin ships order, courier collects COD (₹155) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. CodCollection created (flow_type: upstream) │
│ collected_by: Super Admin │
│ collected_amount: ₹155 │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Margins flow DOWN to each reseller │
│ - Distributor wallet +₹18 │
│ - Sub-Reseller wallet +₹17 │
│ - Super Admin keeps ₹100 + ₹20 margin = ₹120 │
└─────────────────────────────────────────────────────────────┘
Downstream Flow (Reseller Fulfills Locally)
┌─────────────────────────────────────────────────────────────┐
│ 1. Sub-Reseller ships locally, collects COD (₹155) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. CodCollection created (flow_type: downstream) │
│ collected_by: Sub-Reseller │
│ collected_amount: ₹155 │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Sub-Reseller keeps margin (₹17), owes parent ₹138 │
│ - CodRemittance created: Sub-Reseller → Distributor │
│ - Amount: ₹138, Due: 3 days │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. When remittance paid, Distributor owes parent ₹120 │
│ - CodRemittance created: Distributor → Super Admin │
│ - Amount: ₹120, Due: 3 days │
└─────────────────────────────────────────────────────────────┘
Navigation Architecture
Capability-Based Filtering
Navigation items support both legacy tenantTypes and capability-based requiredCapabilities:
{
"navigation": {
"title": "Chain Orders",
"href": "/reseller-orders",
"icon": "ArrowRightLeft",
"tenantTypes": ["super_admin", "reseller"],
"requiredCapabilities": ["create_sub_tenants"],
"children": [
{
"title": "Master Products",
"href": "/reseller-catalog/master-products",
"requiredCapabilities": ["manage_master_catalog"]
},
{
"title": "My Catalog",
"href": "/reseller-catalog/my-catalog",
"requiredCapabilities": ["create_sub_tenants"]
}
]
}
}
Filtering Logic
// lib/module-navigation.ts
export function filterNavigationByCapabilities(
navigation: NavSection[],
tenant: { role: string; capabilities: Record<string, boolean> }
): NavSection[] {
return navigation
.map(section => ({
...section,
items: filterNavItemsByCapabilities(section.items, tenant),
}))
.filter(section => section.items.length > 0);
}
function filterNavItemsByCapabilities(
items: ResolvedNavItem[],
tenant: { role: string; capabilities: Record<string, boolean> }
): ResolvedNavItem[] {
return items.filter(item => {
if (item.isCore) return true;
// Check required capabilities first
if (item.requiredCapabilities?.length > 0) {
return item.requiredCapabilities.some(cap => tenant.capabilities[cap]);
}
// Fall back to legacy tenantTypes check
if (!item.tenantTypes || item.tenantTypes.length === 0) return true;
return item.tenantTypes.includes(tenant.role);
});
}
Module Registration
modules_statuses.json
{
"Core": true,
"Orders": true,
"Products": true,
"Customers": true,
"ResellerCatalog": true,
"ResellerOrders": true,
"ResellerFinance": true
}
Module Type
All reseller modules use type: "reseller" for proper section grouping:
{
"name": "ResellerOrders",
"type": "reseller",
"priority": 21
}
Security Considerations
Tenant Isolation
- Central database queries always filter by tenant chain
- Context switching validates capabilities before allowing access
- Wallet operations use database transactions
- All financial operations are logged
Capability-Based Permission Checks
// Example capability check in controller
public function index(Request $request)
{
$tenant = $request->attributes->get('effective_tenant') ?? tenant();
// Use string-based capability check
if (!$tenant->hasCapability('access_descendants')) {
abort(403, 'Insufficient capabilities');
}
// Query scoped to tenant's accessible chain
$orders = ChainOrder::forTenantChain($tenant->id)->get();
}
// Creating sub-tenants with capability + license check
public function createSubTenant(Request $request)
{
$tenant = $request->attributes->get('effective_tenant') ?? tenant();
// canAddSubResellers() checks both capability AND license limit
if (!$tenant->canAddSubResellers()) {
abort(403, 'This organization cannot create sub-tenants');
}
// Proceed with creation...
}
Audit Trail
All significant operations are logged:
- Order status changes →
chain_order_status_history - Wallet transactions →
wallet_transactions - Remittance processing →
cod_remittanceswith timestamps - Context switches → Activity logs
Next Steps
- Tenant Roles Architecture - Dynamic role/capability system
- Database Schema - Complete table definitions
- API Reference - Endpoint documentation
- Frontend Pages - UI components