## Tenant Control Plane API Contract

## Purpose

This document defines the PoC API contract for the tenant control-plane flows described in `feature-proposal.md`.

Scope:
- admin create/list clients
- client create/list tenants for the authenticated client
- client get tenant by id for the authenticated client
- client bootstrap trigger for authenticated-client tenants

This document focuses on request/response contracts and expected behavior.

## Conventions

- API keeps the technical word `tenant`
- business meaning of `tenant` is restaurant
- all identifiers are `uuid`
- codes are generated by the system
- `databaseName` is generated by the system
- timestamps use ISO 8601 UTC format

## Tenant Systems

For the PoC, tenant system configuration should favor flexibility over strict classification.

The tenant stores a simple collection of system names:

- `systems: string[]`

Examples:
- `Brink`
- `LogicControls`

Rules:
- values are simple names used as bootstrap input
- values are normalized case-insensitively
- duplicates are not allowed after normalization
- order is not significant
- the exact catalog can evolve later
- classification such as POS/KDS/Inventory is intentionally deferred

Normalization rule for the PoC:
- trim leading/trailing whitespace
- compare case-insensitively
- keep one canonical stored/display value per system entry

### TenantStatus

- `PendingBootstrap`
- `Active`
- `BootstrapFailed`

## API Surfaces

Two endpoint groups should exist over the same domain model.

### Admin surface

Used by platform operators.

Characteristics:
- may operate across all clients
- limited to client management in this PoC

### Client surface

Used by customer self-service users.

Characteristics:
- scoped to the authenticated client
- creates and lists tenants for the authenticated client
- reads basic details for a specific tenant owned by the authenticated client
- may only operate on tenants owned by the authenticated client

## 1. Create Client

`POST /admin/clients`

### Request

```json
{
  "name": "Acme Restaurants"
}
```

### Behavior

- creates a new client
- generates `code` automatically from `name`
- sets `isActive = true`
- `code` is immutable after creation

### Success Response

`201 Created`

```json
{
  "clientId": "d3fd91d5-8dc2-4d1b-a6f0-10a260da4b2e",
  "name": "Acme Restaurants",
  "code": "acme_restaurants",
  "isActive": true,
  "createdAt": "2026-05-14T11:00:00Z",
  "updatedAt": "2026-05-14T11:00:00Z"
}
```

### Validation Errors

`400 Bad Request`

Cases:
- missing name
- name too short or too long
- generated code conflicts and cannot be resolved according to system rules

## 2. List Clients

`GET /admin/clients`

### Behavior

- returns a simple admin list
- no paging or filtering in PoC

### Success Response

`200 OK`

```json
[
  {
    "clientId": "d3fd91d5-8dc2-4d1b-a6f0-10a260da4b2e",
    "name": "Acme Restaurants",
    "code": "acme_restaurants",
    "isActive": true,
    "tenantCount": 2,
    "createdAt": "2026-05-14T11:00:00Z",
    "updatedAt": "2026-05-14T11:00:00Z"
  }
]
```

## 3. Create Tenant For Current Client

`POST /client/tenants`

### Request

```json
{
  "name": "KFC Warsaw Zlote Tarasy",
  "systems": [
    "Brink",
    "LogicControls"
  ]
}
```

### Behavior

- resolves the current client from authentication context
- creates a tenant under that client
- generates `code` automatically from `name`
- derives `databaseName` automatically from `code`
- validates `databaseName` against Redshift naming rules
- sets `status = PendingBootstrap`
- sets `isActive = false`
- stores system names directly on tenant
- normalizes `systems` for duplicate detection before save

### Success Response

`201 Created`

```json
{
  "tenantId": "0f4bf6cf-18b0-4ec3-9628-d8ff0db96855",
  "clientId": "d3fd91d5-8dc2-4d1b-a6f0-10a260da4b2e",
  "name": "KFC Warsaw Zlote Tarasy",
  "code": "kfc_warsaw_zlote_tarasy",
  "databaseName": "kfc_warsaw_zlote_tarasy",
  "systems": [
    "Brink",
    "LogicControls"
  ],
  "status": "PendingBootstrap",
  "isActive": false,
  "createdAt": "2026-05-14T11:10:00Z",
  "updatedAt": "2026-05-14T11:10:00Z"
}
```

### Validation Errors

`400 Bad Request`

Cases:
- authenticated client cannot be resolved
- tenant name missing
- tenant name already exists within the current client
- generated tenant code conflicts and cannot be resolved according to system rules
- derived `databaseName` violates Redshift naming rules
- `systems` contains duplicate values after normalization

## 4. List Tenants For Current Client

`GET /client/tenants`

### Behavior

- resolves the current client from authentication context
- returns only tenants owned by that client
- no paging or filtering in PoC

### Success Response

`200 OK`

```json
[
  {
    "tenantId": "0f4bf6cf-18b0-4ec3-9628-d8ff0db96855",
    "name": "KFC Warsaw Zlote Tarasy",
    "code": "kfc_warsaw_zlote_tarasy",
    "databaseName": "kfc_warsaw_zlote_tarasy",
    "systems": [
      "Brink",
      "LogicControls"
    ],
    "status": "PendingBootstrap",
    "isActive": false,
    "createdAt": "2026-05-14T11:10:00Z",
    "updatedAt": "2026-05-14T11:10:00Z"
  }
]
```

