Appearance
Business Registration Gates
The business registration gates feature closes three business risks on the public endpoint POST /identity/auth/user/register/business and prevents agency administrators from driving the system into an inconsistent state through two symmetric guards.
For the architectural rationale behind the bidirectional gateway pattern used to implement these guards, see ADR 007 — Bidirectional Gateway Pattern.
For the panel UI that surfaces this feature, see Settings — Registro y Onboarding.
Overview
Before this feature, any caller who knew an agency's x-agency-id header value could register business accounts regardless of whether the agency had activated registration, had any plans available, or had capacity for more users.
Three fail-fast gates now run in order at registration time:
| # | Gate | Condition | Error on fail |
|---|---|---|---|
| 1 | Flag check | agency.allowBusinessRegistration === true | IDENTITY.BUSINESS_REGISTRATION_DISABLED (HTTP 403) |
| 2 | Active plans check | agency has ≥1 active public plan | IDENTITY.NO_ACTIVE_AGENCY_PLANS (HTTP 409) |
| 3 | User capacity check | countByAgencyId < limit.quantity (or unlimited) | IDENTITY.USER_LIMIT_REACHED (HTTP 409) |
Gates run cheapest-first. Gate 1 is a pure memory read. Gate 2 and Gate 3 hit the database only if all preceding gates pass.
Two admin guards protect the reverse direction — preventing an admin from creating an inconsistent state:
| Guard | When it fires | Error |
|---|---|---|
| Guard 1 — Enable flag | Admin sets allowBusinessRegistration: true with zero active plans | IDENTITY.CANNOT_ENABLE_REGISTRATION_WITHOUT_PLANS (HTTP 409) |
| Guard 2 — Last plan | Admin deactivates or deletes the last active plan while flag is ON | PLANS.CANNOT_DEACTIVATE_LAST_PLAN_REGISTRATION_ENABLED (HTTP 409) |
allowBusinessRegistration Flag
The flag is a boolean field on the Organization entity, stored in column identity.organizations.allow_business_registration.
Semantics:
- Meaningful only for organizations of type
AGENCY. - Defaults to
falseonOrganization.newAgency()andOrganization.newBusiness(). - BUSINESS-type organizations cannot set this field; attempting to do so via
PATCH /organization/myresults inIDENTITY.FLAG_NOT_APPLICABLE_TO_BUSINESS(HTTP 409). - Turning the flag OFF is always allowed, regardless of plan state.
Gate 1 — Registration Flag Check
Location: RegisterUserBusinessOwnerHandler (identity module application layer)
When a caller sends POST /identity/auth/user/register/business with x-agency-id, the handler loads the agency and immediately checks agency.allowBusinessRegistration. If it is false, the request is rejected before any cross-module call is made.
HTTP 403 Forbidden
{
"code": "IDENTITY.BUSINESS_REGISTRATION_DISABLED",
"message": "El registro público de negocios está deshabilitado para esta agencia"
}Gate 2 — Active Agency Plans Check
Location: RegisterUserBusinessOwnerHandler, uses ISubscriptionLimitsGateway.hasActiveAgencyPlans()
If Gate 1 passes, the handler calls the Plans module (via gateway) to verify that the agency has at least one public, active plan available for new businesses to subscribe to. If zero plans exist, the registration is rejected.
HTTP 409 Conflict
{
"code": "IDENTITY.NO_ACTIVE_AGENCY_PLANS",
"message": "Esta agencia no tiene planes activos disponibles para nuevos negocios"
}Gate 3 — User Capacity Check
Location: RegisterUserBusinessOwnerHandler, uses ISubscriptionLimitsGateway.getResourceLimit() and IUserRepository.countByAgencyId()
If Gates 1 and 2 pass, the handler fetches the USERS resource limit from the agency's active platform subscription and counts all existing users in the agency.
Resolution table:
getResourceLimit result | limit.quantity | countByAgencyId | Outcome |
|---|---|---|---|
null | — | any | ✅ Pass (no subscription — log warning) |
| non-null | -1 | any | ✅ Pass (unlimited) |
| non-null | > 0 | < quantity | ✅ Pass |
| non-null | > 0 | >= quantity | ❌ Reject |
When limit === null (no active platform subscription), registration is allowed but the handler emits a structured warning log with event registration_allowed_without_subscription and payload { agencyId, userCount }. This is a diagnostic signal for the billing team — it does not block the user.
HTTP 409 Conflict
{
"code": "IDENTITY.USER_LIMIT_REACHED",
"message": "Se ha alcanzado el límite de usuarios para esta agencia"
}Guard 1 — Enable Flag Requires Active Plans
Location: UpdateMyOrganizationHandler
When an agency admin sends PATCH /organization/my with { allowBusinessRegistration: true } and the flag is currently false, the handler performs a pre-condition check via ISubscriptionLimitsGateway.hasActiveAgencyPlans(). If no active plans exist, the toggle is rejected.
Turning the flag OFF (false) is always allowed — no plans check is performed.
HTTP 409 Conflict
{
"code": "IDENTITY.CANNOT_ENABLE_REGISTRATION_WITHOUT_PLANS",
"message": "No se puede habilitar el registro de negocios sin al menos un plan activo"
}Admin workflow: Create or activate at least one plan first, then enable the flag. See How to enable business registration.
Guard 2 — Cannot Deactivate the Last Plan While Flag Is ON
Location: UpdateAgencyPlanHandler (deactivation via PATCH /agency-plans/:id with { isActive: false }) and SoftDeleteAgencyPlanHandler (DELETE /agency-plans/:id)
Both plan mutation handlers check IOrganizationFlagsGateway.isBusinessRegistrationEnabled() before applying the change. If the flag is ON and the plan being deactivated or deleted is the last active public plan of the agency, the operation is rejected.
Deactivating or deleting a plan when either condition is false (flag OFF, or at least one other plan remains) always succeeds.
HTTP 409 Conflict
{
"code": "PLANS.CANNOT_DEACTIVATE_LAST_PLAN_REGISTRATION_ENABLED",
"message": "No se puede desactivar o eliminar el último plan activo mientras el registro de negocios está habilitado"
}Registration Status Endpoint
A new authenticated endpoint allows agency admins to query the current registration status without needing to read separate organization and subscription APIs:
GET /organization/my/registration-status
Authorization: Bearer <access_token> (agency admin role required)Response:
json
{
"allowBusinessRegistration": true,
"hasActivePlans": true,
"userCount": 7,
"userLimit": 10,
"limitSource": "platform_plan"
}limitSource values:
| Value | Meaning |
|---|---|
"platform_plan" | Active subscription with a finite USERS limit (quantity > 0) |
"unlimited" | Active subscription with unlimited users (quantity === -1) |
"no_subscription" | No active platform subscription found |
When limitSource is "unlimited" or "no_subscription", userLimit is null.
Source: apps/api/src/modules/identity/application/queries/org/get-registration-status.query.ts
Error Code Reference
All error codes introduced by this feature:
| Code | HTTP | Raised by | When |
|---|---|---|---|
IDENTITY.BUSINESS_REGISTRATION_DISABLED | 403 | Gate 1 | agency.allowBusinessRegistration === false |
IDENTITY.NO_ACTIVE_AGENCY_PLANS | 409 | Gate 2 | Agency has zero active public plans |
IDENTITY.USER_LIMIT_REACHED | 409 | Gate 3 | countByAgencyId >= limit.quantity (and limit is bounded) |
IDENTITY.CANNOT_ENABLE_REGISTRATION_WITHOUT_PLANS | 409 | Guard 1 | Admin toggles flag ON with zero plans |
IDENTITY.FLAG_NOT_APPLICABLE_TO_BUSINESS | 409 | UpdateMyOrganizationHandler | BUSINESS-type org attempts to set the flag |
PLANS.CANNOT_DEACTIVATE_LAST_PLAN_REGISTRATION_ENABLED | 409 | Guard 2 | Admin deactivates or deletes the last plan while flag is ON |
HTTP code rationale:
403 Forbidden— Gate 1 is a permission-like check: the feature is turned off for this agency.409 Conflict— All other gates and guards signal a conflict between the request and the current resource state.
Data Flow
Happy Path — Public Business Registration
sequenceDiagram
participant C as Caller
participant UAC as UserAuthController
participant RUH as RegisterUserBusinessOwnerHandler
participant SLG as SubscriptionLimitsGateway
participant UR as UserRepository
participant DB as Database
C->>+UAC: POST /identity/auth/user/register/business
Note over UAC: x-agency-id header
UAC->>+RUH: RegisterUserBusinessOwnerCommand(dto, agencyId)
RUH->>DB: orgRepo.findById(agencyId)
DB-->>RUH: Agency entity
Note over RUH: Gate 1 — flag check
RUH->>RUH: agency.allowBusinessRegistration === true ✓
Note over RUH: Gate 2 — plans check
RUH->>+SLG: hasActiveAgencyPlans(agencyId)
SLG-->>-RUH: true ✓
Note over RUH: Gate 3 — capacity check
RUH->>+SLG: getResourceLimit(agencyId, USERS)
SLG-->>-RUH: { quantity: 10 }
RUH->>+UR: countByAgencyId(agencyId)
UR-->>-RUH: 7 (< 10) ✓
RUH->>DB: create User + Business org + Member + Role
RUH-->>-UAC: Result.ok()
UAC-->>-C: 201 Created
Related Docs
- ADR 007 — Bidirectional Gateway Pattern — architectural rationale
- Settings — Registro y Onboarding — admin UI
- Runbook — Enable Business Registration — step-by-step admin guide
- Plans Catalog and Management — creating and managing plans
- API Module Architecture — module dependency graph