Skip to main content

Tenant Controller Split

The admin backend used to ship with a 1076-line TenantController doing CRUD, suspend/activate, version updates, K8s status, provisioning, scaling config (4 services × 7 fields), storage usage, two email proxies, SMS proxy, and service-health checks — all in one file. This page documents the split that replaced it.

Resulting layout

app/Http/Controllers/Tenant/
├── TenantController.php # CRUD + suspend/activate + show/index + services list
├── TenantProvisioningController.php # provisioning-status, retry, sync-config
├── TenantVersionController.php # PUT /versions
├── TenantScalingController.php # GET/PUT/DELETE /scaling-config
├── TenantStorageController.php # GET /storage-usage
├── TenantEmailProxyController.php # /email/* — proxies CRM
├── TenantSmsController.php # /sms/*
└── TenantHealthController.php # /service-health

Plus a new service: app/Services/ScalingConfigService.php extracts the duplicated "build response from feature overrides" / "apply overrides" logic that previously lived twice in the controller.

A FormRequest replaces inline $request->validate() calls:

  • app/Http/Requests/UpdateScalingConfigRequest.php — 30-line validation moved out of the controller method.

Routes

routes/api.php was updated; collection-level tenants-services is registered before apiResource('tenants', ...) to avoid being shadowed by tenants/{tenant}. URLs did not change — frontend stores keep working without modification.

Reasons not to merge it back

  1. Method-level granularity for future PHPDoc annotations (Scramble emits richer types per controller).
  2. Adding new methods to the relevant area no longer needs scrolling 600+ lines.
  3. The split lets us add per-area policies (TenantPolicy, TenantBillingPolicy, …) without conflating concerns.
  4. Tests can target a single controller without instantiating dependencies for the other domains.

Co-incident bug fixes

While splitting we also fixed:

  • destroy() no longer logs 'success' and locally deletes the tenant when DevOps API returned null.
  • destroy() checks $devOpsApi->isConfigured() before attempting to remove (consistency with suspend/activate).
  • index() allowed plan_id in sortBy, but the column is plan (slug); sortable-list now matches the schema.
  • updateVersions() reads prUrl (camelCase) before falling back to pr_url — DevOps API's actual response shape.

What did not change

  • All public URLs and HTTP verbs are identical.
  • Service contracts (DevOpsApiService, TenantSystemApiService, FeatureService) are unchanged.
  • Audit log keys are unchanged.
  • Database schema is unchanged.

The only consumer-visible change is the OpenAPI operationId values, which now follow the new method names — so regenerate the SDK after pulling.