mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 03:03:12 +00:00
Some checks failed
Deploy / deploy (push) Has been cancelled
* Add discount settings and two-tier pricing to order aggregation Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item. Aggregation now tracks bulk_price and employee_price separately, with grand_total (company cost) and employee_total (payroll deductions). * Add Google OAuth, server-side discounts, and Slack order confirmations Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint, calculates two-tier discount pricing server-side, and async-invokes the Slack notifier for per-employee order confirmation DMs. Deadlines updated to Thursday 11:59pm across all Slack messages. * Update SAM template for Google auth, Slack invocation, and deadline change Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order function with lambda:InvokeFunction policy. Move close-form schedule to Thursday 11:59pm EST/EDT. * Update order form UI and CI workflow for new features Form now shows discount pricing, responsive grid layout, Google Sign-In overlay, and closed-orders page with countdown timer. CI workflow fetches discount settings from DynamoDB and Google Client ID from SSM. * Add SSM GetParameter permission to submit order Lambda Required for reading the Google Client ID from Parameter Store at /meal-order-manager/google-client-id. * Harden auth, pricing, and reliability in order handlers Enforce Google auth when configured (reject missing tokens with 403), return 503 on token verification outages, switch to Decimal with ROUND_HALF_UP for financial precision, clamp discount bounds 0-100, use email-based slugs, add 5-min cache TTL with time.monotonic(), wrap Slack invocation in try/except, add reopen_at timestamp to closed form status, add reminder dedup guards for dual EST/EDT crons, escape Slack mrkdwn special characters, and handle empty employee names. * Fix XSS risks and add closed-form UX to order page Add escapeHtml() for all scraped content in innerHTML, fix script injection via </script> in JSON, fix JWT base64url decoding, match backend two-step rounding in JS employeePrice(), disable qty buttons and submit when form is closed, add server-driven countdown from reopen_at, add duplicate order warning via localStorage, add back button after submission, embed favicon, use :g format for fractional discounts, and exclude dead loadRoster code when Google auth enabled. * Document CORS, cron idempotency, and SSM config in template Add comments explaining CORS dev server strategy, dual EST/EDT cron idempotency, and manual SSM parameter creation for Google Client ID. * Add unit tests for submit, notify, and aggregate handlers 50 tests covering pricing pipeline (Decimal rounding, clamping, totals), Google auth (enforcement, bypass prevention, audience/domain validation, 503 on outage), email slug generation, form status with reopen_at, input validation, Slack failure resilience, reminder dedup guards, order confirmation DMs, aggregated summaries, CSV generation, and mrkdwn escaping. * Use full email as order slug for defense-in-depth Replace email-prefix slug with full lowercase email to eliminate any possibility of cross-domain collisions, per senior review sign-off. * Remove unused imports flagged by ruff * Apply ruff formatting * Fix PR review findings: auth, rounding, and close-form guard - Remove dead elif branch in submit_order auth (always returned 403) - Catch HTTPError before URLError so expired tokens return 403 not 503 - Wrap SSM get_parameter in try/except for fresh deployments - Add wall-clock guard to close_form handler (Friday >= 11 PM ET) - Add epsilon nudge to JS employeePrice for IEEE 754 boundary match - Switch Flask dev server from round() to Decimal ROUND_HALF_UP - Add tests for HTTPError handling and close_form guard (6 new tests) * Fix close-form weekday guard and SSM auth fail-open - Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the crons fire at Thursday 11:59 PM ET, when weekday() is 3 - SSM fail-closed: separate _google_auth_configured() (checks env var) from _get_google_client_id() (fetches value). If auth is configured but the SSM fetch fails, return 503 instead of silently falling back to manual auth - Update close_form tests to use Thursday dates - Add test_ssm_failure_fails_closed * Harden Flask dev server auth and escaping - Add hosted domain check to _verify_google_token (mirror Lambda) - Gate auth on config (client_id presence), not request body — prevents bypass by omitting google_id_token when auth is configured - Add discount percentage clamping to match Lambda handler - Add </script> escaping to google_client_id_json * fix: Email order filenames, SSM param TTL, DST-safe reopen_at - Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo) - shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes - form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta) - Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to submit_order handler * fix(server): retry SSM for Google client id after TTL on failure Transient SSM errors no longer cache empty client id for the process lifetime; matches Lambda handler refresh behavior (300s TTL). Co-authored-by: Cursor <cursoragent@cursor.com> * style(server): ruff-format Google client id cache helper Co-authored-by: Cursor <cursoragent@cursor.com> * fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron EventBridge can deliver past midnight ET; widen the wall-clock guard so a delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * fix(submit-order): bill from Dynamo menu retail, not client JSON Load authoritative meal prices from get_menu(week); reject unknown meal names and return 503 when the menu has no priced meals. Use meal_name in the pricing loop to avoid shadowing the employee name. Adds regression tests for tampering, unknown meals, and empty menu meals. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * fix: use single braces in loadRoster JS nested string Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Eastern fallback countdown * Fix pricing validation and JWT display decoding * Fix optional Google auth detection * Format app.py line length for ruff compliance * Fix auth config check and URL escaping in form - _google_auth_configured() now checks env var presence (intent), not the fetched SSM value — prevents silent auth bypass if SSM param is deleted - Add </script> escaping to URL values in generate_form.py for consistency with other injected values --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
519 lines
17 KiB
YAML
519 lines
17 KiB
YAML
AWSTemplateFormatVersion: '2010-09-09'
|
|
Transform: AWS::Serverless-2016-10-31
|
|
Description: >
|
|
meal-order-manager — automated weekly meal ordering from Redefine Meals
|
|
with employee order collection, Slack notifications, and payroll deduction reports.
|
|
|
|
Parameters:
|
|
CustomDomain:
|
|
Type: String
|
|
Default: orders.seahaven.com
|
|
Description: Custom domain for the order form (requires ACM cert)
|
|
CertificateArn:
|
|
Type: String
|
|
Default: ''
|
|
Description: ACM certificate ARN for the custom domain (us-east-1)
|
|
PayrollEmail:
|
|
Type: String
|
|
Default: payroll@seahavenind.com
|
|
Description: Email address for payroll deduction reports
|
|
SenderEmail:
|
|
Type: String
|
|
Default: adam@seahavenind.com
|
|
Description: SES verified sender email for payroll reports
|
|
Conditions:
|
|
HasCustomDomain: !Not [!Equals [!Ref CertificateArn, '']]
|
|
|
|
Globals:
|
|
Function:
|
|
Runtime: python3.12
|
|
Architectures:
|
|
- arm64
|
|
Timeout: 30
|
|
MemorySize: 256
|
|
Environment:
|
|
Variables:
|
|
TABLE_NAME: !Ref OrdersTable
|
|
REPORTS_BUCKET: !Ref ReportsBucket
|
|
SLACK_BOT_TOKEN_SECRET: meal-order-manager/slack-bot-token
|
|
SLACK_CHANNEL_PARAM: /meal-order-manager/slack-channel-id
|
|
FORM_URL: !If
|
|
- HasCustomDomain
|
|
- !Sub 'https://${CustomDomain}'
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
|
|
Resources:
|
|
|
|
# ─── Shared Layer ───────────────────────────────────────────────
|
|
|
|
SharedLayer:
|
|
Type: AWS::Serverless::LayerVersion
|
|
Properties:
|
|
LayerName: meal-order-manager-shared
|
|
ContentUri: src/shared/
|
|
CompatibleRuntimes:
|
|
- python3.12
|
|
CompatibleArchitectures:
|
|
- arm64
|
|
Metadata:
|
|
BuildMethod: python3.12
|
|
BuildArchitecture: arm64
|
|
|
|
# ─── DynamoDB ───────────────────────────────────────────────────
|
|
|
|
OrdersTable:
|
|
Type: AWS::DynamoDB::Table
|
|
Properties:
|
|
TableName: meal-order-manager-orders
|
|
BillingMode: PAY_PER_REQUEST
|
|
AttributeDefinitions:
|
|
- AttributeName: PK
|
|
AttributeType: S
|
|
- AttributeName: SK
|
|
AttributeType: S
|
|
KeySchema:
|
|
- AttributeName: PK
|
|
KeyType: HASH
|
|
- AttributeName: SK
|
|
KeyType: RANGE
|
|
TimeToLiveSpecification:
|
|
AttributeName: ttl
|
|
Enabled: true
|
|
|
|
# ─── S3 Buckets ────────────────────────────────────────────────
|
|
|
|
FormBucket:
|
|
Type: AWS::S3::Bucket
|
|
Properties:
|
|
BucketName: !Sub 'meal-order-manager-form-${AWS::AccountId}'
|
|
PublicAccessBlockConfiguration:
|
|
BlockPublicAcls: true
|
|
BlockPublicPolicy: true
|
|
IgnorePublicAcls: true
|
|
RestrictPublicBuckets: true
|
|
LifecycleConfiguration:
|
|
Rules:
|
|
- Id: delete-old-archives
|
|
Prefix: archive/
|
|
Status: Enabled
|
|
ExpirationInDays: 90
|
|
Tags:
|
|
- Key: Purpose
|
|
Value: meal-order-form-hosting
|
|
- Key: ManagedBy
|
|
Value: meal-order-manager
|
|
|
|
FormBucketPolicy:
|
|
Type: AWS::S3::BucketPolicy
|
|
Properties:
|
|
Bucket: !Ref FormBucket
|
|
PolicyDocument:
|
|
Version: '2012-10-17'
|
|
Statement:
|
|
- Sid: AllowCloudFrontOAC
|
|
Effect: Allow
|
|
Principal:
|
|
Service: cloudfront.amazonaws.com
|
|
Action: s3:GetObject
|
|
Resource: !Sub '${FormBucket.Arn}/*'
|
|
Condition:
|
|
StringEquals:
|
|
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${FormDistribution}'
|
|
|
|
ReportsBucket:
|
|
Type: AWS::S3::Bucket
|
|
Properties:
|
|
BucketName: !Sub 'meal-order-manager-reports-${AWS::AccountId}'
|
|
PublicAccessBlockConfiguration:
|
|
BlockPublicAcls: true
|
|
BlockPublicPolicy: true
|
|
IgnorePublicAcls: true
|
|
RestrictPublicBuckets: true
|
|
LifecycleConfiguration:
|
|
Rules:
|
|
- Id: archive-old-reports
|
|
Status: Enabled
|
|
Transitions:
|
|
- StorageClass: GLACIER_IR
|
|
TransitionInDays: 90
|
|
Tags:
|
|
- Key: Purpose
|
|
Value: meal-order-reports
|
|
- Key: ManagedBy
|
|
Value: meal-order-manager
|
|
|
|
# ─── CloudFront ────────────────────────────────────────────────
|
|
|
|
FormOAC:
|
|
Type: AWS::CloudFront::OriginAccessControl
|
|
Properties:
|
|
OriginAccessControlConfig:
|
|
Name: meal-order-manager-oac
|
|
OriginAccessControlOriginType: s3
|
|
SigningBehavior: always
|
|
SigningProtocol: sigv4
|
|
|
|
FormDistribution:
|
|
Type: AWS::CloudFront::Distribution
|
|
Properties:
|
|
DistributionConfig:
|
|
Enabled: true
|
|
DefaultRootObject: index.html
|
|
Comment: meal-order-manager form hosting
|
|
PriceClass: PriceClass_100
|
|
HttpVersion: http2and3
|
|
Aliases: !If
|
|
- HasCustomDomain
|
|
- [!Ref CustomDomain]
|
|
- !Ref AWS::NoValue
|
|
ViewerCertificate: !If
|
|
- HasCustomDomain
|
|
- AcmCertificateArn: !Ref CertificateArn
|
|
SslSupportMethod: sni-only
|
|
MinimumProtocolVersion: TLSv1.2_2021
|
|
- CloudFrontDefaultCertificate: true
|
|
Origins:
|
|
- Id: S3FormOrigin
|
|
DomainName: !GetAtt FormBucket.RegionalDomainName
|
|
OriginAccessControlId: !Ref FormOAC
|
|
S3OriginConfig:
|
|
OriginAccessIdentity: ''
|
|
DefaultCacheBehavior:
|
|
TargetOriginId: S3FormOrigin
|
|
ViewerProtocolPolicy: redirect-to-https
|
|
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
|
|
Compress: true
|
|
AllowedMethods:
|
|
- GET
|
|
- HEAD
|
|
CachedMethods:
|
|
- GET
|
|
- HEAD
|
|
CustomErrorResponses:
|
|
- ErrorCode: 403
|
|
ResponseCode: 200
|
|
ResponsePagePath: /index.html
|
|
|
|
# ─── API Gateway ───────────────────────────────────────────────
|
|
|
|
OrderApi:
|
|
Type: AWS::Serverless::HttpApi
|
|
Properties:
|
|
StageName: $default
|
|
# CORS only allows the production domain. For local development, use the
|
|
# Flask dev server (app.py) which proxies API requests and doesn't enforce CORS.
|
|
CorsConfiguration:
|
|
AllowOrigins:
|
|
- !If
|
|
- HasCustomDomain
|
|
- !Sub 'https://${CustomDomain}'
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
AllowMethods:
|
|
- GET
|
|
- POST
|
|
- OPTIONS
|
|
AllowHeaders:
|
|
- Content-Type
|
|
- x-api-key
|
|
MaxAge: 3600
|
|
|
|
# ─── Lambda Functions ──────────────────────────────────────────
|
|
|
|
SubmitOrderFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-submit-order
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/submit_order/
|
|
MemorySize: 128
|
|
Timeout: 10
|
|
Environment:
|
|
Variables:
|
|
FORM_API_KEY_SECRET: meal-order-manager/form-api-key
|
|
SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn
|
|
GOOGLE_CLIENT_ID_PARAM: /meal-order-manager/google-client-id
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: secretsmanager:GetSecretValue
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
- Effect: Allow
|
|
Action: lambda:InvokeFunction
|
|
Resource: !GetAtt SlackNotifierFunction.Arn
|
|
- Effect: Allow
|
|
Action: ssm:GetParameter
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
Events:
|
|
SubmitOrder:
|
|
Type: HttpApi
|
|
Properties:
|
|
ApiId: !Ref OrderApi
|
|
Path: /api/submit-order
|
|
Method: POST
|
|
FormStatus:
|
|
Type: HttpApi
|
|
Properties:
|
|
ApiId: !Ref OrderApi
|
|
Path: /api/form-status/{week}
|
|
Method: GET
|
|
Roster:
|
|
Type: HttpApi
|
|
Properties:
|
|
ApiId: !Ref OrderApi
|
|
Path: /api/roster
|
|
Method: GET
|
|
|
|
CloseFormFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-close-form
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/close_form/
|
|
MemorySize: 128
|
|
Timeout: 30
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: lambda:InvokeFunction
|
|
Resource: !GetAtt AggregateOrdersFunction.Arn
|
|
Environment:
|
|
Variables:
|
|
AGGREGATE_FUNCTION_ARN: !GetAtt AggregateOrdersFunction.Arn
|
|
# Both EST and EDT schedules fire every week year-round. The handler is
|
|
# idempotent, so the "wrong timezone" firing is a harmless no-op.
|
|
Events:
|
|
CloseEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(59 4 ? * FRI *)
|
|
Description: 'Close form Thursday 11:59pm EST (04:59 UTC Friday)'
|
|
Enabled: true
|
|
CloseEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(59 3 ? * FRI *)
|
|
Description: 'Close form Thursday 11:59pm EDT (03:59 UTC Friday)'
|
|
Enabled: true
|
|
|
|
AggregateOrdersFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-aggregate-orders
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/aggregate_orders/
|
|
MemorySize: 256
|
|
Timeout: 60
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- S3CrudPolicy:
|
|
BucketName: !Ref ReportsBucket
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: lambda:InvokeFunction
|
|
Resource: !GetAtt SlackNotifierFunction.Arn
|
|
Environment:
|
|
Variables:
|
|
SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
SlackNotifierFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-slack-notifier
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/slack_notifier/
|
|
MemorySize: 128
|
|
Timeout: 30
|
|
Policies:
|
|
- DynamoDBReadPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: secretsmanager:GetSecretValue
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
- Effect: Allow
|
|
Action: ssm:GetParameter
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
Events:
|
|
ReminderEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 15 ? * THU *)
|
|
Description: 'DM reminders Thursday 10am EST (15:00 UTC)'
|
|
Enabled: true
|
|
Input: '{"event": "reminder"}'
|
|
ReminderEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 14 ? * THU *)
|
|
Description: 'DM reminders Thursday 10am EDT (14:00 UTC)'
|
|
Enabled: true
|
|
Input: '{"event": "reminder"}'
|
|
|
|
SyncRosterFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-sync-roster
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/sync_roster/
|
|
MemorySize: 128
|
|
Timeout: 60
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: secretsmanager:GetSecretValue
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
- Effect: Allow
|
|
Action: ssm:GetParameter
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
Events:
|
|
SyncEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(55 11 ? * MON *)
|
|
Description: 'Sync roster Monday 6:55am EST (11:55 UTC) — before menu publish'
|
|
Enabled: true
|
|
SyncEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(55 10 ? * MON *)
|
|
Description: 'Sync roster Monday 6:55am EDT (10:55 UTC) — before menu publish'
|
|
Enabled: true
|
|
|
|
EmailReportFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-email-report
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/email_report/
|
|
MemorySize: 128
|
|
Timeout: 30
|
|
Environment:
|
|
Variables:
|
|
PAYROLL_EMAIL: !Ref PayrollEmail
|
|
SENDER_EMAIL: !Ref SenderEmail
|
|
Policies:
|
|
- DynamoDBReadPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- S3ReadPolicy:
|
|
BucketName: !Ref ReportsBucket
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- ses:SendRawEmail
|
|
Resource: '*'
|
|
Events:
|
|
PayrollEmailEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 12 ? * MON *)
|
|
Description: 'Email payroll deductions Monday 7am EST (12:00 UTC)'
|
|
Enabled: true
|
|
PayrollEmailEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 11 ? * MON *)
|
|
Description: 'Email payroll deductions Monday 7am EDT (11:00 UTC)'
|
|
Enabled: true
|
|
|
|
# ─── CloudWatch Log Groups (60-day retention) ──────────────────
|
|
|
|
SubmitOrderLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${SubmitOrderFunction}'
|
|
RetentionInDays: 60
|
|
|
|
CloseFormLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${CloseFormFunction}'
|
|
RetentionInDays: 60
|
|
|
|
AggregateOrdersLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${AggregateOrdersFunction}'
|
|
RetentionInDays: 60
|
|
|
|
SlackNotifierLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${SlackNotifierFunction}'
|
|
RetentionInDays: 60
|
|
|
|
EmailReportLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${EmailReportFunction}'
|
|
RetentionInDays: 60
|
|
|
|
SyncRosterLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${SyncRosterFunction}'
|
|
RetentionInDays: 60
|
|
|
|
# ─── SSM Parameters ────────────────────────────────────────────
|
|
|
|
SlackChannelParam:
|
|
Type: AWS::SSM::Parameter
|
|
Properties:
|
|
Name: /meal-order-manager/slack-channel-id
|
|
Type: String
|
|
Value: CHANGE_ME
|
|
Description: Slack channel ID for meal order notifications
|
|
|
|
# GoogleClientIdParam (/meal-order-manager/google-client-id) is managed
|
|
# manually via AWS CLI since it varies per environment. Create it with:
|
|
# aws ssm put-parameter --name /meal-order-manager/google-client-id \
|
|
# --type String --value "<YOUR_GOOGLE_CLIENT_ID>"
|
|
|
|
Outputs:
|
|
ApiUrl:
|
|
Description: API Gateway endpoint URL
|
|
Value: !Sub 'https://${OrderApi}.execute-api.${AWS::Region}.amazonaws.com'
|
|
FormUrl:
|
|
Description: Order form URL
|
|
Value: !If
|
|
- HasCustomDomain
|
|
- !Sub 'https://${CustomDomain}'
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
DistributionId:
|
|
Description: CloudFront distribution ID (for cache invalidation)
|
|
Value: !Ref FormDistribution
|
|
FormBucketName:
|
|
Description: S3 bucket for form HTML
|
|
Value: !Ref FormBucket
|
|
ReportsBucketName:
|
|
Description: S3 bucket for CSV reports
|
|
Value: !Ref ReportsBucket
|
|
OrdersTableName:
|
|
Description: DynamoDB table name
|
|
Value: !Ref OrdersTable
|
|
SubmitOrderFunctionArn:
|
|
Description: Submit Order Lambda ARN
|
|
Value: !GetAtt SubmitOrderFunction.Arn
|
|
CloseFormFunctionArn:
|
|
Description: Close Form Lambda ARN
|
|
Value: !GetAtt CloseFormFunction.Arn
|
|
AggregateOrdersFunctionArn:
|
|
Description: Aggregate Orders Lambda ARN
|
|
Value: !GetAtt AggregateOrdersFunction.Arn
|
|
SlackNotifierFunctionArn:
|
|
Description: Slack Notifier Lambda ARN
|
|
Value: !GetAtt SlackNotifierFunction.Arn
|
|
SyncRosterFunctionArn:
|
|
Description: Sync Roster Lambda ARN
|
|
Value: !GetAtt SyncRosterFunction.Arn
|
|
EmailReportFunctionArn:
|
|
Description: Email Report Lambda ARN
|
|
Value: !GetAtt EmailReportFunction.Arn
|