diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5f6083d --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +COMPOSE_PROJECT_NAME=payguard + +ORACLE_IMAGE=gvenzl/oracle-free:23-slim-faststart +WALLET_ORACLE_PORT=1521 +LOAN_ORACLE_PORT=1522 +COLLATERAL_ORACLE_PORT=1523 +WALLET_ORACLE_SYSTEM_PASSWORD=change-wallet-system-password +LOAN_ORACLE_SYSTEM_PASSWORD=change-loan-system-password +COLLATERAL_ORACLE_SYSTEM_PASSWORD=change-collateral-system-password +WALLET_ORACLE_APP_USER=payguard_wallet +LOAN_ORACLE_APP_USER=payguard_loan +COLLATERAL_ORACLE_APP_USER=payguard_collateral +WALLET_ORACLE_APP_PASSWORD=change-wallet-app-password +LOAN_ORACLE_APP_PASSWORD=change-loan-app-password +COLLATERAL_ORACLE_APP_PASSWORD=change-collateral-app-password +WALLET_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1521/FREE +LOAN_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1522/FREE +COLLATERAL_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1523/FREE + +WALLET_SERVICE_PORT=8081 +LOAN_SERVICE_PORT=8082 +COLLATERAL_SERVICE_PORT=8083 + +KAFKA_IMAGE=apache/kafka:latest +KAFKA_PORT=9092 +KAFKA_CLUSTER_ID=MkU3OEVBNTcwNTJENDM2Qk +KAFKA_BOOTSTRAP_SERVERS=localhost:9092 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f2bbabf..0000000 --- a/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -# syntax=docker/dockerfile:1 - -# ─── Stage 1: Dependency Cache ──────────────────────────────────────────────── -FROM eclipse-temurin:25-jdk-alpine AS dependencies - -WORKDIR /app - -# Copy root and all module POM files for precise dependency caching -COPY pom.xml ./ -COPY payguard-proto/pom.xml ./payguard-proto/ -COPY payguard-api-gateway/pom.xml ./payguard-api-gateway/ -COPY payguard-wallet-service/pom.xml ./payguard-wallet-service/ -COPY payguard-loan-service/pom.xml ./payguard-loan-service/ -COPY payguard-collateral-service/pom.xml ./payguard-collateral-service/ - -COPY --chmod=0755 mvnw ./mvnw -COPY .mvn/ .mvn/ - -# Download dependencies offline to leverage Docker layer caching -RUN ./mvnw --batch-mode --no-transfer-progress dependency:go-offline -B || true - - -# ─── Stage 2: Application Build ─────────────────────────────────────────────── -FROM dependencies AS builder - -WORKDIR /app - -# Copy source code and configurations for all modules -COPY config/ ./config/ -COPY payguard-proto/ ./payguard-proto/ -COPY payguard-api-gateway/./payguard-api-gateway/ -COPY payguard-wallet-service/ ./payguard-wallet-service/ -COPY payguard-loan-service/ ./payguard-loan-service/ -COPY payguard-collateral-service/ ./payguard-collateral-service/ - -# Receive service name via Build Arg (defaults to wallet service) -ARG SERVICE_NAME=payguard-wallet-service - -# Build the target service package along with internal dependencies (-am) without running tests -RUN ./mvnw --batch-mode --no-transfer-progress clean package -pl :${SERVICE_NAME} -am -DskipTests - - -# ─── Stage 3: Runtime ───────────────────────────────────────────────────────── -FROM eclipse-temurin:25-jre-alpine AS runtime - -WORKDIR /app - -# High security: Create a non-root system user and isolate the data directory -RUN addgroup -S payguard \ - && adduser -S payguard -G payguard \ - && mkdir -p /var/lib/payguard \ - && chown -R payguard:payguard /var/lib/payguard - -ARG SERVICE_NAME=payguard-wallet-service - -# Copy the output JAR file from the build stage to the lightweight JRE runtime image -COPY --from=builder --chown=payguard:payguard /app/${SERVICE_NAME}/target/*.jar ./app.jar - -# Run the container as a non-privileged user -USER payguard - -EXPOSE 8080 - -# Java 25 runtime optimizations -ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 8e2bbd6..d69e8f3 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,9 @@ It is not a payment-card switch or a full core-banking platform. The scope is in | Service | Responsibility | Persistence | Current State | |---|---|---|---| -| `wallet-service` | Account balances, double-entry ledger, holds/captures | PostgreSQL/Oracle via **jOOQ** (append-only, no ORM overhead) | In development | -| `loan-service` | Loan lifecycle, amortization schedules, repayments | Oracle via JPA/Hibernate | In development | -| `collateral-service` | Collateral locking, price valuation, LTV monitoring, liquidation | Oracle via JPA/Hibernate | In development | +| `wallet-service` | Account balances, double-entry ledger, holds/captures | Dedicated Oracle database via **jOOQ** (append-only, no ORM overhead) | In development | +| `loan-service` | Loan lifecycle, amortization schedules, repayments | Dedicated Oracle database via JPA/Hibernate | In development | +| `collateral-service` | Collateral locking, price valuation, LTV monitoring, liquidation | Dedicated Oracle database via JPA/Hibernate | In development | | `api-gateway` | Single entry point; REST from clients, gRPC to internal services | — | Planned | > `wallet-service` deliberately uses jOOQ instead of JPA: a ledger is append-only, and jOOQ gives explicit control over `INSERT`-only SQL with no risk of an accidental `UPDATE` slipping in through dirty-checking. `loan-service` and `collateral-service` manage mutable, CRUD-shaped state, where JPA is a better fit. @@ -127,7 +127,7 @@ flowchart LR ### Dependency Rules - Domain code has no Spring, persistence, or HTTP dependencies — invariants (e.g. debit = credit) are testable without a running application. -- Each service owns its own database schema; no service queries another's tables directly. +- Each service owns its own Oracle instance, credentials, volume, and schema; no service queries another's tables directly. - Cross-service calls happen only through the declared client ports (`WalletServiceClient`, `LoanServiceClient`, `CollateralServiceClient`), each with a REST adapter today and a gRPC adapter available for the gateway path. - Dependencies point inward, toward the domain core. @@ -183,16 +183,26 @@ cd payguard Create a `.env` file in the repository root: ```env -# Oracle +# Independent Oracle databases (copy `.env.example` to `.env` and set secrets) ORACLE_IMAGE=gvenzl/oracle-free:23-slim-faststart -ORACLE_HOST_PORT=1521 -ORACLE_PASSWORD=change-me-for-local-development - -# Redis -REDIS_IMAGE=redis:7-alpine -REDIS_HOST_PORT=6379 +WALLET_ORACLE_PORT=1521 +LOAN_ORACLE_PORT=1522 +COLLATERAL_ORACLE_PORT=1523 +WALLET_ORACLE_SYSTEM_PASSWORD=change-wallet-system-password +LOAN_ORACLE_SYSTEM_PASSWORD=change-loan-system-password +COLLATERAL_ORACLE_SYSTEM_PASSWORD=change-collateral-system-password +WALLET_ORACLE_APP_USER=payguard_wallet +LOAN_ORACLE_APP_USER=payguard_loan +COLLATERAL_ORACLE_APP_USER=payguard_collateral +WALLET_ORACLE_APP_PASSWORD=change-wallet-app-password +LOAN_ORACLE_APP_PASSWORD=change-loan-app-password +COLLATERAL_ORACLE_APP_PASSWORD=change-collateral-app-password +WALLET_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1521/FREE +LOAN_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1522/FREE +COLLATERAL_ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1523/FREE # Kafka (KRaft mode — no Zookeeper) +KAFKA_IMAGE=apache/kafka:latest KAFKA_CLUSTER_ID=replace-with-a-generated-cluster-id # Per-service ports diff --git a/docker-compose.yaml b/docker-compose.yaml index 6bb8e6b..b269e06 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,41 +1,63 @@ name: ${COMPOSE_PROJECT_NAME} services: - oracle: - image: gvenzl/oracle-free:23-slim-faststart - container_name: payguard-oracle + oracle-wallet: + image: ${ORACLE_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-oracle-wallet ports: - - "${ORACLE_PORT}:1521" + - "${WALLET_ORACLE_PORT}:1521" environment: - ORACLE_PASSWORD: ${ORACLE_PASSWORD} - APP_USER: ${ORACLE_APP_USER} - APP_PASSWORD: ${ORACLE_APP_PASSWORD} + ORACLE_PASSWORD: ${WALLET_ORACLE_SYSTEM_PASSWORD} + APP_USER: ${WALLET_ORACLE_APP_USER} + APP_PASSWORD: ${WALLET_ORACLE_APP_PASSWORD} healthcheck: test: ["CMD-SHELL", "/opt/oracle/checkDBStatus.sh"] interval: 10s timeout: 5s - retries: 10 - start_period: 30s + retries: 20 + start_period: 45s volumes: - - oracle_data:/opt/oracle/oradata + - oracle_wallet_data:/opt/oracle/oradata - redis: - image: redis:7-alpine - container_name: payguard-redis + oracle-loan: + image: ${ORACLE_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-oracle-loan ports: - - "${REDIS_PORT}:6379" + - "${LOAN_ORACLE_PORT}:1521" + environment: + ORACLE_PASSWORD: ${LOAN_ORACLE_SYSTEM_PASSWORD} + APP_USER: ${LOAN_ORACLE_APP_USER} + APP_PASSWORD: ${LOAN_ORACLE_APP_PASSWORD} healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - start_period: 5s + test: ["CMD-SHELL", "/opt/oracle/checkDBStatus.sh"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 45s volumes: - - redis_data:/data + - oracle_loan_data:/opt/oracle/oradata + + oracle-collateral: + image: ${ORACLE_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-oracle-collateral + ports: + - "${COLLATERAL_ORACLE_PORT}:1521" + environment: + ORACLE_PASSWORD: ${COLLATERAL_ORACLE_SYSTEM_PASSWORD} + APP_USER: ${COLLATERAL_ORACLE_APP_USER} + APP_PASSWORD: ${COLLATERAL_ORACLE_APP_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "/opt/oracle/checkDBStatus.sh"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 45s + volumes: + - oracle_collateral_data:/opt/oracle/oradata kafka: - image: apache/kafka:latest - container_name: payguard-kafka + image: ${KAFKA_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-kafka ports: - "${KAFKA_PORT}:9092" environment: @@ -57,12 +79,61 @@ services: test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:29092 --list"] interval: 10s timeout: 5s - retries: 5 - start_period: 15s + retries: 10 + start_period: 20s volumes: - kafka_data:/tmp/kraft-combined-logs + wallet-service: + build: + context: . + dockerfile: payguard-wallet-service/Dockerfile + container_name: ${COMPOSE_PROJECT_NAME}-wallet-service + ports: + - "${WALLET_SERVICE_PORT}:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:oracle:thin:@oracle-wallet:1521/FREE + SPRING_DATASOURCE_USERNAME: ${WALLET_ORACLE_APP_USER} + SPRING_DATASOURCE_PASSWORD: ${WALLET_ORACLE_APP_PASSWORD} + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + depends_on: + oracle-wallet: { condition: service_healthy } + kafka: { condition: service_healthy } + + loan-service: + build: + context: . + dockerfile: payguard-loan-service/Dockerfile + container_name: ${COMPOSE_PROJECT_NAME}-loan-service + ports: + - "${LOAN_SERVICE_PORT}:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:oracle:thin:@oracle-loan:1521/FREE + SPRING_DATASOURCE_USERNAME: ${LOAN_ORACLE_APP_USER} + SPRING_DATASOURCE_PASSWORD: ${LOAN_ORACLE_APP_PASSWORD} + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + depends_on: + oracle-loan: { condition: service_healthy } + kafka: { condition: service_healthy } + + collateral-service: + build: + context: . + dockerfile: payguard-collateral-service/Dockerfile + container_name: ${COMPOSE_PROJECT_NAME}-collateral-service + ports: + - "${COLLATERAL_SERVICE_PORT}:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:oracle:thin:@oracle-collateral:1521/FREE + SPRING_DATASOURCE_USERNAME: ${COLLATERAL_ORACLE_APP_USER} + SPRING_DATASOURCE_PASSWORD: ${COLLATERAL_ORACLE_APP_PASSWORD} + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + depends_on: + oracle-collateral: { condition: service_healthy } + kafka: { condition: service_healthy } + volumes: - oracle_data: - redis_data: + oracle_wallet_data: + oracle_loan_data: + oracle_collateral_data: kafka_data: diff --git a/payguard-collateral-service/Dockerfile b/payguard-collateral-service/Dockerfile new file mode 100644 index 0000000..fd7502c --- /dev/null +++ b/payguard-collateral-service/Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /workspace +COPY . . +RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-collateral-service -am -DskipTests package + +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +RUN addgroup -S payguard && adduser -S payguard -G payguard +COPY --from=build /workspace/payguard-collateral-service/target/payguard-collateral-service-*.jar /app/app.jar +USER payguard +EXPOSE 8080 +ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] diff --git a/payguard-collateral-service/README.md b/payguard-collateral-service/README.md new file mode 100644 index 0000000..7f1c486 --- /dev/null +++ b/payguard-collateral-service/README.md @@ -0,0 +1,5 @@ +# Collateral service + +The service owns cash collateral locks, valuation history, LTV thresholds, margin calls, liquidation authorization, retention metadata, audit records, and outbox publication. Price and liquidation integrations are ports; the portfolio profile supplies a simulated foreign-currency oracle and execution adapter. + +Build independently from the repository root with `docker build -f payguard-collateral-service/Dockerfile -t payguard/collateral-service .`. Run it with `SPRING_DATASOURCE_URL="$COLLATERAL_ORACLE_JDBC_URL"`, `SPRING_DATASOURCE_USERNAME="$COLLATERAL_ORACLE_APP_USER"`, and `SPRING_DATASOURCE_PASSWORD="$COLLATERAL_ORACLE_APP_PASSWORD"` from `.env`; its Flyway schema is applied only to the collateral-owned Oracle instance. diff --git a/payguard-loan-service/Dockerfile b/payguard-loan-service/Dockerfile new file mode 100644 index 0000000..fe78d23 --- /dev/null +++ b/payguard-loan-service/Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /workspace +COPY . . +RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-loan-service -am -DskipTests package + +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +RUN addgroup -S payguard && adduser -S payguard -G payguard +COPY --from=build /workspace/payguard-loan-service/target/payguard-loan-service-*.jar /app/app.jar +USER payguard +EXPOSE 8080 +ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] diff --git a/payguard-loan-service/README.md b/payguard-loan-service/README.md new file mode 100644 index 0000000..6636c11 --- /dev/null +++ b/payguard-loan-service/README.md @@ -0,0 +1,5 @@ +# Loan service + +The service owns the `Loan` aggregate, schedules, Actual/365 accrual, repayment waterfall, retention metadata, immutable audit records, outbox publication, and idempotent Kafka consumers. It references opaque borrower and collateral identifiers and requests wallet settlement through an event/port boundary. + +Build independently from the repository root with `docker build -f payguard-loan-service/Dockerfile -t payguard/loan-service .`. Run it with `SPRING_DATASOURCE_URL="$LOAN_ORACLE_JDBC_URL"`, `SPRING_DATASOURCE_USERNAME="$LOAN_ORACLE_APP_USER"`, and `SPRING_DATASOURCE_PASSWORD="$LOAN_ORACLE_APP_PASSWORD"` from `.env`; its Flyway schema is applied only to the loan-owned Oracle instance. diff --git a/payguard-wallet-service/Dockerfile b/payguard-wallet-service/Dockerfile new file mode 100644 index 0000000..daa3720 --- /dev/null +++ b/payguard-wallet-service/Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /workspace +COPY . . +RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-wallet-service -am -DskipTests package + +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +RUN addgroup -S payguard && adduser -S payguard -G payguard +COPY --from=build /workspace/payguard-wallet-service/target/payguard-wallet-service-*.jar /app/app.jar +USER payguard +EXPOSE 8080 +ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] diff --git a/payguard-wallet-service/README.md b/payguard-wallet-service/README.md new file mode 100644 index 0000000..498b101 --- /dev/null +++ b/payguard-wallet-service/README.md @@ -0,0 +1,3 @@ +# Wallet service + +Build independently from the repository root with `docker build -f payguard-wallet-service/Dockerfile -t payguard/wallet-service .`. Run with `docker run --env-file .env -e SPRING_DATASOURCE_URL="$WALLET_ORACLE_JDBC_URL" -e SPRING_DATASOURCE_USERNAME="$WALLET_ORACLE_APP_USER" -e SPRING_DATASOURCE_PASSWORD="$WALLET_ORACLE_APP_PASSWORD" -p 8081:8080 payguard/wallet-service`.