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
- Method-level granularity for future PHPDoc annotations (Scramble emits richer types per controller).
- Adding new methods to the relevant area no longer needs scrolling 600+ lines.
- The split lets us add per-area policies (
TenantPolicy,TenantBillingPolicy, …) without conflating concerns. - 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 returnednull.destroy()checks$devOpsApi->isConfigured()before attempting to remove (consistency with suspend/activate).index()allowedplan_idinsortBy, but the column isplan(slug); sortable-list now matches the schema.updateVersions()readsprUrl(camelCase) before falling back topr_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.