AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: After-Hours Shift Manager — Slack bot for managing on-call shifts with 3CX integration Parameters: Timezone: Type: String Default: "America/New_York" ShiftChannel: Type: String Description: Slack channel ID for schedule posts and shift notifications QueueNumber: Type: String Default: "801" Description: 3CX queue extension number to update Globals: Function: Runtime: python3.12 Timeout: 30 MemorySize: 1024 Architectures: - arm64 # Access logging + default throttling on the implicit HTTP API (audit M-18). HttpApi: AccessLogSettings: DestinationArn: !GetAtt ApiAccessLogGroup.Arn Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","method":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}' DefaultRouteSettings: ThrottlingBurstLimit: 50 ThrottlingRateLimit: 100 Resources: ApiAccessLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/apigateway/afterhours-shift-manager RetentionInDays: 90 # --- Shared Lambda Layer --- SharedLayer: Type: AWS::Serverless::LayerVersion Properties: LayerName: afterhours-shared ContentUri: src/shared/ CompatibleRuntimes: - python3.12 CompatibleArchitectures: - arm64 Metadata: BuildMethod: python3.12 BuildArchitecture: arm64 # --- DynamoDB --- ShiftTable: Type: AWS::DynamoDB::Table Properties: TableName: afterhours-shifts BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: PK AttributeType: S - AttributeName: SK AttributeType: S KeySchema: - AttributeName: PK KeyType: HASH - AttributeName: SK KeyType: RANGE TimeToLiveSpecification: AttributeName: expires_at Enabled: true # --- Slack Bot Lambda --- SlackBotFunction: Type: AWS::Serverless::Function Properties: FunctionName: afterhours-shift-manager Handler: handler.handler CodeUri: src/slack-bot/ Layers: - !Ref SharedLayer Environment: Variables: SHIFT_TABLE: !Ref ShiftTable SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret SHIFT_CHANNEL: !Ref ShiftChannel TCX_SECRET_PREFIX: afterhours-shift-manager/3cx- QUEUE_NUMBER: !Ref QueueNumber TZ: !Ref Timezone Policies: - DynamoDBCrudPolicy: TableName: !Ref ShiftTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*" Events: SlackEvents: Type: HttpApi Properties: Path: /slack/events Method: POST # --- Weekly Schedule Post (Monday 7am ET) --- WeeklyPostFunction: Type: AWS::Serverless::Function Properties: FunctionName: afterhours-weekly-post Handler: app.handler CodeUri: src/weekly-post/ Layers: - !Ref SharedLayer Environment: Variables: SHIFT_TABLE: !Ref ShiftTable SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token SHIFT_CHANNEL: !Ref ShiftChannel SES_SENDER: noreply@seahaven.com PAYROLL_RECIPIENTS: payroll@seahaven.com PAY_REPORT_USER: U0A3SC48T47 TZ: !Ref Timezone Policies: - DynamoDBCrudPolicy: TableName: !Ref ShiftTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*" - Effect: Allow Action: - ses:SendEmail Resource: - !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/*" Events: # EST: 7am ET = 12:00 UTC (Nov-Mar) WeeklyPostEST: Type: Schedule Properties: Schedule: cron(0 12 ? * MON *) Description: "Post weekly schedule Monday 7am EST" Enabled: true # EDT: 7am ET = 11:00 UTC (Mar-Nov) WeeklyPostEDT: Type: Schedule Properties: Schedule: cron(0 11 ? * MON *) Description: "Post weekly schedule Monday 7am EDT" Enabled: true # --- Roster Sync Lambda (daily sync from 3CX) --- RosterSyncFunction: Type: AWS::Serverless::Function Properties: FunctionName: afterhours-roster-sync Handler: app.handler CodeUri: src/roster-sync/ Layers: - !Ref SharedLayer Timeout: 60 Environment: Variables: SHIFT_TABLE: !Ref ShiftTable TCX_SECRET_PREFIX: afterhours-shift-manager/3cx- SYNC_GROUP: DEFAULT TZ: !Ref Timezone Policies: - DynamoDBCrudPolicy: TableName: !Ref ShiftTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*" Events: # Daily at 6am ET (before the 7am schedule post and 8am 3CX scheduler) # EST: 6am ET = 11:00 UTC (Nov-Mar) RosterSyncEST: Type: Schedule Properties: Schedule: cron(0 11 ? * * *) Description: "Sync roster from 3CX at 6am EST" Enabled: true # EDT: 6am ET = 10:00 UTC (Mar-Nov) RosterSyncEDT: Type: Schedule Properties: Schedule: cron(0 10 ? * * *) Description: "Sync roster from 3CX at 6am EDT" Enabled: true # --- Ring Scheduler (daily 3CX queue routing updates) --- RingSchedulerFunction: Type: AWS::Serverless::Function Properties: FunctionName: afterhours-ring-scheduler Handler: app.handler CodeUri: src/ring-scheduler/ Layers: - !Ref SharedLayer Timeout: 60 Environment: Variables: SHIFT_TABLE: !Ref ShiftTable TCX_SECRET_PREFIX: afterhours-shift-manager/3cx- QUEUE_NUMBER: !Ref QueueNumber TZ: !Ref Timezone Policies: - DynamoDBCrudPolicy: TableName: !Ref ShiftTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*" Events: # Daily at 8am ET — update after-hours routing DailyScheduleEST: Type: Schedule Properties: Schedule: cron(0 13 ? * * *) Description: "Update 3CX queue at 8am EST" Enabled: true DailyScheduleEDT: Type: Schedule Properties: Schedule: cron(0 12 ? * * *) Description: "Update 3CX queue at 8am EDT" Enabled: true # Weekends at 5pm ET — switch to night shift person WeekendEveningEST: Type: Schedule Properties: Schedule: cron(0 22 ? * SAT,SUN *) Description: "Update 3CX queue at 5pm EST weekends" Enabled: true WeekendEveningEDT: Type: Schedule Properties: Schedule: cron(0 21 ? * SAT,SUN *) Description: "Update 3CX queue at 5pm EDT weekends" Enabled: true # --- CloudWatch Log Groups (explicit 60-day retention) --- SlackBotLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${SlackBotFunction}" RetentionInDays: 60 WeeklyPostLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${WeeklyPostFunction}" RetentionInDays: 60 RosterSyncLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${RosterSyncFunction}" RetentionInDays: 60 RingSchedulerLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${RingSchedulerFunction}" RetentionInDays: 60 Outputs: SlackBotApiUrl: Description: URL for Slack app Request URL configuration Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events" ShiftTableName: Value: !Ref ShiftTable SlackBotFunctionArn: Value: !GetAtt SlackBotFunction.Arn WeeklyPostFunctionArn: Value: !GetAtt WeeklyPostFunction.Arn RosterSyncFunctionArn: Value: !GetAtt RosterSyncFunction.Arn RingSchedulerFunctionArn: Value: !GetAtt RingSchedulerFunction.Arn