# API Endpoints Documentation

Internal engineering document. Do not publish this page to platform users. It contains API paths, roles, payloads, and error behavior intended for authorized engineers and super-admin testing only.

This document describes the available HTTP API endpoints for the Smart Estate Visitor Management Platform backend.

## Base URL

- Local development: `http://localhost:4000/api/v1`
- GateWise production target: `https://api.gatewise.ng/v1`
- Swagger UI: `http://localhost:4000/api/docs` (super-admin console login required)
- Browser sandbox: `http://localhost:4000/api/v1/sandbox` (super-admin console login required)
- GateWise Swagger UI: `https://api.gatewise.ng/docs` (super-admin console login required)
- GateWise browser sandbox: `https://api.gatewise.ng/v1/sandbox` (super-admin console login required)

## Conventions

- Protected endpoints require `Authorization: Bearer <accessToken>`.
- Swagger UI and the browser sandbox require either a super-admin console cookie or a `SUPER_ADMIN` bearer token.
- Request bodies use `Content-Type: application/json`.
- Endpoint headings use the local `/api/v1` prefix. On the GateWise API subdomain, replace `/api/v1` with `/v1`.
- All tenant data is estate-scoped. Non-super-admin users can only access their own `estateId`.
- `SUPER_ADMIN` users can access all estates, but many estate-scoped routes still require an explicit `estateId`.
- `SALES_ADMIN` is a platform-scoped role. It can work across estates through feature-enabled routes, but payment records and sensitive integration features are off by default.
- Feature settings are enforced by the API. Frontend clients should call `GET /api/v1/features/me` after login and hide disabled screens/actions.
- Inactive users with status `SUSPENDED` or `DISABLED` cannot log in or refresh tokens.
- Auth endpoints are rate-limited.

## Authentication

### GET /api/v1/console/login
Open the super-admin console login form for Swagger UI and the browser sandbox.

Only active `SUPER_ADMIN` users can create the console session cookie. This route is intentionally excluded from Swagger.

### POST /api/v1/console/request-otp
Send an OTP for super-admin console login.

Request body accepts form data:
- `phone`
- `returnTo`

### POST /api/v1/console/login
Verify the console OTP and set the HTTP-only console cookie.

Request body accepts form data:
- `phone`
- `otp`
- `returnTo`

### GET /api/v1/sandbox
Open the local API testing sandbox after super-admin console login.

This protected browser page helps test the API during development. It includes seeded login shortcuts, stores access and refresh tokens in browser local storage, loads example requests for common endpoints, and displays raw response bodies.

Response:
- `200 text/html`

## Estate Admin Onboarding & Billing

These routes support public lead generation and pre-activation subscription/payment capture.

### GET /api/v1/onboarding/subscription-plans
List active subscription plans.

Plans seeded by default:

| Plan | Estate Size | Monthly Price |
| --- | --- | --- |
| Starter | 0-100 residents/units | NGN 250,000 |
| Growth | 101-300 residents/units | NGN 600,000 |
| Enterprise | 300+ residents/units | Custom pricing |

### GET /api/v1/onboarding/payment-providers
List payment provider settings and recommendations.

Query params:
- `estateId` optional. When supplied, the response includes estate override and effective enabled status.

Response also includes sandbox runtime fields:
- `sandboxMode`: `sandbox` or `live`.
- `sandboxConfigured`: `true` only when the provider's required env credentials are present and not placeholders.
- `sandboxBaseUrl`: base URL currently configured for provider API calls.

Recommended gateways:

| Use Case | Recommended Gateways |
| --- | --- |
| Nigeria payments | Flutterwave, Monnify |
| Backup/local alternative | Paystack |
| International/diaspora payments | Stripe, Flutterwave |
| Luxury estates | Stripe, Paystack, Flutterwave |

### POST /api/v1/onboarding/estate-admin-signup
Submit a public estate-admin lead.

Request body:
```json
{
  "estateName": "GreenPark Estate",
  "location": "Lagos, Nigeria",
  "unitCount": 120,
  "estateType": "Residential gated community",
  "adminName": "Estate Admin",
  "adminPhone": "+2348000000990",
  "adminEmail": "estate.admin@example.com",
  "metadata": {
    "source": "website"
  }
}
```

Result:
- Estate is created with `status: PENDING`.
- Estate signup is created with `signupStatus: PENDING_APPROVAL`.
- Estate admin user is created with `status: SUSPENDED`.
- Super-admin notification is recorded in audit logs.

### POST /api/v1/onboarding/estates/:estateId/select-plan
Select subscription plan and payment provider after super-admin approval.

Request body:
```json
{
  "adminPhone": "+2348000000990",
  "planCode": "GROWTH",
  "paymentProvider": "FLUTTERWAVE"
}
```

