Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
- Use proper separation of concerns (e.g. TanStack layout routes instead of polluting `__root.tsx`).
- Implement robust error handling, proper typing, and scalable folder structures from the very first commit.
- Never use anti-patterns to save time. If a proper implementation takes more steps, take the time to do it right.

# Package Manager
- ALWAYS use `pnpm` for frontend/Node.js package management instead of `npm`. Do not use `npm install`.
25 changes: 16 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,19 @@ dev:

dev-as2:
@echo "Starting AS2 Server with hot-reload for local development..."
ENVIRONMENT=development uv run uvicorn as2_server.main:app --reload --port 8000
ENVIRONMENT=development uv run uvicorn as2_server.main:app --reload --host 0.0.0.0 --port 8000

dev-api:
@echo "Starting API Gateway with hot-reload for local development..."
ENVIRONMENT=development uv run uvicorn api.main:app --reload --port 8001
ENVIRONMENT=development uv run uvicorn api.main:app --reload --host 0.0.0.0 --port 8001

dev-web:
@echo "Starting React Frontend with Vite..."
cd frontend/web && pnpm dev

dev-worker:
@echo "Starting Provision Worker for local development..."
ENVIRONMENT=development PYTHONPATH=services/worker/src:libs/database/src:libs/config/src:libs/pipeline/src uv run python services/worker/src/worker/provision/main.py
@echo "Starting Unified Worker (Data + Provision) for local development..."
ENVIRONMENT=development PYTHONPATH=services/worker/src:libs/database/src:libs/config/src:libs/pipeline/src:libs/domain/src:libs/transformer/src uv run python services/worker/src/worker/main.py

db-init:
@echo "Waiting for databases to be ready..."
Expand All @@ -69,16 +69,23 @@ db-init:

db-reset:
@echo "Wiping application databases (leaving Zitadel intact)..."
docker compose stop postgres_global postgres_shard_1
docker compose rm -f -v postgres_global postgres_shard_1
-docker volume rm $$(docker volume ls -q | grep -E "postgres_global_data|postgres_shard_[0-9]+_data") 2>/dev/null
@echo "Restarting application databases..."
docker compose up -d postgres_global postgres_shard_1
docker compose stop postgres_global postgres_shard_1 debezium_shard_1
docker compose rm -f -v postgres_global postgres_shard_1 debezium_shard_1
-docker volume rm $$(docker volume ls -q | grep -E "postgres_global_data|postgres_shard_[0-9]+_data|debezium_data") 2>/dev/null
@echo "Restarting application databases and Debezium..."
docker compose up -d postgres_global postgres_shard_1 debezium_shard_1
@echo "Waiting for databases to initialize..."
sleep 5
@echo "Re-running migrations and seeding..."
$(MAKE) db-init

sqs-purge:
@echo "Purging all LocalStack SQS queues..."
uv run python scripts/purge_sqs.py

db-sqs-reset: db-reset sqs-purge
@echo "Database and SQS queues have been completely reset."

seed: db-init

# --- Docker Infrastructure (Postgres, LocalStack, OTel) ---
Expand Down
47 changes: 40 additions & 7 deletions TECHNICAL_DEBT.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
# Technical Debt
# Technical Debt Register

A living record of known gaps, shortcuts, and missing enterprise capabilities.
This document tracks identified technical debt, proposed refactoring, and estimated effort to resolve.

---
## 1. Modernization of Bots Core (Stateless AST Migration)

## AS2 Protocol
**Description:**
The open-source `bots_core` library heavily relies on a legacy, stateful `Node` class pattern to build, traverse, and count EDI segments (e.g., `SE01`, `GE01` counting via `node.getcount()`). Currently, we are bypassing this internal engine completely in our modern microservices architecture by injecting raw stateless Python JSON dictionaries (AST format) into the `bots_core.facade`.

Because `bots_core` doesn't natively parse our stateless AST for trailing counts, we temporarily built an `ASTUtils.count_segments()` workaround in the `transformer` domain layer.

