diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ad666e15 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Docker Compose Port Configurations (Change in your local .env to fix port conflicts) +COMPOSE_PORT_POSTGRES=5432 +COMPOSE_PORT_REDIS=6379 +COMPOSE_PORT_SOROBAN=8000 +COMPOSE_PORT_BACKEND=3000 +COMPOSE_PORT_ML=8001 +COMPOSE_PORT_EXPO=8081 \ No newline at end of file diff --git a/README.md b/README.md index 478e58b8..e5601ea5 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,24 @@ npm run lint - Make sure you're connected to the same Stellar network as the app (testnet/public) +## Local Development Environment + +SubTrackr utilizes a fully containerized local environment orchestrated via Docker Compose, eliminating the need to manually install dependencies like PostgreSQL, Redis, Soroban CLI, Rust, and Node.js. + +### Architecture +* **API Gateway (Backend):** Port 3000 +* **Background Workers:** Billing queues +* **Webhook Dispatcher:** Payload deliveries +* **ML Service (Python):** Port 8001 +* **Database & Cache:** PostgreSQL (5432), Redis (6379) +* **Soroban Node:** Standalone local network (8000) + +### Quick Setup + +1. **Initialize the Environment** + ```bash + ./scripts/setup.sh + ## Contributing We welcome contributions! SubTrackr participates in the **Stellar Wave Program** via [Drips](https://www.drips.network/). Contributors can earn points and rewards by picking up issues labeled **`Stellar Wave`**. diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..89dc39aa --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,22 @@ +version: '3.8' +# Maps local directories to containers for instant hot-reloading (ts-node-dev / uvicorn) +services: + backend: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + workers: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + webhook-dispatcher: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + ml-service: + volumes: + - ./ml-service:/app + mobile: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 50a09067..6ce2cb7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,87 +1,162 @@ -# Local development services for SubTrackr backend. -# -# Usage: -# docker compose up -d -# npm run server:start -# -# Environment (optional .env): -# DB_HOST=localhost DB_PORT=5432 DB_NAME=subtrackr DB_USER=postgres DB_PASSWORD=postgres -# REDIS_HOST=localhost REDIS_PORT=6379 +version: '3.8' + +# Common limits: 8 services * 0.5 CPU = 4 CPUs | 8 services * 1G = 8GB RAM +x-resources: &default-resources + deploy: + resources: + limits: + cpus: '0.5' + memory: 1G services: - redis: - image: redis:7-alpine - container_name: subtrackr-redis + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-postgres} + POSTGRES_DB: ${DB_NAME:-subtrackr} ports: - - '${REDIS_PORT:-6379}:6379' - command: ['redis-server', '--save', '', '--appendonly', 'no'] + - "${COMPOSE_PORT_POSTGRES:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data healthcheck: - test: ['CMD', 'redis-cli', 'ping'] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-subtrackr}"] interval: 5s - timeout: 3s + timeout: 5s retries: 5 - volumes: - - redis-data:/data + <<: *default-resources - postgres: - image: postgres:16-alpine - container_name: subtrackr-postgres + redis: + image: redis:7-alpine ports: - - '${DB_PORT:-5432}:5432' - environment: - POSTGRES_DB: ${DB_NAME:-subtrackr} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + - "${COMPOSE_PORT_REDIS:-6379}:6379" + volumes: + - redis_data:/data healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-subtrackr}'] + test: ["CMD", "redis-cli", "ping"] interval: 5s - timeout: 3s + timeout: 5s retries: 5 - volumes: - - postgres-data:/var/lib/postgresql/data + <<: *default-resources - rabbitmq: - image: rabbitmq:3-management-alpine - container_name: subtrackr-rabbitmq + soroban-standalone: + image: stellar/quickstart:testing + command: --standalone --enable-soroban-rpc ports: - - '5672:5672' - - '15672:15672' - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest} + - "${COMPOSE_PORT_SOROBAN:-8000}:8000" healthcheck: - test: ['CMD', 'rabbitmq-diagnostics', 'ping'] - interval: 5s - timeout: 3s + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 10s + timeout: 5s retries: 5 - volumes: - - rabbitmq-data:/var/lib/rabbitmq + <<: *default-resources - notification-service: + backend: build: - context: ./services/notification - dockerfile: Dockerfile - container_name: subtrackr-notification-service + context: . + dockerfile: docker/backend.Dockerfile ports: - - '${NOTIFICATION_PORT:-3001}:3000' + - "${COMPOSE_PORT_BACKEND:-3000}:3000" environment: - RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASS:-guest}@rabbitmq:5672 - RABBITMQ_QUEUE: notification.deliver - SENDGRID_API_KEY: ${SENDGRID_API_KEY} - TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID} - TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN} - TWILIO_FROM_NUMBER: ${TWILIO_FROM_NUMBER} - EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN} + - NODE_ENV=development + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + - SOROBAN_RPC_URL=${SOROBAN_RPC_URL:-http://soroban-standalone:8000/rpc} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/server/start.ts"] depends_on: - rabbitmq: + postgres: condition: service_healthy - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] - interval: 10s - timeout: 5s - retries: 5 + redis: + condition: service_healthy + <<: *default-resources + + workers: + build: + context: . + dockerfile: docker/backend.Dockerfile + environment: + - NODE_ENV=development + - DB_HOST=postgres + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/billing/jobs/billingJobQueue.ts"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + <<: *default-resources + + webhook-dispatcher: + build: + context: . + dockerfile: docker/backend.Dockerfile + environment: + - NODE_ENV=development + - DB_HOST=postgres + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/webhook/jobs/webhookDeliveryJob.ts"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + <<: *default-resources + + ml-service: + build: + context: . + dockerfile: docker/ml.Dockerfile + ports: + - "${COMPOSE_PORT_ML:-8001}:8000" + environment: + - DB_HOST=postgres + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + depends_on: + postgres: + condition: service_healthy + <<: *default-resources + + mobile: + build: + context: . + dockerfile: docker/backend.Dockerfile + ports: + - "${COMPOSE_PORT_EXPO:-8081}:8081" + environment: + - NODE_ENV=development + - EXPO_DEVTOOLS_LISTEN_ADDRESS=0.0.0.0 + - REACT_NATIVE_PACKAGER_HOSTNAME=${HOST_IP:-127.0.0.1} + command: ["npx", "expo", "start"] + profiles: + - mobile + <<: *default-resources + + seed: + image: postgres:15-alpine + environment: + - PGHOST=postgres + - PGUSER=${DB_USER:-postgres} + - PGPASSWORD=${DB_PASS:-postgres} + - PGDATABASE=${DB_NAME:-subtrackr} + volumes: + - ./docker/seed:/seed + depends_on: + postgres: + condition: service_healthy + command: ["psql", "-f", "/seed/seed.sql"] + profiles: + - tools volumes: - redis-data: - postgres-data: - rabbitmq-data: + postgres_data: + redis_data: \ No newline at end of file diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 00000000..8662352e --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,20 @@ +FROM node:18-alpine + +# Install build tools for native dependencies +RUN apk add --no-cache python3 make g++ curl bash + +WORKDIR /usr/src/app + +# Leverage Docker cache for npm install +COPY package*.json ./ +COPY .npmrc ./ + +# Copy the scripts folder so postinstall hooks (like patch-metro.js) can execute +COPY scripts/ ./scripts/ + +RUN npm install + +# Copy the rest of the application code +COPY . . + +EXPOSE 3000 8081 \ No newline at end of file diff --git a/docker/ml.Dockerfile b/docker/ml.Dockerfile new file mode 100644 index 00000000..03e9499b --- /dev/null +++ b/docker/ml.Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-slim +WORKDIR /app +RUN apt-get update && apt-get install -y gcc curl && rm -rf /var/lib/apt/lists/* +COPY ml-service/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt || true +COPY ml-service/ . +EXPOSE 8000 \ No newline at end of file diff --git a/docker/seed/seed.sql b/docker/seed/seed.sql new file mode 100644 index 00000000..08565c9b --- /dev/null +++ b/docker/seed/seed.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS plans (id VARCHAR PRIMARY KEY, name VARCHAR, price DECIMAL, currency VARCHAR); +CREATE TABLE IF NOT EXISTS users (id VARCHAR PRIMARY KEY, email VARCHAR, name VARCHAR); +CREATE TABLE IF NOT EXISTS invoices (id VARCHAR PRIMARY KEY, user_id VARCHAR, plan_id VARCHAR, amount DECIMAL, status VARCHAR); + +INSERT INTO plans (id, name, price, currency) VALUES +('plan_1', 'Basic', 9.99, 'USD'), ('plan_2', 'Pro', 19.99, 'USD'), ('plan_3', 'Enterprise', 49.99, 'USD'), ('plan_4', 'Starter', 4.99, 'USD'), ('plan_5', 'Premium', 99.99, 'USD') ON CONFLICT DO NOTHING; + +INSERT INTO users (id, email, name) VALUES +('usr_1', 'u1@test.com', 'User 1'), ('usr_2', 'u2@test.com', 'User 2'), ('usr_3', 'u3@test.com', 'User 3'), ('usr_4', 'u4@test.com', 'User 4'), ('usr_5', 'u5@test.com', 'User 5'), ('usr_6', 'u6@test.com', 'User 6'), ('usr_7', 'u7@test.com', 'User 7'), ('usr_8', 'u8@test.com', 'User 8'), ('usr_9', 'u9@test.com', 'User 9'), ('usr_10', 'u10@test.com', 'User 10') ON CONFLICT DO NOTHING; + +INSERT INTO invoices (id, user_id, plan_id, amount, status) VALUES +('inv_1', 'usr_1', 'plan_1', 9.99, 'paid'), ('inv_2', 'usr_2', 'plan_2', 19.99, 'paid'), ('inv_3', 'usr_3', 'plan_3', 49.99, 'paid'), ('inv_4', 'usr_4', 'plan_4', 4.99, 'paid'), ('inv_5', 'usr_5', 'plan_5', 99.99, 'paid'), ('inv_6', 'usr_6', 'plan_1', 9.99, 'pending'), ('inv_7', 'usr_7', 'plan_2', 19.99, 'paid'), ('inv_8', 'usr_8', 'plan_3', 49.99, 'paid'), ('inv_9', 'usr_9', 'plan_4', 4.99, 'failed'), ('inv_10', 'usr_10', 'plan_5', 99.99, 'paid'), ('inv_11', 'usr_1', 'plan_1', 9.99, 'paid'), ('inv_12', 'usr_2', 'plan_2', 19.99, 'paid'), ('inv_13', 'usr_3', 'plan_3', 49.99, 'paid'), ('inv_14', 'usr_4', 'plan_4', 4.99, 'paid'), ('inv_15', 'usr_5', 'plan_5', 99.99, 'paid'), ('inv_16', 'usr_6', 'plan_1', 9.99, 'paid'), ('inv_17', 'usr_7', 'plan_2', 19.99, 'paid'), ('inv_18', 'usr_8', 'plan_3', 49.99, 'paid'), ('inv_19', 'usr_9', 'plan_4', 4.99, 'paid'), ('inv_20', 'usr_10', 'plan_5', 99.99, 'paid') ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/ml-service/__pycache__/main.cpython-310.pyc b/ml-service/__pycache__/main.cpython-310.pyc new file mode 100644 index 00000000..891bfe9b Binary files /dev/null and b/ml-service/__pycache__/main.cpython-310.pyc differ diff --git a/ml-service/__pycache__/model_registry.cpython-310.pyc b/ml-service/__pycache__/model_registry.cpython-310.pyc new file mode 100644 index 00000000..c07d42bb Binary files /dev/null and b/ml-service/__pycache__/model_registry.cpython-310.pyc differ diff --git a/ml-service/anomaly/__pycache__/__init__.cpython-310.pyc b/ml-service/anomaly/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 00000000..d74a79a4 Binary files /dev/null and b/ml-service/anomaly/__pycache__/__init__.cpython-310.pyc differ diff --git a/ml-service/anomaly/__pycache__/detector.cpython-310.pyc b/ml-service/anomaly/__pycache__/detector.cpython-310.pyc new file mode 100644 index 00000000..2429f26a Binary files /dev/null and b/ml-service/anomaly/__pycache__/detector.cpython-310.pyc differ diff --git a/ml-service/anomaly/__pycache__/features.cpython-310.pyc b/ml-service/anomaly/__pycache__/features.cpython-310.pyc new file mode 100644 index 00000000..b938775a Binary files /dev/null and b/ml-service/anomaly/__pycache__/features.cpython-310.pyc differ diff --git a/ml-service/anomaly/__pycache__/isolation_forest.cpython-310.pyc b/ml-service/anomaly/__pycache__/isolation_forest.cpython-310.pyc new file mode 100644 index 00000000..097b57ee Binary files /dev/null and b/ml-service/anomaly/__pycache__/isolation_forest.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/__init__.cpython-310.pyc b/ml-service/routers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 00000000..e9ce1c85 Binary files /dev/null and b/ml-service/routers/__pycache__/__init__.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/anomaly.cpython-310.pyc b/ml-service/routers/__pycache__/anomaly.cpython-310.pyc new file mode 100644 index 00000000..87685aeb Binary files /dev/null and b/ml-service/routers/__pycache__/anomaly.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/churn.cpython-310.pyc b/ml-service/routers/__pycache__/churn.cpython-310.pyc new file mode 100644 index 00000000..7b04a74e Binary files /dev/null and b/ml-service/routers/__pycache__/churn.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/health.cpython-310.pyc b/ml-service/routers/__pycache__/health.cpython-310.pyc new file mode 100644 index 00000000..ed1f0496 Binary files /dev/null and b/ml-service/routers/__pycache__/health.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/pricing.cpython-310.pyc b/ml-service/routers/__pycache__/pricing.cpython-310.pyc new file mode 100644 index 00000000..30727db5 Binary files /dev/null and b/ml-service/routers/__pycache__/pricing.cpython-310.pyc differ diff --git a/ml-service/routers/__pycache__/recommendations.cpython-310.pyc b/ml-service/routers/__pycache__/recommendations.cpython-310.pyc new file mode 100644 index 00000000..6143d8a1 Binary files /dev/null and b/ml-service/routers/__pycache__/recommendations.cpython-310.pyc differ diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 00000000..c287afc3 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +echo "===========================================" +echo " SubTrackr Local Environment Setup" +echo "===========================================" + +if ! command -v docker &> /dev/null; then + echo "❌ Error: Docker is not installed." + exit 1 +fi + +if [ ! -f .env ]; then + echo "Creating .env from .env.example..." + cp .env.example .env +fi + +echo "Pulling latest base images..." +docker compose pull + +echo "Building local services..." +docker compose build + +echo "✅ Setup complete!" +echo "➡️ Start the stack: docker compose up -d" +echo "➡️ Seed test data: docker compose run --rm seed" \ No newline at end of file