Rules:
- Estate signup must be `APPROVED`.
- Selected provider must be effectively enabled for the estate.

### POST /api/v1/onboarding/estates/:estateId/payments/initialize
Initialize hosted sandbox checkout after plan/provider selection.

Request body:
```json
{
  "adminPhone": "+2348000000990",
  "customerEmail": "estate.admin@example.com",
  "customerName": "Estate Admin",
  "planCode": "GROWTH",
  "paymentProvider": "FLUTTERWAVE",
  "metadata": {
    "channel": "card"
  }
}
```

Response:
```json
{
  "payment": {
    "id": "payment-uuid",
    "estateId": "00000000-0000-4000-8000-000000000001",
    "provider": "FLUTTERWAVE",
    "reference": "FLU-1778881112222-a1b2c3d4",
    "status": "PENDING"
  },
  "checkout": {
    "provider": "FLUTTERWAVE",
    "mode": "sandbox",
    "reference": "FLU-1778881112222-a1b2c3d4",
    "providerReference": "FLU-1778881112222-a1b2c3d4",
    "checkoutUrl": "https://checkout.flutterwave.com/v3/hosted/pay/example",
    "accessCode": null
  },
  "nextStep": "Open checkoutUrl, complete sandbox payment, then ask a super admin to confirm the payment."
}
```

Rules:
- Estate signup must be `APPROVED`.
- Selected provider must be effectively enabled for the estate.
- Provider sandbox credentials must be configured in `.env` or `apps/api/.env`.
- `amountKobo` is required when the selected plan has custom pricing.

### GET /api/v1/onboarding/payments/callback
Receive hosted payment redirect callback.

Common query params:
- `provider` optional. Provider name when your return URL includes it.
- `reference` optional. Paystack and Stripe callback reference.
- `tx_ref` optional. Flutterwave transaction reference.
- `transaction_id` optional. Flutterwave transaction id.
- `session_id` optional. Stripe checkout session id.
- `transactionReference` optional. Monnify transaction reference.

Result:
- Callback query data is stored on the payment metadata.
- Estate is not activated automatically.
- Super admin still confirms payment, and hosted payments are verified against the provider sandbox before activation.

### POST /api/v1/onboarding/estates/:estateId/payments
Record a manual/offline payment reference after bank transfer or non-hosted checkout.

Request body:
```json
{
  "adminPhone": "+2348000000990",
  "reference": "FLW-TEST-REFERENCE-001",
  "metadata": {
    "channel": "card"
  }
}
```

Result:
- Payment is stored as `PENDING`.
- Estate subscription status becomes `PAYMENT_PENDING`.
- Super admin must confirm payment before activation.

### POST /api/v1/auth/login
Request an OTP for a registered phone number.

Delivery is controlled by the global super-admin OTP policy. `DEV` mode returns `devOtp`; live modes send SMS through Termii, Africa's Talking, or a custom HTTP SMS endpoint. The enabled provider list and provider priority order apply to all phone OTP login, including normal auth and super-admin console auth.

Request body:
```json
{
  "phone": "+2348000000001"
}
```

Response:
```json
{
  "message": "OTP sent",
  "provider": "DEV",
  "providerPriority": ["DEV"],
  "expiresInSeconds": 300,
  "devOtp": "123456"
}
```

### POST /api/v1/auth/verify-otp
Verify OTP and receive access and refresh tokens.

Request body:
```json
{
  "phone": "+2348000000001",
  "otp": "123456"
}
```

Response:
```json
{
  "accessToken": "...",
  "refreshToken": "..."
}
```

### POST /api/v1/auth/refresh
Refresh access and refresh tokens using a refresh token.

Request body:
```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlYzk4ZjRkZi1hZDgwLTRhNTEtYmQ1Zi05MmQyNTU3MTU0ZTUiLCJlc3RhdGVJZCI6bnVsbCwicm9sZSI6IlNVUEVSX0FETUlOIiwiaWF0IjoxNzc4ODc4NjY1LCJleHAiOjE3ODE0NzA2NjV9.FjtR7X9kVAL8wbmi4UCbWWaLWs9imBbtdgzEDFsgEYY"
}
```

Response:
```json
{
  "accessToken": "...",
  "refreshToken": "..."
}
```

### POST /api/v1/auth/signup
Create a new user account. This endpoint is protected.

