Skip to content

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:

#GateConditionError on fail
1Flag checkagency.allowBusinessRegistration === trueIDENTITY.BUSINESS_REGISTRATION_DISABLED (HTTP 403)
2Active plans checkagency has ≥1 active public planIDENTITY.NO_ACTIVE_AGENCY_PLANS (HTTP 409)
3User capacity checkcountByAgencyId < 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:

GuardWhen it firesError
Guard 1 — Enable flagAdmin sets allowBusinessRegistration: true with zero active plansIDENTITY.CANNOT_ENABLE_REGISTRATION_WITHOUT_PLANS (HTTP 409)
Guard 2 — Last planAdmin deactivates or deletes the last active plan while flag is ONPLANS.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 false on Organization.newAgency() and Organization.newBusiness().
  • BUSINESS-type organizations cannot set this field; attempting to do so via PATCH /organization/my results in IDENTITY.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 resultlimit.quantitycountByAgencyIdOutcome
nullany✅ Pass (no subscription — log warning)
non-null-1any✅ 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:

ValueMeaning
"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:

CodeHTTPRaised byWhen
IDENTITY.BUSINESS_REGISTRATION_DISABLED403Gate 1agency.allowBusinessRegistration === false
IDENTITY.NO_ACTIVE_AGENCY_PLANS409Gate 2Agency has zero active public plans
IDENTITY.USER_LIMIT_REACHED409Gate 3countByAgencyId >= limit.quantity (and limit is bounded)
IDENTITY.CANNOT_ENABLE_REGISTRATION_WITHOUT_PLANS409Guard 1Admin toggles flag ON with zero plans
IDENTITY.FLAG_NOT_APPLICABLE_TO_BUSINESS409UpdateMyOrganizationHandlerBUSINESS-type org attempts to set the flag
PLANS.CANNOT_DEACTIVATE_LAST_PLAN_REGISTRATION_ENABLED409Guard 2Admin 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