**Proposed Resolution:**
To achieve the long-term vision of completely merging and modernizing `bots_core` as a first-class citizen of our modern Python stack:
1. Refactor the inner workings of `bots_core` (specifically `message.py`, `outmessage.py`, and `node.py`) to natively accept, traverse, and serialize stateless dictionary ASTs without forcing instantiation of legacy `Node` objects.
2. Move AST utility functions (like `count_segments`) natively into `bots_core/domain/ast_utils.py`.
3. Eliminate the legacy mapping engine scripts (`inn.get()`, `out.put()`) internally inside `bots_core` if they are fully deprecated by the API gateway.

**Estimated Effort:** Medium-High
**Estimated Time:** 1 to 2 Sprint Weeks (40 - 80 hours)
**Impact:** Will result in a completely modernized, lightning-fast, fully stateless fork of the `bots` EDI engine perfectly aligned with our cloud-native event-driven architecture.

## 2. Outbox Sweeper (CDC Fallback Relay)

**Description:**
The system currently relies exclusively on Debezium (CDC) reading the PostgreSQL Write-Ahead Log (WAL) to route `Outbox` events to SQS. If Debezium crashes, loses offsets, or experiences network partitioning, `PENDING` outbox events will be permanently trapped in the database, breaking the asynchronous event pipeline.

**Proposed Resolution:**
Implement an Outbox Sweeper background worker that acts as a robust enterprise fallback and garbage collector:
1. **Fallback Poller:** A cron/scheduled task that periodically queries `SELECT * FROM outbox WHERE status = 'PENDING'` for events older than a configured threshold (e.g., 60 seconds) and manually relays them to SQS.
2. **Garbage Collector:** A cleanup task that runs `DELETE FROM outbox WHERE status = 'COMPLETED'` for events older than 7 days to prevent unbounded database growth.

**Estimated Effort:** Low
**Estimated Time:** 1 to 2 days
**Impact:** Essential for enterprise-grade high availability. Guarantees no messages are ever lost due to CDC infrastructure failures and keeps the database optimized over time.

## 3. AS2 Protocol

### Async MDN — Inbound Callback Not Implemented
**Priority:** High
Expand All @@ -25,11 +54,15 @@ A living record of known gaps, shortcuts, and missing enterprise capabilities.
store the sent `Message-ID` and `MIC` so they can be matched when the callback arrives.
- Database: Add an `outbound_mdn_pending` table `(message_id, mic, trace_id, expires_at)`.

---

## Testing
## 4. Testing

### No Frontend Test Runner (Vitest)
**Priority:** Medium
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`make test` skips frontend tests with a placeholder comment.
React component tests and TanStack Query mutation tests are not covered.

## 5. Database Schema

### Missing `edi_headers` Table
**Priority:** Medium
**Description:** We currently lack an `edi_headers` table to store extracted EDI header metadata (e.g. ST/GS segments). This table needs to be created and linked via foreign key to the `outbound_route` table so that EDI messages can be properly tracked and correlated with their configured outbound routes.
153 changes: 63 additions & 90 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
version: "3.9"

# Include the observability and identity stacks
version: '3.9'
include:
# - libs/observability/docker/docker-compose.yml
- libs/identity/docker/docker-compose.yml

- libs/identity/docker/docker-compose.yml
services:

# ---------------------------------------------------------------------------
# Postgres Databases (Hybrid Multi-Tenancy Architecture)
# ---------------------------------------------------------------------------

# 1. Global Control Plane DB
postgres_global:
image: postgres:15-alpine
container_name: edi_postgres_global
Expand All @@ -20,127 +10,110 @@ services:
POSTGRES_PASSWORD: edi_password
POSTGRES_DB: edi_global
ports:
- "5432:5432"
- 5432:5432
volumes:
- postgres_global_data:/var/lib/postgresql/data
- postgres_global_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U edi"]
test:
- CMD-SHELL
- pg_isready -U edi
interval: 5s
timeout: 5s
retries: 5

# 2. Standard Tier (Pooled Shard) DB
postgres_shard_1:
image: postgres:15-alpine
container_name: edi_postgres_shard_1
environment:
POSTGRES_USER: edi
POSTGRES_PASSWORD: edi_password
POSTGRES_DB: edi_shard_1
command: ["postgres", "-c", "wal_level=logical"]
command:
- postgres
- -c
- wal_level=logical
ports:
- "5433:5432"
- 5433:5432
volumes:
- postgres_shard_1_data:/var/lib/postgresql/data
- postgres_shard_1_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U edi"]
test:
- CMD-SHELL
- pg_isready -U edi
interval: 5s
timeout: 5s
retries: 5

