AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Payments Dashboard - S3 CSV ingestion to Slack App Home Globals: Function: Runtime: nodejs22.x Architectures: - arm64 Timeout: 30 MemorySize: 256 PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary Environment: Variables: TABLE_NAME: !Ref DashboardTable # 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/payments-dashboard RetentionInDays: 90 # VPC with private subnet + NAT Gateway for static outbound IP Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.20.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: payments-dashboard-vpc PrivateSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.20.1.0/24 AvailabilityZone: !Select [0, !GetAZs ""] Tags: - Key: Name Value: payments-dashboard-private PublicSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.20.2.0/24 AvailabilityZone: !Select [0, !GetAZs ""] Tags: - Key: Name Value: payments-dashboard-public InternetGateway: Type: AWS::EC2::InternetGateway VpcGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref Vpc InternetGatewayId: !Ref InternetGateway NatEip: Type: AWS::EC2::EIP Properties: Domain: vpc NatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatEip.AllocationId SubnetId: !Ref PublicSubnet PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc PublicRoute: Type: AWS::EC2::Route DependsOn: VpcGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc PrivateRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway # Gateway endpoints (audit M-22): keep S3/DynamoDB traffic off the NAT # gateway — free, and removes per-GB NAT data-processing charges. S3GatewayEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcId: !Ref Vpc ServiceName: !Sub com.amazonaws.${AWS::Region}.s3 VpcEndpointType: Gateway RouteTableIds: - !Ref PublicRouteTable - !Ref PrivateRouteTable DynamoDbGatewayEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcId: !Ref Vpc ServiceName: !Sub com.amazonaws.${AWS::Region}.dynamodb VpcEndpointType: Gateway RouteTableIds: - !Ref PublicRouteTable - !Ref PrivateRouteTable PrivateSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable LambdaSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Payments Dashboard Lambda outbound access VpcId: !Ref Vpc SecurityGroupEgress: - IpProtocol: "-1" CidrIp: 0.0.0.0/0 PaymentsCsvBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub seahaven-payments-csv-${AWS::AccountId} DashboardTable: Type: AWS::DynamoDB::Table Properties: TableName: PaymentsDashboard BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: pk AttributeType: S KeySchema: - AttributeName: pk KeyType: HASH TimeToLiveSpecification: AttributeName: ttl Enabled: true PayrollEmailBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} LifecycleConfiguration: Rules: - Id: ExpireEmails Status: Enabled ExpirationInDays: 30 PayrollEmailBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref PayrollEmailBucket PolicyDocument: Version: "2012-10-17" Statement: - Sid: AllowSESPut Effect: Allow Principal: Service: ses.amazonaws.com Action: s3:PutObject Resource: !Sub arn:aws:s3:::seahaven-payroll-emails-${AWS::AccountId}/* Condition: StringEquals: AWS:SourceAccount: !Ref AWS::AccountId PayrollEmailRule: Type: AWS::SES::ReceiptRule DependsOn: PayrollEmailBucketPolicy Properties: RuleSetName: INBOUND_MAIL After: ExistingRuleSetWorkorderEmailRuleEA29F845-okepxcVarTfu Rule: Name: store-payroll-emails Enabled: true ScanEnabled: true Recipients: - payroll@int.seahaven.com Actions: - S3Action: BucketName: !Ref PayrollEmailBucket ObjectKeyPrefix: inbound/ PayrollBatchQueue: Type: AWS::SQS::Queue Properties: QueueName: payments-payroll-batch DelaySeconds: 600 MessageRetentionPeriod: 86400 VisibilityTimeout: 60 RedrivePolicy: deadLetterTargetArn: !GetAtt PayrollBatchDLQ.Arn maxReceiveCount: 3 # Audit L-15: payroll-batch messages were lost after max receives. 14-day # retention so a failure on Friday survives the weekend. PayrollBatchDLQ: Type: AWS::SQS::Queue Properties: QueueName: payments-payroll-batch-dlq MessageRetentionPeriod: 1209600 PayrollBatchDLQAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-payroll-batch-dlq-messages AlarmDescription: Failed payroll-batch messages landed in the DLQ Namespace: AWS/SQS MetricName: ApproximateNumberOfMessagesVisible Dimensions: - Name: QueueName Value: !GetAtt PayrollBatchDLQ.QueueName Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching # ALARM-only notification by convention — no OK/recovery action AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # Async-invoke OnFailure DLQs (INFRA-41 / H-8). Reconciles the interim # CLI-created queues into CloudFormation. Names are CFN-generated to avoid # colliding with the live interim payments--dlq queues (deleted after # this deploy). 14-day retention mirrors the payments-payroll-batch DLQ so a # Friday failure survives the weekend. # Distinct -async-dlq name (not the live interim payments--dlq) so this # CFN queue does not collide with the queue being deleted post-deploy. ProcessPaymentCsvDLQ: Type: AWS::SQS::Queue Properties: QueueName: payments-processPaymentCsv-async-dlq MessageRetentionPeriod: 1209600 # 14d, matches payments-payroll-batch-dlq ProcessPayrollEmailDLQ: Type: AWS::SQS::Queue Properties: QueueName: payments-processPayrollEmail-async-dlq MessageRetentionPeriod: 1209600 # 14d, matches payments-payroll-batch-dlq # ALARM-only Lambda Errors alarms (INFRA-41 / H-8). Threshold > 0 on the # Errors Sum, no OK/recovery action by convention. ProcessPaymentCsvErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPaymentCsv-errors AlarmDescription: payments-processPaymentCsv invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref ProcessPaymentCsvFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ProcessPayrollEmailErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPayrollEmail-errors AlarmDescription: payments-processPayrollEmail invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref ProcessPayrollEmailFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ProcessPayrollEmailLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-processPayrollEmail RetentionInDays: 60 ProcessPaymentCsvLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-processPaymentCsv RetentionInDays: 60 SlackAppHomeLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-slackAppHome RetentionInDays: 60 FetchBoaTransactionsLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-fetchBoaTransactions RetentionInDays: 60 ExpenseReceiverLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-expenseReceiver RetentionInDays: 60 ExpenseProcessorLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/payments-expenseProcessor RetentionInDays: 60 ProcessPayrollEmailFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-processPayrollEmail Handler: src/processPayrollEmail.handler Timeout: 60 EventInvokeConfig: MaximumRetryAttempts: 2 MaximumEventAgeInSeconds: 21600 DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt ProcessPayrollEmailDLQ.Arn Environment: Variables: SLACK_BOT_TOKEN_SECRET_NAME: payments-dashboard/slack-bot-token PAYROLL_CHANNEL_ID: C0AV5RBMYKU PAYROLL_BATCH_QUEUE_URL: !Ref PayrollBatchQueue Events: EmailReceived: Type: S3 Properties: Bucket: !Ref PayrollEmailBucket Events: s3:ObjectCreated:* Filter: S3Key: Rules: - Name: prefix Value: inbound/ PayrollBatch: Type: SQS Properties: Queue: !GetAtt PayrollBatchQueue.Arn BatchSize: 1 Policies: - S3ReadPolicy: BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/slack-bot-token-* - SQSSendMessagePolicy: QueueName: !GetAtt PayrollBatchQueue.QueueName - SQSPollerPolicy: QueueName: !GetAtt PayrollBatchQueue.QueueName ProcessPaymentCsvFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-processPaymentCsv Handler: src/processPaymentCsv.handler Timeout: 120 EventInvokeConfig: MaximumRetryAttempts: 2 MaximumEventAgeInSeconds: 21600 DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt ProcessPaymentCsvDLQ.Arn Environment: Variables: BOA_BASE_URL: https://api.bofa.com BOA_CHECK_MGMT_SECRET_NAME: payments-dashboard/boa-check-mgmt VpcConfig: SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref LambdaSecurityGroup Events: CsvUpload: Type: S3 Properties: Bucket: !Ref PaymentsCsvBucket Events: s3:ObjectCreated:* Filter: S3Key: Rules: - Name: suffix Value: .csv Policies: - S3ReadPolicy: BucketName: !Sub seahaven-payments-csv-${AWS::AccountId} - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/boa-check-mgmt-* - Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface Resource: "*" SlackAppHomeFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-slackAppHome Handler: src/slackAppHome.handler VpcConfig: SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref LambdaSecurityGroup Environment: Variables: SLACK_BOT_TOKEN_SECRET_NAME: payments-dashboard/slack-bot-token Events: SlackEvent: Type: HttpApi Properties: Path: /slack/events Method: POST Policies: - DynamoDBReadPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/slack-bot-token-* - Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface Resource: "*" FetchBoaTransactionsFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-fetchBoaTransactions Handler: src/fetchBoaTransactions.handler Timeout: 60 VpcConfig: SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref LambdaSecurityGroup Environment: Variables: BOA_BASE_URL: https://api.bofa.com BOA_REPORTING_SECRET_NAME: payments-dashboard/boa-reporting Events: DailySchedule: Type: Schedule Properties: Schedule: cron(0 13 ? * MON-FRI *) Description: Fetch BoA previous day transactions at 9am ET (13:00 UTC) Enabled: true Policies: - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/boa-reporting-* - Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface Resource: "*" ExpenseProcessorFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-expenseProcessor Handler: src/expenseProcessor.handler Timeout: 15 Environment: Variables: EXPENSE_BOT_TOKEN_SECRET_NAME: payments-dashboard/expense-slack-token Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/expense-slack-token-* ExpenseReceiverFunction: Type: AWS::Serverless::Function Properties: FunctionName: payments-expenseReceiver Handler: src/expenseReceiver.handler Timeout: 5 Environment: Variables: EXPENSE_PROCESSOR_FN: !Ref ExpenseProcessorFunction EXPENSE_SIGNING_SECRET_NAME: payments-dashboard/expense-slack-signing-secret Events: ExpenseSlackEvent: Type: HttpApi Properties: Path: /slack/expense-events Method: POST Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt ExpenseProcessorFunction.Arn - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/expense-slack-signing-secret-* Outputs: SlackEventUrl: Description: URL to set as the Slack app Request URL Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events CsvBucket: Description: S3 bucket for CSV uploads Value: !Ref PaymentsCsvBucket StaticOutboundIp: Description: Static IP for BoA API whitelist Value: !Ref NatEip PayrollEmailBucket: Description: S3 bucket for inbound payroll emails from SES Value: !Ref PayrollEmailBucket ExpenseSlackEventsUrl: Description: URL for Expense Approval Bot Slack Event Subscriptions Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/expense-events ExpenseProcessorFunctionArn: Description: Expense Processor Lambda ARN Value: !GetAtt ExpenseProcessorFunction.Arn ExpenseReceiverFunctionArn: Description: Expense Receiver Lambda ARN Value: !GetAtt ExpenseReceiverFunction.Arn