meal-order-manager/openapi.yaml
Adam Moussa d7ad49d00f
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) (#206)
* feat(infra): export attached VPC ids and lock prod to afterhours (DEV-289)

Prod must keep existing_vpc_id pointed at the afterhours VPC. Outputs
expose the resolved vpc_id and public subnet IDs.

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. Documents current { error: string } JSON errors.

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

* fix(api): document 4xx and reject invalid form-status weeks (DEV-289)

Health, form-status, and roster document 400. form-status now maps
current and returns 400 for a week that is not current or YYYY-WNN.
Redocly treats 302 as a success response, matching the portal.

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

* style(test): format VPC contract assertions for ruff (DEV-289)

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.
Replace unused health and roster 400s with 403, matching portal health.
Form-status keeps its real 400 for invalid week.

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

* fix(api): split week params and allow live menu nulls (DEV-289)

Menu and form-status take current or YYYY-WNN. Orders take YYYY-WNN or a
calendar date and reject current. Menu payloads may emit null menu_url,
calories, protein, and image_url.

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

* fix(infra): fail prod apply without the afterhours VPC (DEV-289)

Prod never creates the 10.60 fallback VPC. A terraform_data precondition
fails plan and apply when existing_vpc_id is empty, instead of a check
block that only warns.

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:33:48 +00:00

665 lines
16 KiB
YAML

openapi: 3.1.0
info:
title: Meal Order Manager
version: 0.1.0
description: >
Flask HTTP API on ECS Fargate behind orders.seahaven.com. The internal
portal SPA calls menu, submit, and admin with a Cognito ID token from
GET /api/auth/meals-token. Weekly-menu GitHub Actions uses HMAC publish
routes that CloudFront does not expose. JSON errors are currently
`{ error: string }`. Health matches the portal BFF `{ stage, sha }`.
Lint with the same Redocly `extends: recommended` config as
internal-portal and afterhours-shift-manager.
contact:
name: Sea Haven Engineering
license:
name: Proprietary
identifier: LicenseRef-SeaHaven
servers:
- url: /
description: orders.seahaven.com CloudFront / local Flask :5050
tags:
- name: Runtime
description: Unauthenticated health
- name: Menu
description: Public weekly menu and form status
- name: Orders
description: Employee submit and own-order lookup
- name: Admin
description: Admin order edit and summary PDF
- name: Publish
description: Weekly-menu HMAC publish (not on CloudFront)
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/StringError"
/api/menu/{week}:
get:
operationId: getMenu
tags: [Menu]
summary: Published menu for a week
security: []
parameters:
- $ref: "#/components/parameters/MenuWeekPath"
responses:
"200":
description: Menu payload
content:
application/json:
schema:
$ref: "#/components/schemas/MenuPayload"
"400":
$ref: "#/components/responses/StringError"
"404":
$ref: "#/components/responses/StringError"
/api/form-status/{week}:
get:
operationId: getFormStatus
tags: [Menu]
summary: Open or closed for a week
security: []
parameters:
- $ref: "#/components/parameters/MenuWeekPath"
responses:
"200":
description: Form status
content:
application/json:
schema:
$ref: "#/components/schemas/FormStatus"
"400":
$ref: "#/components/responses/StringError"
/api/submit-order:
post:
operationId: submitOrder
tags: [Orders]
summary: Place or replace this week's order
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SubmitBody"
responses:
"200":
description: Order saved
content:
application/json:
schema:
$ref: "#/components/schemas/SubmitResult"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
"404":
$ref: "#/components/responses/StringError"
"410":
$ref: "#/components/responses/StringError"
"429":
$ref: "#/components/responses/StringError"
"503":
$ref: "#/components/responses/StringError"
/api/orders/{week}:
get:
operationId: getMyOrder
tags: [Orders]
summary: Caller's order only
security:
- portalCognito: []
parameters:
- $ref: "#/components/parameters/OrderWeekPath"
responses:
"200":
description: Empty list when the caller has no order
content:
application/json:
schema:
$ref: "#/components/schemas/MyOrders"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
"503":
$ref: "#/components/responses/StringError"
/api/admin/orders:
get:
operationId: adminListOrders
tags: [Admin]
summary: List weeks or one week's orders
security:
- portalCognito: []
parameters:
- name: week
in: query
schema:
type: string
responses:
"200":
description: Weeks list or week detail
content:
application/json:
schema:
$ref: "#/components/schemas/AdminOrdersResponse"
"403":
$ref: "#/components/responses/StringError"
put:
operationId: adminUpdateOrder
tags: [Admin]
summary: Recalculate and replace an order
security:
- portalCognito: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUpdateBody"
responses:
"200":
description: Updated
content:
application/json:
schema:
$ref: "#/components/schemas/AdminMutation"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
"404":
$ref: "#/components/responses/StringError"
"503":
$ref: "#/components/responses/StringError"
delete:
operationId: adminDeleteOrder
tags: [Admin]
summary: Delete an order
security:
- portalCognito: []
parameters:
- name: week
in: query
required: true
schema:
type: string
- name: email
in: query
required: true
schema:
type: string
responses:
"200":
description: Deleted
content:
application/json:
schema:
$ref: "#/components/schemas/AdminMutation"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
"404":
$ref: "#/components/responses/StringError"
/api/admin/summary-pdf:
get:
operationId: adminSummaryPdf
tags: [Admin]
summary: Presigned summary PDF URL
security:
- portalCognito: []
parameters:
- name: week
in: query
required: true
schema:
type: string
responses:
"200":
description: Short-lived URL
content:
application/json:
schema:
$ref: "#/components/schemas/SummaryPdf"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
"404":
$ref: "#/components/responses/StringError"
"500":
$ref: "#/components/responses/StringError"
/api/roster:
get:
operationId: getRoster
tags: [Orders]
summary: Name and email list used by the static form
security: []
responses:
"200":
description: Roster
content:
application/json:
schema:
$ref: "#/components/schemas/FormRoster"
"403":
$ref: "#/components/responses/StringError"
/api/publish/settings:
get:
operationId: publishSettings
tags: [Publish]
summary: Bulk discount and subsidy for scrape
security:
- publishKey: []
responses:
"200":
description: Pricing fields only
content:
application/json:
schema:
$ref: "#/components/schemas/PublishSettings"
"403":
$ref: "#/components/responses/StringError"
/api/publish/menu:
post:
operationId: publishMenu
tags: [Publish]
summary: Write this week's scraped menu
security:
- publishKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PublishMenuBody"
responses:
"200":
description: Published
content:
application/json:
schema:
$ref: "#/components/schemas/PublishMenuResult"
"400":
$ref: "#/components/responses/StringError"
"403":
$ref: "#/components/responses/StringError"
components:
securitySchemes:
portalCognito:
type: http
scheme: bearer
bearerFormat: JWT
description: Portal Cognito ID token. GIS google_id_token is also accepted on submit until cutover.
publishKey:
type: apiKey
in: header
name: X-Meals-Publish-Key
parameters:
MenuWeekPath:
name: week
in: path
required: true
description: "`current` or `YYYY-WNN`. Other values are 400."
schema:
type: string
minLength: 1
OrderWeekPath:
name: week
in: path
required: true
description: "`YYYY-WNN` or `YYYY-MM-DD`. `current` is 400."
schema:
type: string
minLength: 1
responses:
StringError:
description: Current meals JSON error
content:
application/json:
schema:
$ref: "#/components/schemas/StringErrorBody"
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
StringErrorBody:
type: object
additionalProperties: false
required: [error]
properties:
error:
type: string
minLength: 1
Meal:
type: object
additionalProperties: true
required: [name, price]
properties:
name:
type: string
price:
type: number
calories:
type: [string, number, "null"]
protein:
type: [string, number, "null"]
description:
type: [string, "null"]
dietary_tags:
type: [array, "null"]
items:
type: string
image_url:
type: [string, "null"]
is_new:
type: boolean
MenuPayload:
type: object
additionalProperties: false
required:
- week
- form_status
- meal_count
- meals
- bulk_discount_percent
- company_subsidy_percent
properties:
week:
type: string
form_status:
type: string
scraped_at:
type: [string, "null"]
menu_url:
type: [string, "null"]
meal_count:
type: integer
meals:
type: array
items:
$ref: "#/components/schemas/Meal"
bulk_discount_percent:
type: number
company_subsidy_percent:
type: number
order_deadline:
type: string
FormStatus:
type: object
additionalProperties: false
required: [week, status]
properties:
week:
type: string
status:
type: string
reopen_at:
type: integer
SubmitItem:
type: object
additionalProperties: true
required: [name, quantity]
properties:
name:
type: string
quantity:
type: number
retail_price:
type: number
SubmitBody:
type: object
additionalProperties: true
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/SubmitItem"
google_id_token:
type: string
SubmitResult:
type: object
additionalProperties: false
required: [status]
properties:
status:
type: string
message:
type: string
total:
type: number
MyOrders:
type: object
additionalProperties: false
required: [week, orders]
properties:
week:
type: string
orders:
type: array
items:
type: object
additionalProperties: false
required: [employee_email]
properties:
employee_email:
type: string
AdminWeek:
type: object
additionalProperties: true
required: [week]
properties:
week:
type: string
form_status:
type: string
meal_count:
type: integer
order_count:
type: integer
AdminOrderItem:
type: object
additionalProperties: false
required: [name, quantity, retail_price, price, subtotal]
properties:
name:
type: string
quantity:
type: integer
retail_price:
type: number
price:
type: number
subtotal:
type: number
AdminOrder:
type: object
additionalProperties: false
required: [employee_name, employee_email, items, total, submitted_at]
properties:
employee_name:
type: string
employee_email:
type: string
items:
type: array
items:
$ref: "#/components/schemas/AdminOrderItem"
total:
type: number
submitted_at:
type: string
AdminWeeks:
type: object
additionalProperties: false
required: [weeks]
properties:
weeks:
type: array
items:
$ref: "#/components/schemas/AdminWeek"
AdminWeekOrders:
type: object
additionalProperties: false
required: [week, orders, total_employees, grand_total]
properties:
week:
type: string
orders:
type: array
items:
$ref: "#/components/schemas/AdminOrder"
total_employees:
type: integer
grand_total:
type: number
AdminOrdersResponse:
oneOf:
- $ref: "#/components/schemas/AdminWeeks"
- $ref: "#/components/schemas/AdminWeekOrders"
AdminUpdateBody:
type: object
additionalProperties: false
required: [week, email, items]
properties:
week:
type: string
email:
type: string
items:
type: array
items:
$ref: "#/components/schemas/SubmitItem"
AdminMutation:
type: object
additionalProperties: false
required: [status, week]
properties:
status:
type: string
week:
type: string
email:
type: string
total:
type: number
SummaryPdf:
type: object
additionalProperties: false
required: [week, url]
properties:
week:
type: string
url:
type: string
format: uri
FormRoster:
type: object
additionalProperties: false
required: [employees]
properties:
employees:
type: array
items:
type: object
additionalProperties: false
required: [name, email]
properties:
name:
type: string
email:
type: string
PublishSettings:
type: object
additionalProperties: false
required: [bulk_discount_percent, company_subsidy_percent]
properties:
bulk_discount_percent:
type: number
company_subsidy_percent:
type: number
PublishMenuBody:
type: object
additionalProperties: true
required: [meals]
properties:
meals:
type: array
minItems: 1
items:
$ref: "#/components/schemas/Meal"
scraped_at:
type: string
menu_url:
type: string
PublishMenuResult:
type: object
additionalProperties: false
required: [status, week, meal_count]
properties:
status:
type: string
week:
type: string
meal_count:
type: integer