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