afterhours-shift-manager/openapi.yaml
Adam Moussa edfa34bfbf
Some checks failed
Deploy API / Deploy API to dev (push) Has been cancelled
Deploy API / Deploy API to prod (push) Has been cancelled
feat(portal): store an optional note on swap requests (IP-132) (#283)
* feat(portal): store an optional note on swap requests (IP-132)

* fix(portal): address review feedback

* fix(portal): address review feedback
2026-09-25 20:42:19 +00:00

777 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
note:
type: string
description: Optional. Stored after trim, and the trimmed value must be 500 characters or fewer.
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
note:
type: string
maxLength: 500
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"