Allowed creators:
- `SUPER_ADMIN`: can create any role
- `SALES_ADMIN`: can be created by `SUPER_ADMIN` and does not require `estateId`
- `ESTATE_ADMIN`: can create `SECURITY_SUPERVISOR`, `GUARD`, and `RESIDENT`
- `SECURITY_SUPERVISOR`: can create `GUARD` and `RESIDENT`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "name": "New Resident",
  "phone": "+2348000000100",
  "role": "RESIDENT",
  "unit": "B14"
}
```

Role-specific fields:
- `RESIDENT` requires `unit`.
- Resident phone numbers must be unique; duplicate phone attempts return `409 Conflict`.
- `GUARD` accepts optional `deviceId`.
- Estate-scoped roles require `estateId`.
- `SUPER_ADMIN` and `SALES_ADMIN` do not require `estateId`.

## Feature Access

Feature access is an API-enforced layer on top of safe route role boundaries.

- Super admins can update global feature settings for any role, including `SALES_ADMIN`.
- Super admins can set estate-specific overrides when `estateId` is supplied.
- Estate admins can update only supported `RESIDENT` and `GUARD` features for their own estate.
- Estate admins cannot enable resident or guard features that are not included in the estate subscription plan. Those rows return `planEnabled: false`, `effectiveEnabled: false`, and `source: "PLAN"`.
- `SALES_ADMIN` route access is controlled by enabled feature keys. By default sales admins can view platform/estate operations and create/update estates, but payment records and sensitive integration features stay disabled.
- Sales partner earnings/contact features are WebApp-only. MobileApp should route `SALES_ADMIN` to an access-blocked screen and direct partners to the WebApp.
- WebApp and MobileApp should call `GET /api/v1/features/me` after login and after token refresh if the active user changes.

### GET /api/v1/features/catalog

Returns stable feature keys, labels, categories, default roles, and whether the feature can be estate-overridden.

### GET /api/v1/features/me

Returns effective feature access for the logged-in user after default, global, and estate settings.

Response item example:
```json
{
  "key": "CHECKIN_MANUAL",
  "label": "Manual check-in",
  "role": "GUARD",
  "estateId": "00000000-0000-4000-8000-000000000001",
  "defaultEnabled": true,
  "globalEnabled": true,
  "estateEnabled": false,
  "effectiveEnabled": false,
  "source": "ESTATE"
}
```

### GET /api/v1/features/role-settings?role=SALES_ADMIN

Inspect role feature settings. `SUPER_ADMIN` can inspect any role. `ESTATE_ADMIN` can inspect only `RESIDENT` or `GUARD` settings in their own estate.

### PATCH /api/v1/features/role-settings/:role

Update feature settings for a role.

Enable payment visibility for sales admins:
```json
{
  "settings": [
    { "feature": "PAYMENTS_VIEW", "enabled": true },
    { "feature": "PAYMENT_PROVIDERS_VIEW", "enabled": true }
  ]
}
```

Enable partner earnings/contact for sales admins:
```json
{
  "settings": [
    { "feature": "SALES_PARTNER_EARNINGS_VIEW", "enabled": true },
    { "feature": "SALES_PARTNER_ADMIN_CONTACT", "enabled": true }
  ]
}
```

Disable manual check-in for guards in one estate:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "settings": [
    { "feature": "CHECKIN_MANUAL", "enabled": false }
  ]
}
```

## Protected Routes

> All routes below require a valid `Authorization: Bearer <accessToken>` header.

### Estates

#### GET /api/v1/estates
List estates.

Allowed roles:
- `SUPER_ADMIN`
- `SALES_ADMIN` when `ESTATES_VIEW` is enabled
- `ESTATE_ADMIN`

#### POST /api/v1/estates
Create an estate.

Allowed roles:
- `SUPER_ADMIN`
- `SALES_ADMIN` when `ESTATES_MANAGE` is enabled

Request body:
```json
{
  "name": "GreenPark Estate",
  "location": "Lagos, Nigeria",
  "settings": {
    "timezone": "Africa/Lagos"
  },
  "branding": {
    "primaryColor": "#0f766e"
  }
}
```

### Guards

#### GET /api/v1/guards
List guards for an estate.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Query params:
- `estateId` required for `SUPER_ADMIN`, optional for estate-scoped users.

#### POST /api/v1/guards
Create a guard account and guard profile.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "name": "Main Gate Guard",
  "phone": "+2348000000200",
  "deviceId": "guard-device-002"
}
```

Device assignment notes:
- Guards do not sign themselves up; `ESTATE_ADMIN` or `SECURITY_SUPERVISOR` creates the account.
- `deviceId` is optional at creation time, so a guard can be created before a gate tablet/phone is known.
- When a scan/check-in request includes `deviceId` and the guard has no assigned device, the API binds that device to the guard automatically.
- Once a guard is assigned to a device, future scan/check-in/checkout requests with a different `deviceId` return `403 Forbidden`.
- Requests without `deviceId` remain accepted for backward compatibility, but Phase 2 gate apps should send `deviceId` for tracking.

Duplicate resident phone numbers return `409 Conflict`.

### Residents

#### GET /api/v1/residents
List residents for an estate.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`
- `RESIDENT`

