Files
integreat/terraform/deploy.tf
Bryce 95d0537c69 feat(sales-summaries): refresh on a schedule, skipping accepted days
sales-summaries-v2 recomputes every dirty summary, but nothing set the dirty
flag on a schedule: mark-all-dirty was only ever called by hand from the
comment block, and the job was registered in neither server.clj's
INTEGREAT_JOB dispatch nor terraform/deploy.tf, so -main was dead code that
could never run in production. Summaries were only recalculated when someone
remembered to do it in the REPL, and POS data keeps arriving after a business
day closes, so a summary computed once on the day was routinely wrong and
stayed wrong.

Add a daily job that marks the trailing 7 days dirty and recomputes them,
leaving finished work alone. "Finished" is the condition the app already calls
Balanced -- debits equal credits and every line is mapped to an account. Since
that is derived rather than stored, a summary that later falls out of balance
is picked up again on the next run.

Extract the Balanced predicate into auto-ap.datomic.sales-summaries so the
grid's pill and the job share one definition, rather than a background job
requiring an SSR namespace. total-debits/total-credits resolve the ledger side
from either a plain keyword or the {:db/ident ...} map a pull returns, and
accepted? requires every item to declare a side: un-normalized pulled items
otherwise sum to 0.0 on both sides, read as balanced, and get skipped
silently and permanently.

Also fix sales-summaries-v2 destroying user-entered line items. It filtered
for :sales-summary-item/manual? to preserve them, but dirty-sales-summaries'
index-pull selector never fetched :sales-summary/items, so manual-items was
always empty. Because items is a component attribute upserted via
[:reset-rels ...], every recompute deleted the hand-entered lines -- often the
very lines that make a summary balance. Harmless while nothing ran on a
schedule; destructive the moment this does.

Register the job in the admin Background Jobs dropdown too, with a days
field: schedules are prod-only, so the admin page is the only way to run it
on staging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:17:54 -07:00

483 lines
12 KiB
HCL

