AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Payments Dashboard - S3 CSV ingestion to Slack App Home Parameters: DynamoDbCmkArn: Type: AWS::SSM::Parameter::Value Default: /seahaven/dynamodb/cmk-arn Description: >- ARN of the shared customer-managed CMK (alias/seahaven-dynamodb) that encrypts the PaymentsDashboard table. Functions that read/write the table need kms:Decrypt/GenerateDataKey/DescribeKey on this key (the boundary permits exactly these), or DynamoDB calls fail with AccessDeniedException. Globals: Function: Runtime: nodejs24.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} PublicAccessBlockConfiguration: BlockPublicAcls: true IgnorePublicAcls: true BlockPublicPolicy: true RestrictPublicBuckets: true # Raw archive of every BoA reporting API response (exact bytes, keyed # raw//_/.json). Replayable corpus for # parser changes + audit trail. Retain: a template revert must never # attempt to delete a bank-data bucket; decommission goes through the CFN # decommission runbook (inventory, purge, deliberate deletion). # Retain + fixed name = rollback-orphan hazard (same class as the RETAIN # secret deadlock): if a failed deploy orphans the bucket, ADOPT it back # with a CloudFormation resource import — never delete-and-recreate. BoaRawBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: BucketName: !Sub seahaven-payments-boa-raw-${AWS::AccountId} PublicAccessBlockConfiguration: BlockPublicAcls: true IgnorePublicAcls: true BlockPublicPolicy: true RestrictPublicBuckets: true BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: expire-raw-responses Status: Enabled ExpirationInDays: 730 # Defense-in-depth for bank data: deny any non-TLS access. No Allow # statements — access is IAM-only (the Lambda's PutObject grant); unlike # PayrollEmailBucket there is no cross-service principal here. The Deny # covers bucket-level actions too, which is safe because nothing is # granted List/Get — revisit if read access is ever added. BoaRawBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref BoaRawBucket PolicyDocument: Version: "2012-10-17" Statement: - Sid: DenyInsecureTransport Effect: Deny Principal: "*" Action: s3:* Resource: - !GetAtt BoaRawBucket.Arn - !Sub "${BoaRawBucket.Arn}/*" Condition: Bool: aws:SecureTransport: "false" 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 # SSE-KMS with the shared CMK (alias/seahaven-dynamodb, INFRA-95 / M-3). # The table was migrated to this key out-of-band, so declaring it here # reconciles the template drift (no-op against the live table). Consumer # roles still need explicit kms perms below (SAM policies do not auto-add). SSESpecification: SSEEnabled: true SSEType: KMS KMSMasterKeyId: !Ref DynamoDbCmkArn PayrollEmailBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} PublicAccessBlockConfiguration: BlockPublicAcls: true IgnorePublicAcls: true BlockPublicPolicy: true RestrictPublicBuckets: true 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 # ── Lambda Errors alarms (Wave 1) ────────────────────────────────────────── # Clone of ProcessPaymentCsvErrorsAlarm for the remaining functions. AWS/Lambda # Errors, Sum over 5m, threshold > 0, ALARM-only by convention. SlackAppHomeErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-slackAppHome-errors AlarmDescription: payments-slackAppHome invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref SlackAppHomeFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts FetchBoaTransactionsErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-fetchBoaTransactions-errors AlarmDescription: payments-fetchBoaTransactions invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref FetchBoaTransactionsFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseReceiverErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseReceiver-errors AlarmDescription: payments-expenseReceiver invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref ExpenseReceiverFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseProcessorErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseProcessor-errors AlarmDescription: payments-expenseProcessor invocation errors Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref ExpenseProcessorFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # ── Lambda Throttles alarms (Wave 1) ─────────────────────────────────────── # AWS/Lambda Throttles, Sum over 5m, threshold > 0, ALARM-only. Throttling # signals concurrency exhaustion / reserved-concurrency starvation. ProcessPaymentCsvThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPaymentCsv-throttles AlarmDescription: payments-processPaymentCsv invocations throttled Namespace: AWS/Lambda MetricName: Throttles 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 ProcessPayrollEmailThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPayrollEmail-throttles AlarmDescription: payments-processPayrollEmail invocations throttled Namespace: AWS/Lambda MetricName: Throttles 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 FetchBoaTransactionsThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-fetchBoaTransactions-throttles AlarmDescription: payments-fetchBoaTransactions invocations throttled Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref FetchBoaTransactionsFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts SlackAppHomeThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-slackAppHome-throttles AlarmDescription: payments-slackAppHome invocations throttled Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref SlackAppHomeFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseReceiverThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseReceiver-throttles AlarmDescription: payments-expenseReceiver invocations throttled Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref ExpenseReceiverFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseProcessorThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseProcessor-throttles AlarmDescription: payments-expenseProcessor invocations throttled Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref ExpenseProcessorFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # ── Lambda Duration alarms (Wave 1) ──────────────────────────────────────── # AWS/Lambda Duration (ms), Statistic Maximum over 5m. Thresholds are ~80% of # each function's configured timeout — early warning before timeout-kills. ProcessPaymentCsvDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPaymentCsv-duration AlarmDescription: payments-processPaymentCsv approaching timeout (~80% of 120s) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref ProcessPaymentCsvFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 96000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ProcessPayrollEmailDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPayrollEmail-duration AlarmDescription: payments-processPayrollEmail approaching timeout (~80% of 60s) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref ProcessPayrollEmailFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 48000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts FetchBoaTransactionsDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-fetchBoaTransactions-duration AlarmDescription: payments-fetchBoaTransactions approaching timeout (~80% of 60s) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref FetchBoaTransactionsFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 48000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseProcessorDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseProcessor-duration AlarmDescription: payments-expenseProcessor approaching timeout (~80% of 15s) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref ExpenseProcessorFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 12000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ExpenseReceiverDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-expenseReceiver-duration AlarmDescription: payments-expenseReceiver approaching timeout (~80% of 5s) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref ExpenseReceiverFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 4000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts SlackAppHomeDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-slackAppHome-duration AlarmDescription: payments-slackAppHome approaching timeout (~80% of 30s default) Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref SlackAppHomeFunction Statistic: Maximum Period: 300 EvaluationPeriods: 1 Threshold: 24000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # ── DynamoDB alarms (Wave 1, SCOPE-CONFIRM) ──────────────────────────────── # AWS/DynamoDB throttle metrics for the PaymentsDashboard table. These metrics # emit at the TableName dimension and only on the occurrence of a throttle # event — none are currently present in CloudWatch (the table is # PAY_PER_REQUEST, so sustained throttling is unlikely but possible during # burst-capacity ramp). SystemErrors is intentionally not alarmed: AWS/DynamoDB # SystemErrors does not emit at the TableName-only dimension, so it can never # fire. Threshold > 0, Sum over 5m, ALARM-only. DashboardTableReadThrottleAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-dashboard-table-read-throttle AlarmDescription: PaymentsDashboard table read requests throttled Namespace: AWS/DynamoDB MetricName: ReadThrottleEvents Dimensions: - Name: TableName Value: !Ref DashboardTable Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts DashboardTableWriteThrottleAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-dashboard-table-write-throttle AlarmDescription: PaymentsDashboard table write requests throttled Namespace: AWS/DynamoDB MetricName: WriteThrottleEvents Dimensions: - Name: TableName Value: !Ref DashboardTable Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # ── API Gateway (HTTP API v2) alarms (Wave 1, SCOPE-CONFIRM) ──────────────── # AWS/ApiGateway v2 metrics on the implicit ServerlessHttpApi (ApiId dim). # v2 metric names are 4xx/5xx/Latency (not 4XXError/5XXError). 5xx and Latency # alarm on the API itself; 4xx is mostly client-driven so its threshold is # set above zero to avoid noise (Slack URL-verification / bad requests). ApiGateway5xxAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-dashboard-api-5xx AlarmDescription: payments-dashboard HTTP API returned 5xx responses Namespace: AWS/ApiGateway MetricName: 5xx Dimensions: - Name: ApiId Value: !Ref ServerlessHttpApi Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ApiGateway4xxAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-dashboard-api-4xx AlarmDescription: payments-dashboard HTTP API elevated 4xx responses Namespace: AWS/ApiGateway MetricName: 4xx Dimensions: - Name: ApiId Value: !Ref ServerlessHttpApi Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 10 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts ApiGatewayLatencyAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-dashboard-api-latency-p99 AlarmDescription: payments-dashboard HTTP API p99 latency elevated (>3s) Namespace: AWS/ApiGateway MetricName: Latency Dimensions: - Name: ApiId Value: !Ref ServerlessHttpApi ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 Threshold: 3000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts # Messages-present alarms on the async-invoke OnFailure DLQs, mirroring # PayrollBatchDLQAlarm. Threshold > 0 on the visible-message count, ALARM-only. ProcessPaymentCsvDLQAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPaymentCsv-async-dlq-messages AlarmDescription: Failed processPaymentCsv async invocations landed in the DLQ Namespace: AWS/SQS MetricName: ApproximateNumberOfMessagesVisible Dimensions: - Name: QueueName Value: !GetAtt ProcessPaymentCsvDLQ.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 ProcessPayrollEmailDLQAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: payments-processPayrollEmail-async-dlq-messages AlarmDescription: Failed processPayrollEmail async invocations landed in the DLQ Namespace: AWS/SQS MetricName: ApproximateNumberOfMessagesVisible Dimensions: - Name: QueueName Value: !GetAtt ProcessPayrollEmailDLQ.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 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: - kms:Decrypt - kms:GenerateDataKey - kms:DescribeKey Resource: !Ref DynamoDbCmkArn - 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: - kms:Decrypt - kms:GenerateDataKey - kms:DescribeKey Resource: !Ref DynamoDbCmkArn - 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 SLACK_SIGNING_SECRET_NAME: payments-dashboard/slack-signing-secret Events: SlackEvent: Type: HttpApi Properties: Path: /slack/events Method: POST Policies: - DynamoDBReadPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: - Effect: Allow Action: - kms:Decrypt - kms:DescribeKey Resource: !Ref DynamoDbCmkArn - 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-* - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/slack-signing-secret-* - 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 BOA_RAW_BUCKET: !Ref BoaRawBucket 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 # One rule for all intraday runs so they can be disabled as a unit # (aws events disable-rule) without touching the authoritative 9am # previous-day sweep. Fixed UTC: ~12/3/6pm ET in DST, 11/2/5pm in # winter (accepted drift, documented in README). IntradaySchedule: Type: Schedule Properties: Schedule: cron(0 16,19,22 ? * MON-FRI *) Description: Intraday BoA current-day sweep (~12pm/3pm/6pm ET) Enabled: true Input: '{"endpoint":"current-day"}' Policies: - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - Version: "2012-10-17" Statement: # Archive writes only: no read, no list, no other principal. # Derived from the bucket resource so a rename can't silently # detach the grant. - Effect: Allow Action: s3:PutObject Resource: !Sub "${BoaRawBucket.Arn}/*" - Version: "2012-10-17" Statement: - Effect: Allow Action: - kms:Decrypt - kms:GenerateDataKey - kms:DescribeKey Resource: !Ref DynamoDbCmkArn - 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