afterhours-shift-manager/openapi.yaml
Adam Moussa c611fd5d2b
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
feat(api): add OpenAPI Redocly contract and VPC outputs (DEV-289) (#268)
* feat(infra): export vpc_id and public subnet outputs (DEV-289)

Portal Fargate and meals already attach to this VPC. These outputs are
the HCP existing_vpc_id / existing_public_subnet_ids values.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* feat(api): add OpenAPI 3.1 and Redocly lint in CI (DEV-289)

Same extends: recommended ruleset and @redocly/cli 2.52.1 as
internal-portal. Covers health, roster, and portal /api/shifts.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(api): document 4xx and treat 302 as success in Redocly (DEV-289)

Health and CORS preflight document 400. Recommended only counted 2XX,
so login-style 302s use a shared 2XX-or-3XX rule.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(api): fail Redocly on missing 4xx and 2xx/3xx (DEV-289)

Promote operation-4xx-response and the 2xx-or-3xx success rule to error.
Drop unused 400s on health and CORS OPTIONS. Health documents 403 like the
portal. CORS stays in Flask and is not part of the employee contract.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-09-22 00:34:15 +00:00

771 lines
19 KiB
YAML

openapi: 3.1.0
info:
title: After Hours Shift Manager
version: 0.1.0
description: >
Flask HTTP API on ECS Fargate. Slack `/oncall` stays on POST /slack/events
and is not part of this contract. The internal portal SPA calls /api/shifts
with a Cognito ID token from GET /api/auth/meals-token. Paychex calls
PUT/DELETE /roster with a shared bearer token. Error shape for portal
routes is `{ error: { code, message } }`. Health matches the portal BFF
`{ stage, sha }`. Lint with the same Redocly `extends: recommended` config
as internal-portal and meal-order-manager.
contact:
name: Sea Haven Engineering
license:
name: Proprietary
identifier: LicenseRef-SeaHaven
servers:
- url: /
description: After Hours ALB origin (VITE_SHIFTS_API_BASE)
tags:
- name: Runtime
description: Unauthenticated health
- name: Roster
description: Paychex hire and offboard
- name: Shifts
description: Employee and admin After Hours for the portal SPA
security: []
paths:
/api/health:
get:
operationId: getHealth
tags: [Runtime]
summary: Runtime health
security: []
responses:
"200":
description: Process is up
content:
application/json:
schema:
$ref: "#/components/schemas/Health"
"403":
$ref: "#/components/responses/PortalError"
/roster:
put:
operationId: putRoster
tags: [Roster]
summary: Upsert a roster row
security:
- rosterBearer: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RosterUpsert"
responses:
"200":
description: Row written
content:
application/json:
schema:
$ref: "#/components/schemas/RosterOk"
"400":
$ref: "#/components/responses/RosterError"
"401":
$ref: "#/components/responses/RosterError"
"503":
$ref: "#/components/responses/RosterError"
/roster/{extension}:
delete:
operationId: deleteRoster
tags: [Roster]
summary: Remove a roster row
security:
- rosterBearer: []
parameters:
- $ref: "#/components/parameters/Extension"
responses:
"204":
description: Gone, including when the row was already missing
"400":
$ref: "#/components/responses/RosterError"
"401":
$ref: "#/components/responses/RosterError"
"503":
$ref: "#/components/responses/RosterError"
/api/shifts:
get:
operationId: getShifts
tags: [Shifts]
summary: Week snapshot for the signed-in employee
security:
- portalCognito: []
parameters:
- name: week
in: query
schema:
type: string
enum: [this, next]
default: this
responses:
"200":
description: Linked snapshot or unlinked Google account
content:
application/json:
schema:
$ref: "#/components/schemas/ShiftsSnapshot"
"401":
$ref: "#/components/responses/PortalError"
"503":
$ref: "#/components/responses/PortalError"
/api/shifts/pick:
post:
operationId: pickShift
tags: [Shifts]
summary: Pick up a shift or request late pickup
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ShiftDateBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
"503":
$ref: "#/components/responses/PortalError"
/api/shifts/drop:
post:
operationId: dropShift
tags: [Shifts]
summary: Drop a held shift
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ShiftDateBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
/api/shifts/swap:
post:
operationId: requestSwap
tags: [Shifts]
summary: Request a swap
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SwapBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
/api/shifts/swaps/{date}/{shiftType}/accept:
post:
operationId: acceptSwap
tags: [Shifts]
summary: Accept a pending swap
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/ShiftDate"
- $ref: "#/components/parameters/ShiftType"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"401":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
/api/shifts/swaps/{date}/{shiftType}/decline:
post:
operationId: declineSwap
tags: [Shifts]
summary: Decline a pending swap
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/ShiftDate"
- $ref: "#/components/parameters/ShiftType"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"401":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/override:
post:
operationId: adminOverride
tags: [Shifts]
summary: Assign a shift
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminOverrideBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/open:
post:
operationId: adminOpen
tags: [Shifts]
summary: Mark a shift open
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ShiftDateBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/clear:
post:
operationId: adminClear
tags: [Shifts]
summary: Clear an override
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ShiftDateBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/holidays:
post:
operationId: adminHolidayAdd
tags: [Shifts]
summary: Schedule a holiday day shift
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/HolidayBody"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"400":
$ref: "#/components/responses/PortalError"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/holidays/{date}:
delete:
operationId: adminHolidayRemove
tags: [Shifts]
summary: Remove a holiday
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/ShiftDate"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/approve:
post:
operationId: adminPickupApprove
tags: [Shifts]
summary: Approve a late pickup
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/ShiftDate"
- $ref: "#/components/parameters/ShiftType"
- $ref: "#/components/parameters/Extension"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
"409":
$ref: "#/components/responses/PortalError"
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/deny:
post:
operationId: adminPickupDeny
tags: [Shifts]
summary: Deny a late pickup
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/ShiftDate"
- $ref: "#/components/parameters/ShiftType"
- $ref: "#/components/parameters/Extension"
responses:
"200":
$ref: "#/components/responses/MutationOk"
"401":
$ref: "#/components/responses/PortalError"
"403":
$ref: "#/components/responses/PortalError"
"404":
$ref: "#/components/responses/PortalError"
components:
securitySchemes:
portalCognito:
type: http
scheme: bearer
bearerFormat: JWT
description: Portal Cognito ID token
rosterBearer:
type: http
scheme: bearer
description: Shared Paychex roster token
parameters:
ShiftDate:
name: date
in: path
required: true
schema:
type: string
format: date
ShiftType:
name: shiftType
in: path
required: true
schema:
type: string
enum: [day, night]
Extension:
name: extension
in: path
required: true
schema:
type: string
minLength: 1
responses:
MutationOk:
description: Mutation applied
content:
application/json:
schema:
$ref: "#/components/schemas/MutationResult"
PortalError:
description: Portal JSON error
content:
application/json:
schema:
$ref: "#/components/schemas/PortalErrorEnvelope"
RosterError:
description: Roster string error
content:
application/json:
schema:
$ref: "#/components/schemas/RosterErrorBody"
schemas:
Health:
type: object
additionalProperties: false
required: [stage, sha]
properties:
stage:
type: string
minLength: 1
description: Workspace stage (`dev`, `prod`, or `local`)
sha:
type: string
minLength: 1
description: Git SHA or `unknown` locally
PortalErrorEnvelope:
type: object
additionalProperties: false
required: [error]
properties:
error:
type: object
additionalProperties: false
required: [code, message]
properties:
code:
type: string
minLength: 1
message:
type: string
minLength: 1
RosterErrorBody:
type: object
additionalProperties: false
required: [error]
properties:
error:
type: string
minLength: 1
RosterUpsert:
type: object
additionalProperties: false
required: [name, extension, slack_user_id]
properties:
name:
type: string
minLength: 1
extension:
type: string
minLength: 1
slack_user_id:
type: string
minLength: 1
email:
type: string
format: email
RosterOk:
type: object
additionalProperties: false
required: [ok]
properties:
ok:
type: boolean
const: true
ShiftDateBody:
type: object
additionalProperties: false
required: [date]
properties:
date:
type: string
format: date
shiftType:
type: string
enum: [day, night, holiday]
SwapBody:
type: object
additionalProperties: false
required: [date, targetExtension]
properties:
date:
type: string
format: date
shiftType:
type: string
enum: [day, night]
targetExtension:
type: string
minLength: 1
AdminOverrideBody:
type: object
additionalProperties: false
required: [date, extension]
properties:
date:
type: string
format: date
extension:
type: string
minLength: 1
shiftType:
type: string
enum: [day, night]
HolidayBody:
type: object
additionalProperties: false
required: [date, slots, label]
properties:
date:
type: string
format: date
slots:
type: integer
minimum: 1
label:
type: string
minLength: 1
multiplier:
type: [number, string, "null"]
MutationResult:
type: object
additionalProperties: false
required: [ok]
properties:
ok:
type: boolean
message:
type: string
latePickup:
type: boolean
repointed:
type: boolean
RosterPerson:
type: object
additionalProperties: false
required: [extension, name, email]
properties:
extension:
type: string
name:
type: string
email:
type: string
slackUserId:
type: string
ShiftAssignee:
type: object
additionalProperties: false
required: [extension, name]
properties:
extension:
type: string
name:
type: string
ShiftSlot:
type: object
additionalProperties: false
required:
- kind
- shiftType
- label
- slots
- openSlots
- multiplier
- assignees
- mine
- canPick
- canDrop
- latePickup
properties:
kind:
type: string
enum: [holiday, override, available, weekly]
shiftType:
type: string
enum: [day, night]
label:
type: string
slots:
type: integer
openSlots:
type: integer
multiplier:
type: number
assignees:
type: array
items:
$ref: "#/components/schemas/ShiftAssignee"
mine:
type: boolean
canPick:
type: boolean
canDrop:
type: boolean
latePickup:
type: boolean
ShiftDay:
type: object
additionalProperties: false
required: [date, dayName, slots]
properties:
date:
type: string
format: date
dayName:
type: string
slots:
type: array
items:
$ref: "#/components/schemas/ShiftSlot"
PendingSwap:
type: object
additionalProperties: false
required:
- date
- shiftType
- requesterExt
- requesterName
- targetExt
- targetName
- incoming
properties:
date:
type: string
shiftType:
type: string
enum: [day, night]
requesterExt:
type: string
requesterName:
type: string
targetExt:
type: string
targetName:
type: string
incoming:
type: boolean
PendingPickup:
type: object
additionalProperties: false
required: [date, shiftType, requesterExt, requesterName, isHoliday]
properties:
date:
type: string
shiftType:
type: string
enum: [day, night]
requesterExt:
type: string
requesterName:
type: string
isHoliday:
type: boolean
HolidaySummary:
type: object
additionalProperties: false
required: [date, label, slots, multiplier]
properties:
date:
type: string
label:
type: string
slots:
type: integer
multiplier:
type: number
ShiftsSnapshot:
type: object
additionalProperties: false
required: [linked, isAdmin]
properties:
linked:
type: boolean
email:
type: string
week:
type: string
enum: [this, next]
weekStart:
type: string
format: date
me:
$ref: "#/components/schemas/RosterPerson"
isAdmin:
type: boolean
days:
type: array
items:
$ref: "#/components/schemas/ShiftDay"
pendingSwaps:
type: array
items:
$ref: "#/components/schemas/PendingSwap"
pendingPickups:
type: array
items:
$ref: "#/components/schemas/PendingPickup"
upcomingHolidays:
type: array
items:
$ref: "#/components/schemas/HolidaySummary"
roster:
type: array
items:
$ref: "#/components/schemas/RosterPerson"