variable "task_role_arn" {}
variable "execution_role_arn" {}
variable "ecs_cluster" {}
variable "local_namespace" {}
variable "desired_count" {}
variable "background_desired_count" {}
variable "enable_schedules" {
type = bool
default = null
}
variable "http_listener_sg" {
default = "sg-0024906e0e1f78048"
}
variable "lb_subnets" {
default = [ "subnet-16161a39", "subnet-323deb78", "subnet-44c2774b", "subnet-5e675761", "subnet-8519fde2", "subnet-89bab8d4", ]
}
resource "aws_ecs_task_definition" "integreat_app" {
family = "integreat_app_${var.stage}"
container_definitions = file("${var.stage}-taskdef.json")
memory = 16384
cpu = 4096
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = var.execution_role_arn
task_role_arn = var.task_role_arn
}
resource "aws_ecs_service" "integreat_app" {
name = "integreat_app_${var.stage}"
cluster = var.ecs_cluster
task_definition = aws_ecs_task_definition.integreat_app.arn
desired_count = var.desired_count
health_check_grace_period_seconds = 600
deployment_controller {
type = "ECS"
}
scheduling_strategy = "REPLICA"
platform_version = "LATEST"
network_configuration {
assign_public_ip = true
security_groups = [ "sg-004e5855310c453a3", "sg-02d167406b1082698"]
subnets = [ "subnet-5e675761", "subnet-8519fde2", "subnet-89bab8d4", "subnet-16161a39", "subnet-323deb78","subnet-44c2774b"]
}
load_balancer {
target_group_arn = aws_lb_target_group.integreat_app.arn
container_name = "integreat-app"
container_port = 3000
}
service_registries {
container_port = 0
port = 0
registry_arn = aws_service_discovery_service.service.arn
}
capacity_provider_strategy {
base = 1
capacity_provider = "FARGATE_SPOT"
weight = 5
}
deployment_circuit_breaker {
enable = false
rollback = false
}
wait_for_steady_state = true
timeouts {}
lifecycle {
ignore_changes = [task_definition]
}
}
resource "aws_lb" "integreat_app" {
name = "integreat-app-${var.stage}"
internal = false
load_balancer_type = "application"
security_groups = [var.http_listener_sg]
subnets = var.lb_subnets
ip_address_type = "ipv4"
enable_deletion_protection = true
idle_timeout = 120
}
resource "aws_lb_target_group" "integreat_app" {
name = "integreat-app-${var.stage}"
port = 80
protocol = "HTTP"
vpc_id = "vpc-b5b7d6ce"
deregistration_delay = 120
load_balancing_algorithm_type = "round_robin"
slow_start = 0
tags = {}
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
interval = 60
matcher = "200"
path = "/api/health-check"
port = "traffic-port"
protocol = "HTTP"
timeout = 14
unhealthy_threshold = 3
}
stickiness {
cookie_duration = 86400
enabled = false
type = "lb_cookie"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.integreat_app.arn
port = 80
protocol = "HTTP"
default_action {
order = 1
type = "redirect"
redirect {
host = "#{host}"
path = "/#{path}"
port = "443"
protocol = "HTTPS"
query = "#{query}"
status_code = "HTTP_301"
}
}
timeouts {}
}
resource "aws_lb_listener" "https" {
certificate_arn = aws_acm_certificate.cert.arn
load_balancer_arn = aws_lb.integreat_app.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
default_action {
order = 1
target_group_arn = aws_lb_target_group.integreat_app.arn
type = "forward"
}
timeouts {}
}
resource "aws_lb_listener_rule" "static" {
listener_arn = aws_lb_listener.https.arn
priority = 1
action {
order = 1
type = "redirect"
redirect {
host = "s3.amazonaws.com"
path = "/${var.domain}/#{path}"
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
condition {
path_pattern {
values = [
"/css/*",
"/finance-font/*",
"/img/*",
"/js/compiled/*",
"index.html",
]
}
}
}
resource "aws_s3_bucket" "static" {
bucket = var.domain
request_payer = "BucketOwner"
tags = {}
cors_rule {
allowed_headers = [
"*",
]
allowed_methods = [
"PUT",
"POST",
"DELETE",
"GET",
]
allowed_origins = [
var.base_url,
]
expose_headers = []
max_age_seconds = 0
}
versioning {
enabled = false
mfa_delete = false
}
website {
index_document = "index.html"
}
policy = <<POLICY
{
"Id": "Policy1526084187222",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1526084185514",
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::${var.domain}/*",
"Principal": "*"
}
]
}
POLICY
}
resource "aws_acm_certificate" "cert" {
domain_name = var.domain
validation_method = "DNS"
}
resource "aws_acm_certificate" "data_cert" {
domain_name = "data.${var.stage}.app.integreatconsult.com"
validation_method = "DNS"
}
resource "aws_service_discovery_service" "service" {
name = "integreat-app-${var.stage}"
dns_config {
namespace_id = var.local_namespace
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
module "square_import_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(4 hours)"
job_name = "square-import-job"
execution_role_arn = var.execution_role_arn
memory = 4096
cpu = 1024
}
module "reconcile_ledger_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 hour)"
job_name = "reconcile-ledger"
execution_role_arn = var.execution_role_arn
memory = 8192
cpu = 2048
}
module "yodlee2_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(6 hours)"
job_name = "yodlee2"
execution_role_arn = var.execution_role_arn
memory = 4096
cpu = 2048
}
module "intuit_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(6 hours)"
job_name = "intuit"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 1024
}
module "plaid_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(6 hours)"
job_name = "plaid"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 1024
}
module "vendor_usages_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(4 hours)"
job_name = "vendor-usages"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 1024
}
module "import_uploaded_invoices_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 hour)"
job_name = "import-uploaded-invoices"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 512
}
module "sysco_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(3 hours)"
job_name = "sysco"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 512
}
module "close_auto_invoices_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 hour)"
job_name = "close-auto-invoices"
execution_role_arn = var.execution_role_arn
memory = 2048
cpu = 512
}
module "sales_summaries_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 day)"
job_name = "sales-summaries"
execution_role_arn = var.execution_role_arn
use_schedule = true
memory = 4096
cpu = 2048
}
module "yodlee2_accounts_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "yodlee2-accounts"
execution_role_arn = var.execution_role_arn
use_schedule = false
memory = 2048
cpu = 512
}
module "bulk_journal_import_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "bulk-journal-import"
execution_role_arn = var.execution_role_arn
use_schedule = false
memory = 4096
cpu = 1024
}
module "register_invoice_import_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "register-invoice-import"
execution_role_arn = var.execution_role_arn
use_schedule = false
memory = 8192
cpu = 2048
}
module "load_historical_sales_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "load-historical-sales"
execution_role_arn = var.execution_role_arn
use_schedule = false
memory = 4096
cpu = 1024
}
module "ntg_job" {
count = var.enable_schedules ? 1 : 0
schedule = "rate(6 hours)"
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "ntg"
execution_role_arn = var.execution_role_arn
use_schedule = true
memory = 4096
cpu = 1024
}
module "insight_outcome_recommendation_job" {
count = var.enable_schedules ? 1 : 0
schedule = "rate(6 hours)"
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
job_name = "insight-outcome-recommendation"
execution_role_arn = var.execution_role_arn
use_schedule = true
memory = 4096
cpu = 2048
}