mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 02:13:12 +00:00
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>
This commit is contained in:
parent
df3f0c037d
commit
c611fd5d2b
10 changed files with 911 additions and 6 deletions
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
|
|
@ -62,9 +62,29 @@ jobs:
|
|||
- name: Terraform validate
|
||||
run: terraform validate
|
||||
|
||||
openapi:
|
||||
name: OpenAPI
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24.19.0"
|
||||
cache: npm
|
||||
|
||||
- name: Install JavaScript tooling
|
||||
run: npm ci
|
||||
|
||||
- name: Lint OpenAPI
|
||||
run: npm run openapi:lint
|
||||
|
||||
ci:
|
||||
name: ci / ci
|
||||
needs: [pytest, terraform]
|
||||
needs: [pytest, terraform, openapi]
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
|
@ -73,6 +93,7 @@ jobs:
|
|||
env:
|
||||
PYTEST_RESULT: ${{ needs.pytest.result }}
|
||||
TERRAFORM_RESULT: ${{ needs.terraform.result }}
|
||||
OPENAPI_RESULT: ${{ needs.openapi.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fail=0
|
||||
|
|
@ -91,4 +112,5 @@ jobs:
|
|||
}
|
||||
check pytest "${PYTEST_RESULT}"
|
||||
check terraform "${TERRAFORM_RESULT}"
|
||||
check openapi "${OPENAPI_RESULT}"
|
||||
exit "${fail}"
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.aws-sam/
|
||||
|
|
|
|||
32
.redocly.yaml
Normal file
32
.redocly.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Same Redocly recommended ruleset as internal-portal (DEV-223 / DEV-289).
|
||||
# Recommended operation-2xx-response does not count 302. Login-style redirects
|
||||
# succeed with 302, so that rule is replaced by operation-2xx-or-3xx-response
|
||||
# at error. operation-4xx-response is promoted to error so missing 4xx fails CI.
|
||||
extends:
|
||||
- recommended
|
||||
|
||||
rules:
|
||||
operation-2xx-response: off
|
||||
operation-4xx-response: error
|
||||
rule/operation-2xx-or-3xx-response:
|
||||
subject:
|
||||
type: Responses
|
||||
message: Operation must define a 2XX or 3XX response.
|
||||
severity: error
|
||||
assertions:
|
||||
requireAny:
|
||||
- "200"
|
||||
- "201"
|
||||
- "202"
|
||||
- "204"
|
||||
- "301"
|
||||
- "302"
|
||||
- "303"
|
||||
- "307"
|
||||
- "308"
|
||||
- "2XX"
|
||||
- "3XX"
|
||||
|
||||
apis:
|
||||
afterhours@v1:
|
||||
root: openapi.yaml
|
||||
|
|
@ -58,7 +58,9 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
|
|||
|
||||
## Architecture
|
||||
|
||||
- **Runtime**: Python 3.12 on ECS Fargate (arm64). Cutover completed 2026-09-21. seahaven-prod `011934824531`, seahaven-dev `710827005802`
|
||||
- **Runtime**: Python 3.12 on ECS Fargate (arm64) plus dual-run Lambdas until cutover. seahaven-prod `011934824531`, seahaven-dev `710827005802`
|
||||
- **VPC**: This stack owns `10.70.0.0/16`. Meals and portal Fargate attach with HCP `existing_vpc_id` / `existing_public_subnet_ids` from outputs `vpc_id` and `public_subnet_ids`.
|
||||
- **HTTP contract**: `openapi.yaml`, linted in CI with `npm run openapi:lint` (Redocly `extends: recommended`, same as internal-portal and meal-order-manager).
|
||||
- **Data**: DynamoDB single-table (`afterhours-shifts`)
|
||||
- **IaC**: HCP Terraform workspaces tagged `app:afterhours-shift-manager` (`afterhours-shift-manager-dev` / `-prod`) plus GitHub Actions `deploy-api.yaml` (image). Terraform does not package `src/`.
|
||||
- **Slack**: Slack Bolt on `POST /slack/events`
|
||||
|
|
@ -301,7 +303,8 @@ pytest
|
|||
|
||||
Each Lambda has its own `app.py`, so the per-package `conftest.py` loads each one
|
||||
under a unique module name (importlib mode) to avoid collisions. CI runs the same
|
||||
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` / `validate`.
|
||||
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` /
|
||||
`validate` and `npm run openapi:lint`.
|
||||
|
||||
Local Fargate process (needs the same DynamoDB table and Secrets Manager names
|
||||
the Lambdas use, plus `JOBS_QUEUE_URL` to consume jobs):
|
||||
|
|
|
|||
771
openapi.yaml
Normal file
771
openapi.yaml
Normal file
|
|
@ -0,0 +1,771 @@
|
|||
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"
|
||||
30
package-lock.json
generated
Normal file
30
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.52.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redocly/cli": {
|
||||
"version": "2.52.1",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.52.1.tgz",
|
||||
"integrity": "sha512-gwfx1WelVDHU/cw+GkpLV9+xksOI3obJGgL1nUVrAFUyXNDxM+aifXDUZk7BfbOIpF69Sea2ant0cOqbQfg/KQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"openapi": "bin/cli.js",
|
||||
"redocly": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0 || >=20.19.0 <21.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"openapi:lint": "redocly lint --config .redocly.yaml openapi.yaml"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.52.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,8 @@ locals {
|
|||
artifacts_bucket_name = "afterhours-shift-manager-artifacts-${local.account_id}"
|
||||
ssm_prefix = "/afterhours-shift-manager"
|
||||
|
||||
# Prod has no default VPC. 10.70 is unused in 011934824531
|
||||
# (10.0 proposal-system, 10.20 payments-dashboard, 10.40 syslog,
|
||||
# 10.60 meal-order-manager, 10.80 apm-wo).
|
||||
# Prod has no default VPC. This stack owns 10.70. Meals attaches with
|
||||
# existing_vpc_id. Portal Fargate (PLAT-217) should too. Do not mint 10.62.
|
||||
vpc_cidr = "10.70.0.0/16"
|
||||
public_subnet_cidrs = ["10.70.0.0/24", "10.70.1.0/24"]
|
||||
table_name = "afterhours-shifts"
|
||||
|
|
|
|||
|
|
@ -57,3 +57,13 @@ output "hcptf_plan_role_arn" {
|
|||
description = "HCP plan role ARN. Set TFC_AWS_PLAN_ROLE_ARN after the bootstrap window."
|
||||
value = aws_iam_role.hcptf_plan.arn
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "VPC that meals already attaches to. Portal Fargate prod sets existing_vpc_id to this value."
|
||||
value = aws_vpc.this.id
|
||||
}
|
||||
|
||||
output "public_subnet_ids" {
|
||||
description = "Public subnet IDs for ALB and Fargate. Portal HCP existing_public_subnet_ids."
|
||||
value = aws_subnet.public[*].id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,29 @@ def test_ci_runs_pytest_and_terraform_validate():
|
|||
assert "terraform fmt -check" in CI
|
||||
assert "terraform init -backend=false" in CI
|
||||
assert "terraform validate" in CI
|
||||
assert "openapi:lint" in CI
|
||||
assert "npm ci" in CI
|
||||
|
||||
|
||||
def test_openapi_uses_redocly_recommended():
|
||||
redocly = (ROOT / ".redocly.yaml").read_text()
|
||||
package = (ROOT / "package.json").read_text()
|
||||
spec = (ROOT / "openapi.yaml").read_text()
|
||||
assert "extends:" in redocly
|
||||
assert "- recommended" in redocly
|
||||
assert "operation-2xx-response: off" in redocly
|
||||
assert "operation-4xx-response: error" in redocly
|
||||
assert "rule/operation-2xx-or-3xx-response" in redocly
|
||||
assert "severity: error" in redocly
|
||||
assert "optionsShifts" not in spec
|
||||
health = spec.split("/api/health:", 1)[1].split("\n /", 1)[0]
|
||||
assert '"403":' in health
|
||||
assert '"400":' not in health
|
||||
assert 'root: openapi.yaml' in redocly
|
||||
assert '"openapi:lint"' in package
|
||||
assert '"@redocly/cli": "2.52.1"' in package
|
||||
assert "openapi: 3.1.0" in spec
|
||||
assert "required: [stage, sha]" in spec
|
||||
|
||||
|
||||
def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
||||
|
|
@ -184,6 +207,9 @@ def test_origins_use_fargate_url():
|
|||
assert "aws_apigatewayv2_api.http" not in outputs
|
||||
assert '${local.api_url}/slack/events' in outputs
|
||||
assert "value = local.api_url" in outputs
|
||||
assert "output \"vpc_id\"" in outputs
|
||||
assert "output \"public_subnet_ids\"" in outputs
|
||||
assert "aws_vpc.this.id" in outputs
|
||||
|
||||
|
||||
def test_alb_alarms_remain():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue