"""CDK stack for the work order email ingestion pipeline.""" import aws_cdk as cdk from aws_cdk import ( Duration, RemovalPolicy, Stack, aws_dynamodb as dynamodb, aws_lambda as lambda_, aws_s3 as s3, aws_s3_notifications as s3n, aws_ses as ses, aws_ses_actions as ses_actions, aws_secretsmanager as secretsmanager, ) from constructs import Construct class WorkorderIngestStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs): super().__init__(scope, construct_id, **kwargs) # --- S3 bucket for raw emails --- email_bucket = s3.Bucket( self, "EmailBucket", bucket_name=f"workorder-ingest-emails-{self.account}", removal_policy=RemovalPolicy.RETAIN, lifecycle_rules=[ s3.LifecycleRule(expiration=Duration.days(90)), ], ) # --- DynamoDB tables --- work_orders_table = dynamodb.Table( self, "WorkOrdersTable", table_name="WorkOrders", partition_key=dynamodb.Attribute( name="work_order_id", type=dynamodb.AttributeType.STRING, ), billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=RemovalPolicy.RETAIN, ) # GSI for querying by site code work_orders_table.add_global_secondary_index( index_name="site-code-index", partition_key=dynamodb.Attribute( name="site_code", type=dynamodb.AttributeType.STRING, ), sort_key=dynamodb.Attribute( name="updated_at", type=dynamodb.AttributeType.STRING, ), ) # GSI for querying by status work_orders_table.add_global_secondary_index( index_name="status-index", partition_key=dynamodb.Attribute( name="wo_status", type=dynamodb.AttributeType.STRING, ), sort_key=dynamodb.Attribute( name="updated_at", type=dynamodb.AttributeType.STRING, ), ) comments_table = dynamodb.Table( self, "CommentsTable", table_name="WorkOrderComments", partition_key=dynamodb.Attribute( name="work_order_id", type=dynamodb.AttributeType.STRING, ), sort_key=dynamodb.Attribute( name="comment_id", type=dynamodb.AttributeType.STRING, ), billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=RemovalPolicy.RETAIN, ) # --- Secrets Manager for Anthropic API key --- anthropic_secret = secretsmanager.Secret( self, "AnthropicApiKey", secret_name="workorder-ingest/anthropic-api-key", description="Anthropic API key for work order email parsing", ) # --- Lambda function --- email_processor = lambda_.Function( self, "EmailProcessor", function_name="workorder-email-processor", runtime=lambda_.Runtime.PYTHON_3_12, handler="handler.handler", code=lambda_.Code.from_asset( "../lambdas/email_processor", bundling=cdk.BundlingOptions( image=lambda_.Runtime.PYTHON_3_12.bundling_image, platform="linux/amd64", command=[ "bash", "-c", "pip install --platform manylinux2014_x86_64 --only-binary=:all: " "-r requirements.txt -t /asset-output && " "cp -r . /asset-output/", ], ), ), timeout=Duration.seconds(60), memory_size=256, environment={ "WORK_ORDERS_TABLE": work_orders_table.table_name, "COMMENTS_TABLE": comments_table.table_name, "ANTHROPIC_API_KEY_SECRET_ARN": anthropic_secret.secret_arn, }, ) # Grant permissions email_bucket.grant_read(email_processor) work_orders_table.grant_read_write_data(email_processor) comments_table.grant_read_write_data(email_processor) anthropic_secret.grant_read(email_processor) # S3 event notification → Lambda email_bucket.add_event_notification( s3.EventType.OBJECT_CREATED, s3n.LambdaDestination(email_processor), s3.NotificationKeyFilter(prefix="inbound/"), ) # --- SES Receipt Rule --- rule_set = ses.ReceiptRuleSet.from_receipt_rule_set_name( self, "ExistingRuleSet", "INBOUND_MAIL", ) rule_set.add_rule( "WorkorderEmailRule", recipients=["apm@int.seahaven.com"], actions=[ ses_actions.S3( bucket=email_bucket, object_key_prefix="inbound/", ), ], ) # --- Web UI Lambda --- web_ui = lambda_.Function( self, "WebUI", function_name="workorder-web-ui", runtime=lambda_.Runtime.PYTHON_3_12, handler="handler.handler", code=lambda_.Code.from_asset("../lambdas/web_ui"), timeout=Duration.seconds(15), memory_size=128, environment={ "WORK_ORDERS_TABLE": work_orders_table.table_name, "COMMENTS_TABLE": comments_table.table_name, }, ) work_orders_table.grant_read_data(web_ui) comments_table.grant_read_data(web_ui) # Function URL for direct access web_url = web_ui.add_function_url( auth_type=lambda_.FunctionUrlAuthType.NONE, ) cdk.CfnOutput( self, "WebUIUrl", value=web_url.url, description="Work Order Dashboard URL" )