Files
crewli/dev-docs/API.md
2026-04-10 14:25:11 +02:00

250 lines
9.4 KiB
Markdown

# Crewli API Contract
Base path: `/api/v1/`
Auth: Bearer token (Sanctum)
## Auth
- `POST /auth/login`
- `POST /auth/logout`
- `GET /auth/me`
## Organisations
- `GET /organisations` — list (super admin)
- `POST /organisations` — create
- `GET /organisations/{org}` — show
- `PUT /organisations/{org}` — update
- `GET /organisations/{org}/members` — members
- `POST /organisations/{org}/invite` — invite user
## Events
- `GET /organisations/{org}/events` — list (top-level only by default)
- `GET /organisations/{org}/events?include_children=true` — include sub-events nested in response
- `GET /organisations/{org}/events?type=festival` — filter by event_type (festival|series|event)
- `POST /organisations/{org}/events` — create (supports `parent_event_id` for sub-events)
- `GET /organisations/{org}/events/{event}` — detail (includes children and parent if loaded)
- `PUT /organisations/{org}/events/{event}` — update (does NOT accept `status` — use transition endpoint)
- `POST /organisations/{org}/events/{event}/transition` — change event status via state machine (see below)
- `GET /organisations/{org}/events/{event}/children` — list sub-events of a festival/series
### Event Status Transitions
`POST /organisations/{org}/events/{event}/transition`
Body: `{ "status": "published" }`
Enforces a state machine: only valid forward (and select backward) transitions are allowed.
Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_transitions` when the transition is invalid or prerequisites are missing.
**Prerequisites checked:**
- `→ published`: name, start_date, end_date required
- `→ registration_open`: at least one time slot and one section required
**Festival cascade:** Transitioning a festival parent to `showday`, `teardown`, or `closed` automatically cascades to all children in an earlier status.
**EventResource** includes `allowed_transitions` (array of valid next statuses) so the frontend knows which buttons to show.
## Crowd Types
- `GET /organisations/{org}/crowd-types`
- `POST /organisations/{org}/crowd-types`
- `PUT /organisations/{org}/crowd-types/{type}`
- `DELETE /organisations/{org}/crowd-types/{type}`
## Companies
- `GET /organisations/{org}/companies`
- `POST /organisations/{org}/companies`
- `PUT /organisations/{org}/companies/{company}`
- `DELETE /organisations/{org}/companies/{company}`
## Section Categories
- `GET /organisations/{org}/section-categories` — distinct categories used across the organisation's events (for autocomplete). Returns `{ "data": ["Bar", "Podium", ...] }`
## Festival Sections
- `GET /events/{event}/sections`
- `POST /events/{event}/sections`
- `PUT /events/{event}/sections/{section}`
- `DELETE /events/{event}/sections/{section}`
- `POST /events/{event}/sections/reorder`
> **Festival context:** `{event}` can be a festival parent or a sub-event.
> On a festival parent, sections are for operational planning (build-up, teardown).
> For sub-events, `GET` automatically includes `cross_event` sections from the parent festival.
> Shifts on cross_event sections must use the **parent festival's event_id** in API calls,
> since the section's `event_id` points to the parent.
## Time Slots
- `GET /events/{event}/time-slots`
- `POST /events/{event}/time-slots`
- `PUT /events/{event}/time-slots/{timeSlot}`
- `DELETE /events/{event}/time-slots/{timeSlot}`
> **Festival context:** `{event}` can be a festival parent or a sub-event.
> Festival-level time slots (operational: build-up, teardown, transitions) are separate
> from sub-event time slots (program-specific).
>
> `GET /events/{event}/time-slots` returns only the specified event's own time slots by
> default. For sub-events, pass `?include_parent=true` to also include the parent festival's
> time slots — each time slot is marked with a `source` field (`sub_event` or `festival`)
> and includes `event_name` for display grouping. This parameter has no effect on festivals
> or flat events.
>
> Shifts on sub-event sections may reference parent festival time slots (e.g. for build-up
> shifts). The `time_slot_id` validation accepts time slots from the sub-event itself or
> its parent festival.
## Shifts
- `GET /events/{event}/sections/{section}/shifts`
- `POST /events/{event}/sections/{section}/shifts`
- `PUT /events/{event}/sections/{section}/shifts/{shift}`
- `DELETE /events/{event}/sections/{section}/shifts/{shift}`
- `POST /events/{event}/sections/{section}/shifts/{shift}/assign`
- `POST /events/{event}/sections/{section}/shifts/{shift}/claim`
> **Festival context:** When managing shifts on a `cross_event` section, the `{event}`
> in the URL must be the parent festival's ID (matching `section.event_id`), not the
> sub-event's ID.
## Persons
- `GET /events/{event}/persons`
- `POST /events/{event}/persons`
- `GET /events/{event}/persons/{person}`
- `PUT /events/{event}/persons/{person}`
- `POST /events/{event}/persons/{person}/approve`
- `DELETE /events/{event}/persons/{person}`
## Identity Matches
- `GET /organisations/{org}/identity-matches` — list pending matches for the organisation (paginated, 25 per page)
- `GET /organisations/{org}/persons/{person}/identity-match` — show pending match for a specific person
- `POST /organisations/{org}/identity-matches/{match}/confirm` — confirm a match (links `person.user_id`)
- `POST /organisations/{org}/identity-matches/{match}/dismiss` — dismiss a match (hidden, person stays unlinked)
- `POST /organisations/{org}/identity-matches/bulk-confirm` — bulk confirm multiple matches
### Detection
Matches are created automatically:
- When a person is created (via `POST /events/{event}/persons`) with an email matching an existing user → pending match created
- When a new user account is created (invitation acceptance) with an email matching unlinked persons → pending matches created
No silent auto-linking. Every identity link requires explicit confirmation.
### Bulk Confirm
`POST /organisations/{org}/identity-matches/bulk-confirm`
Body: `{ "match_ids": ["ulid1", "ulid2", ...] }` (max 100)
Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User already has a person record in this event." }] }`
### PersonResource enrichment
`GET /events/{event}/persons` includes `pending_identity_match` inline when a pending match exists:
```json
{
"pending_identity_match": {
"match_id": "ulid",
"matched_user": { "id": "ulid", "name": "Jan", "email": "jan@example.nl" },
"matched_on": "email",
"confidence": "exact"
}
}
```
## Crowd Lists
- `GET /events/{event}/crowd-lists` — list all crowd lists for event (includes `persons_count`)
- `POST /events/{event}/crowd-lists` — create crowd list
- `PUT /events/{event}/crowd-lists/{list}` — update crowd list
- `DELETE /events/{event}/crowd-lists/{list}` — delete crowd list
- `GET /events/{event}/crowd-lists/{list}/persons` — list persons on a crowd list (paginated, 50/page, includes `crowd_list_pivot`)
- `POST /events/{event}/crowd-lists/{list}/persons` — add person to list
- `DELETE /events/{event}/crowd-lists/{list}/persons/{person}` — remove person from list
### Create/Update Body
```json
{
"crowd_type_id": "01JXYZ...",
"name": "VIP Gastenlijst",
"type": "internal|external",
"recipient_company_id": "01JXYZ... (nullable, for external lists)",
"auto_approve": false,
"max_persons": 50
}
```
### Add Person Body
```json
{
"person_id": "01JXYZ..."
}
```
**Business rules:**
- `max_persons`: when set, adding a person beyond the limit returns 422
- `auto_approve`: when true, adding a person with status `pending` automatically sets their status to `approved`
- Duplicate person on same list returns 422
### CrowdListResource
```json
{
"id": "01JXYZ...",
"event_id": "01JXYZ...",
"crowd_type_id": "01JXYZ...",
"name": "VIP Gastenlijst",
"type": "internal",
"recipient_company_id": null,
"auto_approve": false,
"max_persons": 50,
"is_full": false,
"created_at": "2026-04-10T12:00:00+00:00",
"persons_count": 12
}
```
## Locations
- `GET /events/{event}/locations`
- `POST /events/{event}/locations`
- `PUT /events/{event}/locations/{location}`
- `DELETE /events/{event}/locations/{location}`
## Person Tags (Organisation Settings)
- `GET /organisations/{org}/person-tags` — list active tags (ordered)
- `POST /organisations/{org}/person-tags` — create tag
- `PUT /organisations/{org}/person-tags/{tag}` — update tag
- `DELETE /organisations/{org}/person-tags/{tag}` — deactivate tag (soft: sets `is_active = false`)
- `GET /organisations/{org}/person-tag-categories` — distinct categories for autocomplete
## User Tag Assignments
- `GET /organisations/{org}/users/{user}/tags` — list all tags for user in organisation
- `POST /organisations/{org}/users/{user}/tags` — assign a tag
- `DELETE /organisations/{org}/users/{user}/tags/{tagAssignment}` — remove assignment
- `PUT /organisations/{org}/users/{user}/tags/sync` — sync tags by source
> **Sync endpoint:** Replaces tags of the specified `source` only.
> Body: `{ "tag_ids": ["ulid1", "ulid2"], "source": "self_reported" }`
> Removes `self_reported` tags not in the list, adds new ones, leaves `organiser_assigned` untouched (and vice versa).
### Person list tag filtering
- `GET /events/{event}/persons?tag={person_tag_id}` — filter persons by single tag
- `GET /events/{event}/persons?tags=ulid1,ulid2` — filter persons by multiple tags (AND logic: must have all)
_(Extend this contract per module as endpoints are implemented.)_