## 5. Get Tenant By Id For Current Client

`GET /client/tenants/{tenantId}`

### Behavior

- resolves the current client from authentication context
- ensures the tenant belongs to that client
- returns basic tenant details for tenant overview and restaurant-worker access

### Success Response

`200 OK`

```json
{
  "tenantId": "0f4bf6cf-18b0-4ec3-9628-d8ff0db96855",
  "name": "KFC Warsaw Zlote Tarasy",
  "code": "kfc_warsaw_zlote_tarasy",
  "databaseName": "kfc_warsaw_zlote_tarasy",
  "systems": [
    "Brink",
    "LogicControls"
  ],
  "status": "PendingBootstrap",
  "isActive": false,
  "createdAt": "2026-05-14T11:10:00Z",
  "updatedAt": "2026-05-14T11:10:00Z"
}
```

### Failure Response

`404 Not Found` or `403 Forbidden`

Cases:
- tenant does not exist
- tenant does not belong to the authenticated client

## 6. Bootstrap Tenant For Current Client

`POST /client/tenants/{tenantId}/bootstrap`

### Behavior

- resolves the current client from authentication context
- ensures the tenant belongs to that client
- reads generated `databaseName`
- reads `systems` stored on tenant
- rejects bootstrap when `systems` is empty
- triggers bootstrap with required parameters
- endpoint is idempotent
- repeated calls must not duplicate infrastructure

### Success Response

`200 OK`

```json
{
  "tenantId": "0f4bf6cf-18b0-4ec3-9628-d8ff0db96855",
  "databaseName": "kfc_warsaw_zlote_tarasy",
  "status": "Active",
  "isActive": true,
  "bootstrapTriggered": true,
  "updatedAt": "2026-05-14T11:15:00Z"
}
```

### Failure Response

`409 Conflict` or `400 Bad Request`

Cases:
- tenant has no configured systems
- tenant does not belong to the authenticated client
- tenant cannot be bootstrapped from current state
- current bootstrap flow rejects the request

### Idempotent Re-run Response

`200 OK`

```json
{
  "tenantId": "26b1c5b3-221e-4cb7-af8a-474531b7db1b",
  "databaseName": "burger_house_downtown",
  "status": "Active",
  "isActive": true,
  "bootstrapTriggered": false,
  "updatedAt": "2026-05-14T12:20:00Z"
}
```

## Suggested DTO Shapes

### CreateClientRequest

```json
{
  "name": "string"
}
```

### ClientResponse

```json
{
  "clientId": "uuid",
  "name": "string",
  "code": "string",
  "isActive": true,
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}
```

### ClientListItemResponse

```json
{
  "clientId": "uuid",
  "name": "string",
  "code": "string",
  "isActive": true,
  "tenantCount": 0,
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}
```

### CreateTenantRequest

```json
{
  "name": "string",
  "systems": [
    "string"
  ]
}
```

### TenantResponse

```json
{
  "tenantId": "uuid",
  "clientId": "uuid",
  "name": "string",
  "code": "string",
  "databaseName": "string",
  "systems": [
    "string"
  ],
  "status": "PendingBootstrap",
  "isActive": false,
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}
```

### TenantListItemResponse

```json
{
  "tenantId": "uuid",
  "name": "string",
  "code": "string",
  "databaseName": "string",
  "systems": [
    "string"
  ],
  "status": "PendingBootstrap",
  "isActive": false,
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}
```

### BootstrapTenantResponse

```json
{
  "tenantId": "uuid",
  "databaseName": "string",
  "status": "Active",
  "isActive": true,
  "bootstrapTriggered": true,
  "updatedAt": "timestamp"
}
```

### TenantDetailsResponse

```json
{
  "tenantId": "uuid",
  "name": "string",
  "code": "string",
  "databaseName": "string",
  "systems": [
    "string"
  ],
  "status": "PendingBootstrap",
  "isActive": false,
  "createdAt": "timestamp",
  "updatedAt": "timestamp"
}
```

## Validation Rules Summary

### Client

- `name` required
- generated `code` unique
- `code` immutable

### Tenant

- authenticated client must exist and be resolvable
- `name` required
- `name` unique within a client
- generated `code` unique globally
- `code` immutable
- `databaseName` derived from `code`
- `databaseName` must satisfy Redshift naming rules
- `databaseName` immutable
- `systems` may be empty
- `systems` values must not repeat after normalization

### Tenant detail access

- `GET /client/tenants/{tenantId}` must return only tenants owned by the authenticated client
- tenant detail response should contain only the basic tenant fields needed by the tenant-facing UI

### Bootstrap precondition

- tenant must contain at least one configured system before bootstrap can be triggered

## Out of Scope

The following are intentionally not defined in this PoC contract:
- authentication and authorization model
- user-to-tenant access model
- pagination and filtering
- bootstrap internal orchestration details
- final full vendor catalog
