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 type column has been removed. All type checks now go through tenant_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_remittances with timestamps
  • Context switches → Activity logs

Next Steps