2026-04-07 12:12:30 -04:00
|
|
|
"""CDK stack for the Coupa PO email ingestion pipeline."""
|
|
|
|
|
|
|
|
|
|
import aws_cdk as cdk
|
|
|
|
|
from aws_cdk import (
|
|
|
|
|
Duration,
|
|
|
|
|
RemovalPolicy,
|
|
|
|
|
Stack,
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
aws_cloudwatch as cloudwatch,
|
|
|
|
|
aws_cloudwatch_actions as cw_actions,
|
2026-04-07 12:12:30 -04:00
|
|
|
aws_dynamodb as dynamodb,
|
2026-06-10 19:31:55 -04:00
|
|
|
aws_kms as kms,
|
2026-04-07 12:12:30 -04:00
|
|
|
aws_lambda as lambda_,
|
2026-04-30 14:26:53 -04:00
|
|
|
aws_lambda_event_sources as lambda_event_sources,
|
|
|
|
|
aws_logs as logs,
|
2026-04-07 12:12:30 -04:00
|
|
|
aws_s3 as s3,
|
|
|
|
|
aws_s3_notifications as s3n,
|
|
|
|
|
aws_ses as ses,
|
|
|
|
|
aws_ses_actions as ses_actions,
|
|
|
|
|
aws_secretsmanager as secretsmanager,
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
aws_sns as sns,
|
|
|
|
|
aws_sqs as sqs,
|
2026-06-10 19:31:55 -04:00
|
|
|
aws_ssm as ssm,
|
2026-04-07 12:12:30 -04:00
|
|
|
)
|
|
|
|
|
from constructs import Construct
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PoIngestStack(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(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"EmailBucket",
|
2026-04-07 12:12:30 -04:00
|
|
|
bucket_name=f"po-ingest-emails-{self.account}",
|
|
|
|
|
removal_policy=RemovalPolicy.RETAIN,
|
|
|
|
|
lifecycle_rules=[
|
|
|
|
|
s3.LifecycleRule(expiration=Duration.days(90)),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-10 19:31:55 -04:00
|
|
|
# --- Shared customer-managed CMK for sensitive DynamoDB tables ---
|
|
|
|
|
# Owned by the account-baseline app (alias/seahaven-dynamodb, INFRA-95 /
|
|
|
|
|
# M-3); ARN published to SSM. The purchase-orders table was migrated to
|
|
|
|
|
# SSE-KMS out-of-band, so declaring encryption_key here reconciles the
|
|
|
|
|
# drift and — via grant_read_write_data below — propagates the required
|
|
|
|
|
# kms:Decrypt/GenerateDataKey/DescribeKey to the consumer roles.
|
|
|
|
|
dynamodb_cmk = kms.Key.from_key_arn(
|
|
|
|
|
self,
|
|
|
|
|
"DynamoDbCmk",
|
|
|
|
|
ssm.StringParameter.value_for_string_parameter(
|
|
|
|
|
self, "/seahaven/dynamodb/cmk-arn"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-30 14:26:53 -04:00
|
|
|
# --- Purchase-orders DynamoDB table ---
|
|
|
|
|
# Owned by this stack. Streams enabled for the site-extractor pipeline.
|
|
|
|
|
# Other stacks (seahaven-slack-bot) reference this table via fromTableName().
|
|
|
|
|
po_table = dynamodb.Table(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"PurchaseOrdersTable",
|
2026-04-30 14:26:53 -04:00
|
|
|
table_name="purchase-orders",
|
|
|
|
|
partition_key=dynamodb.Attribute(
|
|
|
|
|
name="po_number",
|
|
|
|
|
type=dynamodb.AttributeType.STRING,
|
|
|
|
|
),
|
|
|
|
|
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
|
|
|
removal_policy=RemovalPolicy.RETAIN,
|
Align PO schema with enriched records and improve extraction prompt
Replaces extraction prompt with domain-specific rules: trade
classification taxonomy (23 categories), site_code skip list,
zip padding, revision email type, and structured extraction for
fiscal_year, trade, and coupa_category.
Handler changes:
- New "revision" email type overwrites existing PO via put_item
- enrich_parsed() adds top-level state, ship_to_raw, data_source
- pad_zip() zero-pads short zip codes (e.g., "7001" → "07001")
- Removed invoice_total/invoice_count (Payee Central only)
Web UI: added revision badge, new detail fields (site code, state,
trade, fiscal year, coupa category, data source), line item table
now shows Qty/Unit/Price columns, list view shows Site and Trade.
CDK: fixed StreamViewType to match deployed table (NEW_IMAGE).
README: documented PO record schema and revision flow.
2026-05-01 19:40:18 -04:00
|
|
|
stream=dynamodb.StreamViewType.NEW_IMAGE,
|
2026-06-10 19:31:55 -04:00
|
|
|
encryption=dynamodb.TableEncryption.CUSTOMER_MANAGED,
|
|
|
|
|
encryption_key=dynamodb_cmk,
|
2026-04-07 12:12:30 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# --- Secrets Manager for Anthropic API key ---
|
|
|
|
|
anthropic_secret = secretsmanager.Secret(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"AnthropicApiKey",
|
2026-04-07 12:12:30 -04:00
|
|
|
secret_name="po-ingest/anthropic-api-key",
|
|
|
|
|
description="Anthropic API key for Coupa PO email parsing",
|
2026-05-01 19:17:19 -04:00
|
|
|
removal_policy=RemovalPolicy.RETAIN,
|
2026-04-07 12:12:30 -04:00
|
|
|
)
|
|
|
|
|
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
# --- DLQ for failed async invocations (INFRA-41 / audit H-8) ---
|
|
|
|
|
# SES → S3 → Lambda is async; without an OnFailure destination a failed
|
|
|
|
|
# parse (bad email, transient error) is silently dropped after Lambda's
|
|
|
|
|
# retries. CDK generates the queue name to avoid colliding with the
|
|
|
|
|
# interim CLI-created po-email-processor-dlq (removed post-deploy).
|
|
|
|
|
email_processor_dlq = sqs.Queue(
|
|
|
|
|
self,
|
|
|
|
|
"EmailProcessorDlq",
|
|
|
|
|
retention_period=Duration.days(14),
|
|
|
|
|
enforce_ssl=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-07 12:12:30 -04:00
|
|
|
# --- Lambda function ---
|
|
|
|
|
email_processor = lambda_.Function(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"EmailProcessor",
|
2026-04-07 12:12:30 -04:00
|
|
|
function_name="po-email-processor",
|
|
|
|
|
runtime=lambda_.Runtime.PYTHON_3_12,
|
2026-04-30 14:26:53 -04:00
|
|
|
architecture=lambda_.Architecture.ARM_64,
|
2026-04-07 12:12:30 -04:00
|
|
|
handler="handler.handler",
|
2026-05-08 16:01:21 -04:00
|
|
|
code=lambda_.Code.from_asset(
|
2026-05-12 15:21:06 -04:00
|
|
|
"../lambdas/po/email_processor",
|
2026-05-08 16:01:21 -04:00
|
|
|
bundling=cdk.BundlingOptions(
|
|
|
|
|
image=lambda_.Runtime.PYTHON_3_12.bundling_image,
|
|
|
|
|
command=[
|
|
|
|
|
"bash",
|
|
|
|
|
"-c",
|
2026-06-03 14:56:02 -04:00
|
|
|
"pip install --platform manylinux2014_aarch64 --only-binary=:all: "
|
|
|
|
|
"-r requirements.txt -t /asset-output && "
|
|
|
|
|
"cp handler.py /asset-output/",
|
2026-05-08 16:01:21 -04:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-04-07 12:12:30 -04:00
|
|
|
timeout=Duration.seconds(60),
|
|
|
|
|
memory_size=256,
|
2026-04-30 14:26:53 -04:00
|
|
|
log_retention=logs.RetentionDays.TWO_MONTHS,
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
dead_letter_queue=email_processor_dlq,
|
2026-04-07 12:12:30 -04:00
|
|
|
environment={
|
|
|
|
|
"PO_TABLE": "purchase-orders",
|
|
|
|
|
"ANTHROPIC_API_KEY_SECRET_ARN": anthropic_secret.secret_arn,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Grant permissions
|
|
|
|
|
email_bucket.grant_read(email_processor)
|
|
|
|
|
po_table.grant_read_write_data(email_processor)
|
|
|
|
|
anthropic_secret.grant_read(email_processor)
|
|
|
|
|
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
# --- Errors alarm (INFRA-41 / audit H-8) ---
|
|
|
|
|
# ALARM-only (no OK action, per the CloudWatch-alarm preference) to the
|
|
|
|
|
# shared site-alerts topic (CMK alias/seahaven-alarm-topics lives on the
|
|
|
|
|
# topic). Any errored invocation in a 5-min window pages.
|
|
|
|
|
alarm_topic = sns.Topic.from_topic_arn(
|
|
|
|
|
self,
|
|
|
|
|
"SiteAlertsTopic",
|
|
|
|
|
f"arn:aws:sns:{self.region}:{self.account}:site-alerts",
|
|
|
|
|
)
|
|
|
|
|
email_processor.metric_errors(
|
|
|
|
|
period=Duration.minutes(5),
|
|
|
|
|
statistic="Sum",
|
|
|
|
|
).create_alarm(
|
|
|
|
|
self,
|
|
|
|
|
"EmailProcessorErrorsAlarm",
|
|
|
|
|
alarm_name="po-email-processor-errors",
|
|
|
|
|
alarm_description="po-email-processor async invocation errors",
|
|
|
|
|
threshold=0,
|
|
|
|
|
evaluation_periods=1,
|
|
|
|
|
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
|
|
|
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
|
|
|
).add_alarm_action(cw_actions.SnsAction(alarm_topic))
|
|
|
|
|
|
2026-04-07 12:12:30 -04:00
|
|
|
# S3 event notification → Lambda
|
|
|
|
|
email_bucket.add_event_notification(
|
|
|
|
|
s3.EventType.OBJECT_CREATED,
|
|
|
|
|
s3n.LambdaDestination(email_processor),
|
|
|
|
|
s3.NotificationKeyFilter(prefix="inbound/"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# --- SES Receipt Rule ---
|
|
|
|
|
# Reuse the existing INBOUND_MAIL rule set (shared with workorder-ingest)
|
|
|
|
|
rule_set = ses.ReceiptRuleSet.from_receipt_rule_set_name(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"ExistingRuleSet",
|
|
|
|
|
"INBOUND_MAIL",
|
2026-04-07 12:12:30 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
rule_set.add_rule(
|
|
|
|
|
"PoEmailRule",
|
|
|
|
|
recipients=["amazon_po@int.seahaven.com"],
|
|
|
|
|
actions=[
|
|
|
|
|
ses_actions.S3(
|
|
|
|
|
bucket=email_bucket,
|
|
|
|
|
object_key_prefix="inbound/",
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# --- Web UI Lambda ---
|
|
|
|
|
web_ui = lambda_.Function(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"WebUI",
|
2026-04-07 12:12:30 -04:00
|
|
|
function_name="po-web-ui",
|
|
|
|
|
runtime=lambda_.Runtime.PYTHON_3_12,
|
2026-04-30 14:26:53 -04:00
|
|
|
architecture=lambda_.Architecture.ARM_64,
|
2026-04-07 12:12:30 -04:00
|
|
|
handler="handler.handler",
|
2026-05-12 15:21:06 -04:00
|
|
|
code=lambda_.Code.from_asset("../lambdas/po/web_ui"),
|
2026-04-07 12:12:30 -04:00
|
|
|
timeout=Duration.seconds(60),
|
|
|
|
|
memory_size=256,
|
2026-04-30 14:26:53 -04:00
|
|
|
log_retention=logs.RetentionDays.TWO_MONTHS,
|
2026-04-07 12:12:30 -04:00
|
|
|
environment={
|
|
|
|
|
"PO_TABLE": "purchase-orders",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
po_table.grant_read_data(web_ui)
|
|
|
|
|
|
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.
INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.
INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.
Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
|
|
|
# Public Function URL removed 2026-06-08 (INFRA-74 / audit C-5): the
|
|
|
|
|
# unauthenticated FunctionUrlAuthType.NONE URL was deleted out-of-band
|
|
|
|
|
# via CLI. Removing the construct (and its auto-generated Principal:*
|
|
|
|
|
# invoke permission) reconciles IaC with the live state.
|
2026-04-30 14:26:53 -04:00
|
|
|
|
|
|
|
|
# --- Verified sites table (extracted from PO ship-to addresses) ---
|
|
|
|
|
verified_sites_table = dynamodb.Table(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"VerifiedSitesTable",
|
2026-04-30 14:26:53 -04:00
|
|
|
table_name="verified-sites",
|
|
|
|
|
partition_key=dynamodb.Attribute(
|
|
|
|
|
name="siteCode",
|
|
|
|
|
type=dynamodb.AttributeType.STRING,
|
|
|
|
|
),
|
|
|
|
|
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
|
|
|
removal_policy=RemovalPolicy.RETAIN,
|
|
|
|
|
)
|
2026-06-03 15:32:23 -04:00
|
|
|
# by-state GSI removed 2026-06-03 (audit M-20): 0 reads in 30d against
|
|
|
|
|
# 518 WCU of write amplification. Re-add if a state-level query path ships.
|
2026-04-30 14:26:53 -04:00
|
|
|
|
|
|
|
|
# --- Site extractor Lambda (DynamoDB Streams → verified-sites) ---
|
|
|
|
|
site_extractor = lambda_.Function(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"SiteExtractor",
|
2026-04-30 14:26:53 -04:00
|
|
|
function_name="po-ingest-site-extractor",
|
|
|
|
|
runtime=lambda_.Runtime.PYTHON_3_12,
|
|
|
|
|
architecture=lambda_.Architecture.ARM_64,
|
|
|
|
|
handler="handler.handler",
|
2026-05-12 15:21:06 -04:00
|
|
|
code=lambda_.Code.from_asset("../lambdas/po/site_extractor"),
|
2026-04-30 14:26:53 -04:00
|
|
|
timeout=Duration.seconds(60),
|
|
|
|
|
memory_size=256,
|
|
|
|
|
log_retention=logs.RetentionDays.TWO_MONTHS,
|
|
|
|
|
environment={
|
|
|
|
|
"VERIFIED_SITES_TABLE": verified_sites_table.table_name,
|
2026-04-30 15:01:09 -04:00
|
|
|
"PENDING_REVIEW_TABLE": "pending-site-review",
|
2026-04-30 14:26:53 -04:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
verified_sites_table.grant_read_write_data(site_extractor)
|
|
|
|
|
|
|
|
|
|
site_extractor.add_event_source(
|
|
|
|
|
lambda_event_sources.DynamoEventSource(
|
|
|
|
|
po_table,
|
|
|
|
|
starting_position=lambda_.StartingPosition.TRIM_HORIZON,
|
|
|
|
|
batch_size=10,
|
|
|
|
|
max_batching_window=Duration.seconds(30),
|
|
|
|
|
bisect_batch_on_error=True,
|
|
|
|
|
retry_attempts=3,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-08 16:01:21 -04:00
|
|
|
cdk.CfnOutput(
|
|
|
|
|
self,
|
|
|
|
|
"VerifiedSitesTableName",
|
2026-04-30 14:26:53 -04:00
|
|
|
value=verified_sites_table.table_name,
|
|
|
|
|
description="Verified site addresses extracted from POs",
|
|
|
|
|
)
|
2026-04-30 15:01:09 -04:00
|
|
|
|
|
|
|
|
# --- Pending site review table (POs with no extractable site code) ---
|
|
|
|
|
pending_review_table = dynamodb.Table(
|
2026-05-08 16:01:21 -04:00
|
|
|
self,
|
|
|
|
|
"PendingSiteReviewTable",
|
2026-04-30 15:01:09 -04:00
|
|
|
table_name="pending-site-review",
|
|
|
|
|
partition_key=dynamodb.Attribute(
|
|
|
|
|
name="po_number",
|
|
|
|
|
type=dynamodb.AttributeType.STRING,
|
|
|
|
|
),
|
|
|
|
|
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
|
|
|
removal_policy=RemovalPolicy.RETAIN,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
pending_review_table.grant_read_write_data(site_extractor)
|
|
|
|
|
verified_sites_table.grant_read_data(site_extractor)
|