Query params:
- `estateId` required for `SUPER_ADMIN`, optional for estate-scoped users.
- `q` optional search by name, unit, or phone.

#### POST /api/v1/residents
Create a resident account and resident profile.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "name": "Ada Okafor",
  "phone": "+2348000000300",
  "unit": "A12"
}
```

### Visitors

#### GET /api/v1/visitors
List estate-scoped visitors.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`
- `GUARD`
- `RESIDENT`

Query params:
- `estateId` required for `SUPER_ADMIN`, optional for estate-scoped users.
- `q` optional search by name, phone, or vehicle plate.

#### POST /api/v1/visitors
Create an estate-scoped visitor.

Allowed roles:
- `ESTATE_ADMIN`
- `RESIDENT`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "name": "John Doe",
  "phone": "+2348111111111",
  "vehiclePlate": "LND-123-AB",
  "photoUrl": "https://example.com/photo.jpg"
}
```

#### GET /api/v1/visitors/passes
List visitor passes for an estate.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`
- `GUARD`
- `RESIDENT`

Query params:
- `estateId` required for `SUPER_ADMIN`, optional for estate-scoped users.

#### POST /api/v1/visitors/passes
Create a visitor pass.

Allowed roles:
- `ESTATE_ADMIN`
- `RESIDENT`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "visitorId": "visitor-uuid",
  "hostResidentId": "resident-uuid",
  "passType": "SINGLE_USE",
  "validFrom": "2026-05-16T08:00:00.000Z",
  "validUntil": "2026-05-16T18:00:00.000Z",
  "maxEntries": 1
}
```

Notes:
- `visitorId` and `hostResidentId` must both belong to the target estate.
- `validUntil` must be after `validFrom`.
- Response includes `qrPayload.token` and `visitorCode`.

#### POST /api/v1/visitors/passes/:id/revoke
Revoke a visitor pass.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

### Checkin

All check-in routes require `GUARD`. The submitted `guardId` must belong to the authenticated guard user and estate.

#### POST /api/v1/checkin/scan
Check in a visitor with QR token.

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "qrToken": "qr-token",
  "guardId": "guard-uuid",
  "deviceId": "gate-tablet-001",
  "entryGate": "Main Gate",
  "offlineEventId": "optional-client-event-id"
}
```

#### POST /api/v1/checkin/manual
Check in a visitor with manual visitor code.

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "visitorCode": "ABC12345",
  "guardId": "guard-uuid",
  "deviceId": "gate-tablet-001",
  "entryGate": "Main Gate",
  "offlineEventId": "optional-client-event-id"
}
```

#### POST /api/v1/checkin/checkout
Check out a visit session.

Request body:
```json
{
  "visitSessionId": "visit-session-uuid",
  "guardId": "guard-uuid",
  "deviceId": "gate-tablet-001",
  "exitGate": "Main Gate"
}
```

### Incidents

#### GET /api/v1/incidents
List incidents for an estate.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`
- `GUARD`

Query params:
- `estateId` required for `SUPER_ADMIN`, optional for estate-scoped users.

#### POST /api/v1/incidents
Create an incident report.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`
- `GUARD`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "guardId": "guard-uuid",
  "type": "SECURITY",
  "description": "Suspicious activity near the main gate",
  "metadata": {
    "gate": "Main Gate"
  }
}
```

For guard users, `guardId` must match the authenticated guard identity.

### Offline Sync

#### GET /api/v1/offline/bootstrap
Download active passes, residents, and recent logs for offline gate operation.

Allowed roles:
- `GUARD`
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Query params:
- `estateId` required.

#### POST /api/v1/offline/sync
Submit offline event records.

