mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 01:53:12 +00:00
* 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>
665 lines
16 KiB
YAML
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
|