# ---------------------------------------------------------------------------
# LocalStack (AWS Cloud Emulator for S3, SQS, SNS, KMS, etc.)
# ---------------------------------------------------------------------------
localstack:
image: localstack/localstack:3.8.0
container_name: edi_localstack
ports:
- "4566:4566" # LocalStack Gateway
- "4510-4559:4510-4559" # external services port range
- 4566:4566
- 4510-4559:4510-4559
environment:
- DEBUG=${DEBUG:-0}
- DOCKER_HOST=unix:///var/run/docker.sock
- DEBUG=${DEBUG:-0}
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
# Auto-init scripts are executed when LocalStack boots
- ./docker/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh
- ${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
- ./docker/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
test:
- CMD
- curl
- -f
- http://localhost:4566/_localstack/health
interval: 5s
timeout: 5s
retries: 5

# ---------------------------------------------------------------------------
# HashiCorp Vault (Secrets & PKI)
# ---------------------------------------------------------------------------
vault:
image: hashicorp/vault:1.15
container_name: edi_vault
ports:
- "127.0.0.1:8200:8200"
- 127.0.0.1:8200:8200
environment:
VAULT_DEV_ROOT_TOKEN_ID: "${VAULT_DEV_ROOT_TOKEN_ID}"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_DEV_ROOT_TOKEN_ID:-root}
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
cap_add:
- IPC_LOCK
- IPC_LOCK
healthcheck:
test: ["CMD", "vault", "status", "-address=http://127.0.0.1:8200"]
test:
- CMD
- vault
- status
- -address=http://127.0.0.1:8200
interval: 5s
timeout: 5s
retries: 5

# # ---------------------------------------------------------------------------
# # EDI AS2 Server (FastAPI)
# # ---------------------------------------------------------------------------
# as2-server:
# build:
# context: .
# dockerfile: soopaedi/services/as2_server/Dockerfile
# container_name: edi_as2_server
# ports:
# - "8000:8000"
# environment:
# - APP_ENV=development
# - LOG_LEVEL=DEBUG
# - DB_URL=postgresql+asyncpg://edi:edi_password@postgres:5432/edi
# - S3_BUCKET=edi-as2-payloads
# - S3_ENDPOINT_URL=http://localstack:4566
# - S3_REGION=us-east-1
# - S3_ACCESS_KEY_ID=test
# - S3_SECRET_ACCESS_KEY=test
# - OTEL_SERVICE_NAME=edi-as2-server
# - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# depends_on:
# postgres:
# condition: service_healthy
# localstack:
# condition: service_healthy

# ---------------------------------------------------------------------------
# Debezium CDC Server
# ---------------------------------------------------------------------------
debezium:
cdcproxy:
image: nginx:alpine
container_name: edi_cdcproxy
ports:
- 8002:80
command: "/bin/sh -c \"echo '\nserver {\n listen 80;\n location / {\n \
\ proxy_pass http://host.docker.internal:8001;\n proxy_set_header\
\ Connection \\\"\\\";\n proxy_set_header Upgrade \\\"\\\";\n }\n\
}' > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'\""
extra_hosts:
- host.docker.internal:host-gateway
debezium_shard_1:
image: quay.io/debezium/server:2.7.3.Final
container_name: edi_debezium
container_name: edi_debezium_shard_1
environment:
- DEBEZIUM_SOURCE_DATABASE_HOSTNAME=postgres_shard_1
- DEBEZIUM_SOURCE_DATABASE_PORT=5432
- DEBEZIUM_SOURCE_DATABASE_USER=edi
- DEBEZIUM_SOURCE_DATABASE_PASSWORD=edi_password
- DEBEZIUM_SOURCE_DATABASE_DBNAME=edi_shard_1
- DEBEZIUM_SINK_URL=http://host.docker.internal:8001/internal/cdc/relay
- DEBEZIUM_SOURCE_DATABASE_HOSTNAME=postgres_shard_1
- DEBEZIUM_SOURCE_DATABASE_PORT=5432
- DEBEZIUM_SOURCE_DATABASE_USER=edi
- DEBEZIUM_SOURCE_DATABASE_PASSWORD=edi_password
- DEBEZIUM_SOURCE_DATABASE_DBNAME=edi_shard_1
- DEBEZIUM_SINK_URL=http://cdcproxy/internal/cdc/relay
extra_hosts:
- "host.docker.internal:host-gateway"
- host.docker.internal:host-gateway
volumes:
- ./docker/debezium:/debezium/conf
- debezium_data:/debezium/data
- ./docker/debezium/application.properties:/debezium/conf/application.properties
- debezium_data:/debezium/data
depends_on:
postgres_shard_1:
condition: service_healthy