Allowed roles:
- `GUARD`
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "deviceId": "guard-device-001",
  "items": [
    {
      "clientEventId": "offline-event-001",
      "eventType": "VISITOR_CHECKED_IN",
      "payload": {
        "visitorCode": "ABC12345"
      }
    }
  ]
}
```

Note: offline sync currently stores submitted event records and marks them `APPLIED`; domain replay is a future enhancement.

### Notifications

#### POST /api/v1/notifications/send
Queue a notification request in the audit trail.

Allowed roles:
- `ESTATE_ADMIN`
- `SECURITY_SUPERVISOR`

Request body:
```json
{
  "estateId": "00000000-0000-4000-8000-000000000001",
  "channel": "SMS",
  "recipient": "+2348111111111",
  "message": "Your visitor pass is ready",
  "metadata": {
    "visitorPassId": "visitor-pass-uuid"
  }
}
```

### Super Admin And Sales Admin Dashboard

Admin routes below require a `SUPER_ADMIN` bearer token by default. `SALES_ADMIN` can access only routes whose feature has been enabled for `SALES_ADMIN`; payment records, payment confirmation, payment-provider management, OTP settings, and integrations are disabled for sales admins by default.

When `SALES_ADMIN` has dashboard or signup visibility but not `PAYMENTS_VIEW`, payment counts, payment arrays, and payment-related audit activity are omitted from responses.

- `GET /api/v1/admin/dashboard` - counts and recent audit logs
- `GET /api/v1/admin/estate-signups?status=`
- `PATCH /api/v1/admin/estate-signups/:estateId/review`
- `PATCH /api/v1/admin/estate-signups/:estateId/confirm-payment`
- `GET /api/v1/admin/subscription-plans?includeInactive=`
- `POST /api/v1/admin/subscription-plans`
- `PATCH /api/v1/admin/subscription-plans/:code`
- `GET /api/v1/admin/sales-partners`
- `GET /api/v1/admin/sales-partners/me`
- `GET /api/v1/admin/sales-partners/me/earnings`
- `GET /api/v1/admin/sales-partners/:salesAdminId/earnings`
- `PATCH /api/v1/admin/sales-partners/:salesAdminId`
- `PATCH /api/v1/admin/estates/:id/sales-partner`
- `POST /api/v1/admin/estates/:estateId/admin-contact`
- `GET /api/v1/admin/otp/config`
- `PATCH /api/v1/admin/otp/config`
- `GET /api/v1/admin/payment-providers?estateId=`
- `PATCH /api/v1/admin/payment-providers/:provider`
- `PATCH /api/v1/admin/estates/:estateId/payment-providers/:provider`
- `GET /api/v1/admin/integrations/templates`
- `GET /api/v1/admin/integrations/config`
- `GET /api/v1/admin/integrations/config/:provider`
- `PATCH /api/v1/admin/integrations/config/:provider`
- `GET /api/v1/admin/users?estateId=&role=&status=`
- `PATCH /api/v1/admin/users/:id`
- `GET /api/v1/admin/estates`
- `PATCH /api/v1/admin/estates/:id`
- `GET /api/v1/admin/residents?estateId=`
- `PATCH /api/v1/admin/residents/:id`
- `GET /api/v1/admin/guards?estateId=`
- `PATCH /api/v1/admin/guards/:id`
- `GET /api/v1/admin/visitors?estateId=`
- `PATCH /api/v1/admin/visitors/:id`
- `GET /api/v1/admin/visitor-passes?estateId=&status=`
- `PATCH /api/v1/admin/visitor-passes/:id`
- `GET /api/v1/admin/visit-sessions?estateId=`
- `GET /api/v1/admin/incidents?estateId=`
- `PATCH /api/v1/admin/incidents/:id`
- `GET /api/v1/admin/audit-logs?estateId=`

Example user status update:
```json
{
  "status": "SUSPENDED"
}
```

Example visitor pass update:
```json
{
  "status": "REVOKED"
}
```

Example estate update:
```json
{
  "name": "GreenPark Estate",
  "location": "Lagos, Nigeria",
  "settings": {
    "timezone": "Africa/Lagos"
  }
}
```

Example estate signup review:
```json
{
  "status": "APPROVED",
  "reviewNotes": "Approved for subscription plan selection."
}
```

Example payment confirmation:
```json
{
  "paymentReference": "FLU-1778881112222-a1b2c3d4"
}
```

Hosted checkout payments are verified against the selected provider sandbox before the estate is activated. Manual references recorded with `POST /api/v1/onboarding/estates/:estateId/payments` remain manually confirmable.

### Subscription Plan Feature Ownership

Only `SUPER_ADMIN` can create or update plan definitions, pricing, resident ranges, custom pricing, and included feature keys. Estate admins can toggle allowed resident/guard features for their estate, but the API blocks any enabled feature that is not present on the estate plan.

Create a custom plan:
```json
{
  "code": "CUSTOM_LUXURY",
  "name": "Luxury Custom",
  "minResidents": 301,
  "maxResidents": null,
  "monthlyPriceKobo": 150000000,
  "currency": "NGN",
  "customPricing": true,
  "description": "Custom plan controlled by super admin.",
  "isActive": true,
  "features": ["RESIDENTS_VIEW", "GUARDS_VIEW", "VISITORS_VIEW", "VISITOR_PASSES_CREATE", "CHECKIN_SCAN"]
}
```

Update plan features:
```json
{
  "monthlyPriceKobo": 25000000,
  "isActive": true,
  "features": ["RESIDENTS_VIEW", "VISITORS_VIEW", "VISITOR_PASSES_CREATE", "CHECKIN_SCAN"]
}
```

### Sales Partner Controls

`SALES_ADMIN` remains a platform role. When super admin enables partner earnings on a sales admin profile, the profile response returns `profileLabel: "Partner"`. This partner workspace is WebApp-only; MobileApp should block `SALES_ADMIN` after login.

Super-admin partner endpoints:
- `GET /api/v1/admin/sales-partners` lists sales admins, partner profile state, commission percentage, and assigned estates.
- `PATCH /api/v1/admin/sales-partners/:salesAdminId` enables/disables partner earnings and sets commission percentage.
- `PATCH /api/v1/admin/estates/:id/sales-partner` assigns or clears the sales admin responsible for an estate.
- `GET /api/v1/admin/sales-partners/:salesAdminId/earnings` returns confirmed-payment totals and estimated commission.

Sales-admin WebApp endpoints:
- `GET /api/v1/admin/sales-partners/me` returns profile label, partner state, commission percentage, notes, and assigned estates.
- `GET /api/v1/admin/sales-partners/me/earnings` requires `SALES_PARTNER_EARNINGS_VIEW`.
- `POST /api/v1/admin/estates/:estateId/admin-contact` requires `SALES_PARTNER_ADMIN_CONTACT` and only works for estates assigned to the signed-in sales admin.

Enable partner earnings:
```json
{
  "enabled": true,
  "commissionPercent": 7.5,
  "notes": "Partner status enabled after super-admin review."
}
```

Assign an estate to a sales admin:
```json
{
  "salesAdminId": "user-uuid"
}
```

Contact estate admins:
```json
{
  "channel": "SMS",
  "message": "Please complete your subscription payment so we can activate your estate.",
  "metadata": {
    "reason": "PAYMENT_PENDING"
  }
}
```

### Third-Party Integration Keys

Super admins can manage payment gateway, SMS, and email provider keys without redeploying the API.

Secrets are write-only:
- Send secrets in `secretConfig`.
- GET responses only return `configured` and a masked suffix.
- Values are encrypted in the database with `INTEGRATION_CONFIG_SECRET`.

Supported built-in providers:
- Payment: `FLUTTERWAVE`, `MONNIFY`, `PAYSTACK`, `STRIPE`
- SMS: `TERMII`, `AFRICAS_TALKING`, `CUSTOM_HTTP`
- Email: `SMTP`, `SENDGRID`, `MAILGUN`, `CUSTOM_EMAIL_HTTP`

#### GET /api/v1/admin/integrations/templates

Returns provider templates and the public/secret key names each provider accepts.

#### GET /api/v1/admin/integrations/config

Returns sanitized integration config for all known and custom providers.

#### GET /api/v1/admin/integrations/config/:provider

Returns sanitized integration config for one provider.

#### PATCH /api/v1/admin/integrations/config/:provider

Creates or updates one provider.

Example for Flutterwave:
```json
{
  "category": "PAYMENT",
  "enabled": true,
  "publicConfig": {
    "baseUrl": "https://api.flutterwave.com/v3",
    "publicKey": "FLWPUBK_LIVE_xxx"
  },
  "secretConfig": {
    "secretKey": "FLWSECK_LIVE_xxx",
    "encryptionKey": "FLW_ENCRYPTION_KEY"
  }
}
```

Example for Termii:
```json
{
  "category": "SMS",
  "enabled": true,
  "publicConfig": {
    "baseUrl": "https://api.ng.termii.com",
    "senderId": "SmartEstate",
    "channel": "dnd"
  },
  "secretConfig": {
    "apiKey": "TERMII_API_KEY"
  }
}
```

Example for SMTP:
```json
{
  "category": "EMAIL",
  "enabled": true,
  "publicConfig": {
    "host": "smtp.example.com",
    "port": 587,
    "secure": false,
    "username": "mailer@example.com",
    "fromEmail": "no-reply@example.com",
    "fromName": "Smart Estate"
  },
  "secretConfig": {
    "password": "smtp-password"
  }
}
```

Payment and OTP providers read these refreshed values immediately, with `.env` values as fallback. Email providers are stored for the future email delivery module.

Example global or estate provider toggle:
```json
{
  "enabled": true
}
```

Example subscription plan update:
```json
{
  "monthlyPriceKobo": 25000000,
  "isActive": true
}
```

Example OTP provider config response:
```json
{
  "provider": "DEV",
  "enabledProviders": ["DEV"],
  "providerPriority": ["DEV"],
  "availableProviders": ["DEV", "TERMII", "AFRICAS_TALKING", "CUSTOM_HTTP"],
  "providers": [
    {
      "provider": "DEV",
      "enabled": true,
      "priority": 1,
      "configured": true,
      "activePrimary": true
    },
    {
      "provider": "TERMII",
      "enabled": false,
      "priority": null,
      "configured": false,
      "activePrimary": false
    }
  ],
  "codeLength": 6,
  "expiresInSeconds": 300,
  "maxAttempts": 5,
  "termii": {
    "configured": false,
    "baseUrl": "https://api.ng.termii.com",
    "senderId": "SmartEstate",
    "channel": "dnd"
  },
  "africasTalking": {
    "configured": false,
    "baseUrl": "https://api.africastalking.com/version1/messaging",
    "username": "sandbox"
  },
  "customHttp": {
    "configured": false,
    "url": "",
    "method": "POST",
    "hasHeaders": false,
    "hasBodyTemplate": true
  }
}
```

Example OTP provider switch:
```json
{
  "provider": "TERMII",
  "enabledProviders": ["TERMII", "AFRICAS_TALKING"],
  "providerPriority": ["TERMII", "AFRICAS_TALKING"],
  "ttlSeconds": 300,
  "codeLength": 6,
  "maxAttempts": 5,
  "exposeDevCode": false,
  "termiiBaseUrl": "https://api.ng.termii.com",
  "termiiApiKey": "TERMII_API_KEY",
  "termiiSenderId": "SmartEstate",
  "termiiChannel": "dnd"
}
```

Example custom/self-managed SMS OTP gateway:
```json
{
  "provider": "CUSTOM_HTTP",
  "enabledProviders": ["CUSTOM_HTTP"],
  "providerPriority": ["CUSTOM_HTTP", "TERMII", "AFRICAS_TALKING"],
  "customHttpUrl": "https://sms.example.com/send",
  "customHttpMethod": "POST",
  "customHttpHeadersJson": "{\"Authorization\":\"Bearer sms_api_key\"}",
  "customHttpBodyTemplate": "{\"to\":\"{{phone}}\",\"message\":\"{{message}}\",\"code\":\"{{otp}}\"}"
}
```

OTP config notes:
- Secrets are accepted but never returned by the config response.
- `enabledProviders` activates or deactivates providers.
- `providerPriority` controls fallback order; the first enabled provider is primary.
- The legacy `provider` field is a shortcut that enables that provider and moves it to the front when `enabledProviders` or `providerPriority` are not supplied.
- Runtime changes are in-memory. Mirror stable production settings into `.env`.
- Termii uses its SMS send endpoint with the configured route/channel. Use `dnd` for transactional OTP when enabled on the Termii account.
- Africa's Talking uses its SMS messaging endpoint. Use username `sandbox` and the sandbox base URL for their sandbox environment.

## Header Requirements

- `Content-Type: application/json`
- `Authorization: Bearer <accessToken>` for protected endpoints

## Environment Variables

The API uses the following environment variables in `apps/api/.env`:

- `DATABASE_URL`
- `REDIS_URL`
- `JWT_ACCESS_SECRET`
- `JWT_REFRESH_SECRET`
- `INTEGRATION_CONFIG_SECRET`
- `PORT`
- `API_PORT`
- `API_PREFIX`
- `API_VERSION`
- `CORS_ORIGINS`
- `SUPER_ADMIN_PHONE`
- `SUPER_ADMIN_NAME`
- `OTP_DEV_CODE`
- `OTP_DELIVERY_PROVIDER`
- `OTP_ENABLED_PROVIDERS`
- `OTP_PROVIDER_PRIORITY`
- `OTP_CODE_TTL_SECONDS`
- `OTP_CODE_LENGTH`
- `OTP_MAX_ATTEMPTS`
- `OTP_EXPOSE_DEV_CODE`
- `CONSOLE_SESSION_TTL`
- `TERMII_BASE_URL`
- `TERMII_API_KEY`
- `TERMII_SENDER_ID`
- `TERMII_CHANNEL`
- `AFRICASTALKING_BASE_URL`
- `AFRICASTALKING_USERNAME`
- `AFRICASTALKING_API_KEY`
- `AFRICASTALKING_SENDER_ID`
- `CUSTOM_OTP_URL`
- `CUSTOM_OTP_METHOD`
- `CUSTOM_OTP_HEADERS_JSON`
- `CUSTOM_OTP_BODY_TEMPLATE`
- `PAYMENT_GATEWAY_MODE`
- `PUBLIC_API_BASE_URL`
- `PAYMENT_CALLBACK_URL`
- `FLUTTERWAVE_BASE_URL`
- `FLUTTERWAVE_PUBLIC_KEY`
- `FLUTTERWAVE_SECRET_KEY`
- `FLUTTERWAVE_ENCRYPTION_KEY`
- `MONNIFY_BASE_URL`
- `MONNIFY_API_KEY`
- `MONNIFY_SECRET_KEY`
- `MONNIFY_CONTRACT_CODE`
- `PAYSTACK_BASE_URL`
- `PAYSTACK_PUBLIC_KEY`
- `PAYSTACK_SECRET_KEY`
- `STRIPE_BASE_URL`
- `STRIPE_PUBLIC_KEY`
- `STRIPE_SECRET_KEY`
- `STRIPE_SUCCESS_URL`
- `STRIPE_CANCEL_URL`

## Notes

- The login flow is OTP-based. `DEV` mode uses `OTP_DEV_CODE=123456` by default; live modes send SMS through Termii, Africa's Talking, or a custom HTTP endpoint.
- `POST /api/v1/auth/login` must be used for login; `GET /api/v1/auth/login` is not supported.
- Set `API_PREFIX=""` for the GateWise API subdomain deployment. This makes the production login URL `POST https://api.gatewise.ng/v1/auth/login`.
- Visitor records are estate-scoped.
- Check-in is concurrency-hardened so exhausted passes cannot be reused by rapid scans.
- Super-admin dashboard routes are backend API routes, not a separate visual UI.
- Payment provider settings are controlled globally and can be overridden per estate.
- OTP delivery settings are controlled globally by super admins through `/api/v1/admin/otp/config`; enabled providers and priority order apply to every phone OTP login.
- Swagger UI and the browser sandbox are hidden behind a super-admin console cookie or a super-admin bearer token.
- Protected app access for non-super-admin users requires the estate to be `ACTIVE`.