volumes:
postgres_global_data:
postgres_shard_1_data:
postgres_enterprise_1_data:
debezium_data:
postgres_global_data: null
postgres_shard_1_data: null
postgres_enterprise_1_data: null
debezium_data: null
32 changes: 32 additions & 0 deletions docker/debezium/application-global.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ── Source: Postgres WAL ──────────────────────────────────────────────────────
debezium.source.connector.class=io.debezium.connector.postgresql.PostgresConnector
debezium.source.topic.prefix=soopa_global
debezium.source.database.hostname=${DEBEZIUM_SOURCE_DATABASE_HOSTNAME:postgres_global}
debezium.source.database.port=${DEBEZIUM_SOURCE_DATABASE_PORT:5432}
debezium.source.database.user=${DEBEZIUM_SOURCE_DATABASE_USER:edi}
debezium.source.database.password=${DEBEZIUM_SOURCE_DATABASE_PASSWORD:edi_password}
debezium.source.database.dbname=${DEBEZIUM_SOURCE_DATABASE_DBNAME:edi_global}
debezium.source.plugin.name=pgoutput
debezium.source.publication.name=edi_global_cdc
debezium.source.slot.name=edi_global_slot

debezium.source.table.include.list=.*\\.outbox
debezium.source.snapshot.mode=initial

# ── Sink: HTTP → FastAPI CdcRelay ───────────────────────────────────
debezium.sink.type=http
debezium.sink.http.url=${DEBEZIUM_SINK_URL:http://host.docker.internal:8001/internal/cdc/relay}
debezium.sink.http.timeout.ms=10000
debezium.sink.http.retry.count=10
debezium.sink.http.retry.delay.ms=500
debezium.format.value=json
debezium.format.value.schemas.enable=false
debezium.format.key=json
debezium.transforms=unwrap
debezium.transforms.unwrap.type=io.debezium.transforms.ExtractNewRecordState
debezium.transforms.unwrap.drop.tombstones=true
debezium.transforms.unwrap.add.fields=table,schema,op

debezium.source.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore
debezium.source.offset.storage.file.filename=/debezium/data/offsets-global.dat
debezium.source.offset.flush.interval.ms=5000
7 changes: 7 additions & 0 deletions docker/localstack/init-aws.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ DLQ_ARN=$(awslocal sqs get-queue-attributes --queue-url http://localhost:4566/00
# Create main queue with redrive policy
awslocal sqs create-queue --queue-name EdiTransformerQueue --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}"

# Create Data Plane CDC Queues
awslocal sqs create-queue --queue-name TranslateQueue
awslocal sqs create-queue --queue-name DeliverQueue

# Create Control Plane CDC Queues
awslocal sqs create-queue --queue-name ProvisioningQueue

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
echo "LocalStack Initialization Complete."
2 changes: 2 additions & 0 deletions frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@radix-ui/react-radio-group": "^1.4.2",
"@radix-ui/react-select": "^2.3.1",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-tabs": "^1.1.17",
"@radix-ui/react-toast": "^1.2.18",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-router": "^1.170.16",
Expand All @@ -27,6 +28,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.4.0",
"lucide-react": "^1.21.0",
"next-themes": "^0.4.6",
"oidc-client-ts": "^3.5.0",
Expand Down
Loading
Loading