## Maintenance & Troubleshooting

### Verification Commands

Use `npm.cmd` on Windows PowerShell if `npm.ps1` is blocked.

```powershell
npm.cmd --workspace apps/api run db:generate
npm.cmd --workspace apps/api run test
npm.cmd --workspace apps/api run lint
npm.cmd --workspace apps/api run build
```

### Migration Workflow

Run migrations after schema changes:
```powershell
npm.cmd --workspace apps/api run db:migrate
```

If Prisma client generation fails with an `EPERM` rename error on `query_engine-windows.dll.node`, stop running API processes that may have Prisma loaded, then rerun:
```powershell
npm.cmd --workspace apps/api run db:generate
```

### Local Services

Start PostgreSQL and Redis:
```powershell
docker compose up -d
docker compose ps
```

Potential Docker issues:
- Docker Desktop is not running.
- The current shell cannot access the Docker engine.
- Port `5432`, `6379`, or `4000` is already in use.

### API Sandbox

Open the sandbox after starting the API and signing in as a super admin:
```text
http://localhost:4000/api/v1/sandbox
```

Use it to:
- log in with seeded users
- store the access token locally
- select endpoint examples
- edit JSON bodies
- inspect response status codes and payloads

### Common API Errors

- `401 Bearer token is required`: missing `Authorization` header.
- `401 Invalid bearer token`: token is expired, malformed, or signed with the wrong secret.
- `401 Super admin console login required`: docs or sandbox request is missing a valid super-admin console cookie or bearer token.
- `401 Account is not active`: user status is `SUSPENDED` or `DISABLED`.
- `403 Cross-estate access is not allowed`: request `estateId` does not match the authenticated user.
- `403 Guard identity does not match the authenticated user`: request `guardId` belongs to another guard.
- `403 Guard account is assigned to a different device`: request `deviceId` does not match the guard's assigned scanner.
- `403 Estate is not active`: estate has not completed approval and payment activation, or it was suspended/rejected.
- `409 This device is already assigned to another guard`: first-use binding attempted to reuse a device assigned elsewhere.
- `409 A resident with this phone number already exists`: resident phone number is already registered.
- `404 Active guard was not found in this estate`: guard does not exist, is inactive, or belongs to another estate.
- `400 Visitor pass is outside its validity window`: pass is not valid at the current server time.
- `400 Visitor pass has no remaining entries`: pass is exhausted.
- `400 Estate signup must be approved before selecting a plan`: super admin has not approved the lead yet.
- `400 <PROVIDER> is not enabled for this estate`: selected gateway is disabled globally or by estate override.
- `400 Gateway payment is not confirmed yet`: hosted checkout has not been completed successfully in the provider sandbox.
- `400 Termii OTP is not configured`: set `TERMII_API_KEY` and `TERMII_SENDER_ID` before switching OTP delivery to `TERMII`.
- `400 Africa's Talking OTP is not configured`: set `AFRICASTALKING_USERNAME` and `AFRICASTALKING_API_KEY` before switching OTP delivery to `AFRICAS_TALKING`.
- `400 Custom OTP HTTP provider is not configured`: set `CUSTOM_OTP_URL` before switching OTP delivery to `CUSTOM_HTTP`.
- `502 OTP provider request failed`: SMS provider request failed; check credentials, sender ID approval, route/channel support, and network access.
- `502 <PROVIDER> sandbox request failed`: provider sandbox returned an error or network call failed.
- `503 <PROVIDER> sandbox credentials are not configured`: relevant `*_REPLACE_ME` values still need real sandbox credentials.
- `429 Too Many Requests`: auth rate limit exceeded.
