diff --git a/.dockerignore b/.dockerignore index 61c00af..58e116d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,16 +10,27 @@ **/target **/__pycache__ +**/.gradle +**/build + # Only the four packaged service JARs enter application build contexts. -!payguard-identity/target/ -payguard-identity/target/* -!payguard-identity/target/payguard-identity-1.0.0-SNAPSHOT.jar -!payguard-wallet/target/ -payguard-wallet/target/* -!payguard-wallet/target/payguard-wallet-1.0.0-SNAPSHOT.jar -!payguard-loan/target/ -payguard-loan/target/* -!payguard-loan/target/payguard-loan-1.0.0-SNAPSHOT.jar -!payguard-collateral/target/ -payguard-collateral/target/* -!payguard-collateral/target/payguard-collateral-1.0.0-SNAPSHOT.jar +!payguard-identity/build/ +payguard-identity/build/* +!payguard-identity/build/libs/ +payguard-identity/build/libs/* +!payguard-identity/build/libs/payguard-identity-1.0.0-SNAPSHOT.jar +!payguard-wallet/build/ +payguard-wallet/build/* +!payguard-wallet/build/libs/ +payguard-wallet/build/libs/* +!payguard-wallet/build/libs/payguard-wallet-1.0.0-SNAPSHOT.jar +!payguard-loan/build/ +payguard-loan/build/* +!payguard-loan/build/libs/ +payguard-loan/build/libs/* +!payguard-loan/build/libs/payguard-loan-1.0.0-SNAPSHOT.jar +!payguard-collateral/build/ +payguard-collateral/build/* +!payguard-collateral/build/libs/ +payguard-collateral/build/libs/* +!payguard-collateral/build/libs/payguard-collateral-1.0.0-SNAPSHOT.jar diff --git a/.github/workflows/_database-security.yml b/.github/workflows/_database-security.yml index 832f37e..cbfd6c5 100644 --- a/.github/workflows/_database-security.yml +++ b/.github/workflows/_database-security.yml @@ -23,7 +23,7 @@ jobs: with: distribution: temurin java-version: '25' - cache: maven + cache: gradle - name: Install native Ansible dependencies and offline test dependencies run: | python3 -m venv /tmp/payguard-ansible @@ -38,7 +38,7 @@ jobs: - name: Verify source contracts and build applications run: | python3 -m unittest discover -s infrastructure/tests -v - ./mvnw -B -ntp -pl payguard-identity,payguard-wallet,payguard-loan,payguard-collateral -am test package + bash ./gradlew --no-daemon --build-cache :payguard-identity:test :payguard-wallet:test :payguard-loan:test :payguard-collateral:test :payguard-identity:bootJar :payguard-wallet:bootJar :payguard-loan:bootJar :payguard-collateral:bootJar - name: Prepare isolated fixture and allocate only new explicit volumes run: | ansible-playbook -i localhost, -c local infrastructure/operations/prepare.yml -e fresh_target=true @@ -73,7 +73,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: native-infrastructure-java-reports - path: '**/target/surefire-reports/TEST-*.xml' + path: '**/build/test-results/test/TEST-*.xml' if-no-files-found: warn - name: Stop only this isolated fixture if: always() diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml index a4b8b47..37513f0 100644 --- a/.github/workflows/_publish.yml +++ b/.github/workflows/_publish.yml @@ -12,27 +12,6 @@ jobs: - name: Checkout Repository uses: actions/checkout@v7 - - name: Set up JDK 25 - uses: actions/setup-java@v6 - with: - java-version: '25' - distribution: 'temurin' - cache: 'maven' - - - name: Compile and Package (Skip Tests) - # Tests are skipped because they already passed in the _quality workflow - run: ./mvnw clean package -DskipTests - - - name: Install Maven Modules for Image Builds - # Image builds run as separate Maven invocations and need reactor artifacts in the local repository. - run: ./mvnw -DskipTests -Dpmd.skip=true -Dspotbugs.skip=true install - - - name: Verify Spring Boot plugin resolution - run: | - ./mvnw -version - ./mvnw help:effective-pom -pl payguard-wallet \ - | grep -A8 -B2 spring-boot-maven-plugin - - name: Build Wallet Service Image run: docker build -f payguard-wallet/Dockerfile -t payguard/wallet-service:${{ github.sha }} . diff --git a/.github/workflows/_quality.yml b/.github/workflows/_quality.yml index be14bb5..4fdc14d 100644 --- a/.github/workflows/_quality.yml +++ b/.github/workflows/_quality.yml @@ -20,28 +20,10 @@ jobs: with: java-version: '25' distribution: 'temurin' - cache: 'maven' + cache: 'gradle' - # 1. Spotless: Verifies code formatting without mutating the checkout - - name: Run Spotless - run: ./mvnw spotless:check + - name: Verify pre-push hook + run: python3 -m unittest discover -s git-hook -p 'test_*.py' -v - # 2. CheckStyle: Enforces coding standards - - name: Run CheckStyle - run: ./mvnw checkstyle:check - - # Install reactor modules so independent analysis invocations can resolve local artifacts. - - name: Install Maven Modules - run: ./mvnw -DskipTests -Dpmd.skip=true -Dspotbugs.skip=true install - - # 3. PMD: Scans for programming flaws - - name: Run PMD - run: ./mvnw pmd:check - - # 4. SpotBugs: Looks for bugs in Java bytecode - - name: Run SpotBugs - run: ./mvnw spotbugs:check - - # 5. Unit Tests: Ensures the core domain logic is intact - - name: Run Unit Tests - run: ./mvnw clean verify + - name: Run formatting, static analysis, and tests + run: bash ./gradlew --no-daemon --build-cache check diff --git a/.gitignore b/.gitignore index 55e334c..273f0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ # Generated build output target/ +build/ +.gradle/ # Local workflow guidance GIT_WORKFLOW_CONVENTION.md @@ -18,4 +20,4 @@ CLAUDE.md payguard-identity/NEW IDEAS.MD payguard-identity/ARCHITECTURE.md payguard-identity/IDENTITY_CHECKLIST.MD -NO_LLM_FOR_CODING.MD \ No newline at end of file +NO_LLM_FOR_CODING.MD diff --git a/README.md b/README.md index 1f7c53b..2ccb6b4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ First clone the repository and build it: ```bash git clone https://github.com//payguard.git cd payguard -./mvnw -B -ntp clean verify +bash ./gradlew --build-cache build ``` Then start the infrastructure (PostgreSQL and Vault) and the applications: @@ -209,15 +209,23 @@ Each service is an independently deployable module. ## Requirements -PayGuard runs on the JVM. Use JDK **25**, Docker and Compose, Ansible and OpenSSL. The Maven wrapper is included. +PayGuard runs on the JVM. Use JDK **25**, Docker and Compose, Ansible and OpenSSL. The Gradle wrapper is included. Database integration tests start real PostgreSQL containers. ## Build and local rehearsal ```bash -./mvnw -B -ntp clean verify +bash ./gradlew --build-cache build ``` +Install the repository's pre-push hook once: + +```bash +bash git-hook/install.sh +``` + +The hook validates committed changes. Commit build and workflow changes before pushing. + For the Compose, Vault Agent, Ansible and Flyway CLI deployment, follow [the infrastructure runbook](docs/infrastructure.md). It gives the exact preparation, initialization, migration and test commands. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..891da25 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,214 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import com.github.spotbugs.snom.SpotBugsExtension +import com.github.spotbugs.snom.SpotBugsTask + +plugins { + base + alias(libs.plugins.boot.application) apply false + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) apply false +} + +spotless { + format("buildFiles") { + target("*.gradle.kts", "gradle/*.toml", "buildSrc/**/*.gradle.kts", "payguard-*/build.gradle.kts", "payguard-*/settings.gradle.kts") + targetExclude("buildSrc/build/**", "buildSrc/.gradle/**") + trimTrailingWhitespace() + endWithNewline() + } +} + +subprojects { + pluginManager.withPlugin("java-library") { + dependencies { + "testRuntimeOnly"(rootProject.libs.org.junit.platform.junit.platform.launcher) + } + tasks.withType().configureEach { + useJUnitPlatform() + } + } + + pluginManager.withPlugin(rootProject.libs.plugins.spotless.get().pluginId) { + extensions.configure { + java { + target("src/**/*.java") + targetExclude("src/**/generated/**") + googleJavaFormat(rootProject.libs.versions.google.java.format.get()).aosp() + formatAnnotations() + } + } + } + + pluginManager.withPlugin("checkstyle") { + extensions.configure { + toolVersion = rootProject.libs.versions.checkstyle.get() + configFile = rootProject.file("config/checkstyle/checkstyle.xml") + } + tasks.withType().configureEach { + exclude { "/generated/" in it.file.invariantSeparatorsPath } + } + } + + pluginManager.withPlugin("pmd") { + extensions.configure { + toolVersion = rootProject.libs.versions.pmd.get() + ruleSets = listOf("category/java/errorprone.xml") + } + tasks.withType().configureEach { + exclude { "/generated/" in it.file.invariantSeparatorsPath } + } + } + + pluginManager.withPlugin(rootProject.libs.plugins.spotbugs.get().pluginId) { + extensions.configure { + toolVersion.set(rootProject.libs.versions.spotbugs.asProvider()) + excludeFilter.set(rootProject.file("config/spotbugs/exclude.xml")) + } + tasks.withType().configureEach { + reports.create("html") { required.set(true) } + } + } +} + +// Share declarations only among the modules that already used them, preserving their scopes. +configure(listOf( + project(":payguard-collateral"), + project(":payguard-gateway"), + project(":payguard-identity"), + project(":payguard-loan"), + project(":payguard-shared"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.actuator) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-gateway"), + project(":payguard-identity"), + project(":payguard-loan"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.webmvc) + "testImplementation"(rootProject.libs.org.springframework.boot.spring.boot.starter.webmvc.test) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-identity"), + project(":payguard-loan"), + project(":payguard-shared"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "testImplementation"(rootProject.libs.org.flywaydb.flyway.database.postgresql) + "testImplementation"(rootProject.libs.org.testcontainers.testcontainers.postgresql) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-identity"), + project(":payguard-loan"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "runtimeOnly"(rootProject.libs.org.postgresql.postgresql) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-identity"), + project(":payguard-loan"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.kafka) + "testImplementation"(rootProject.libs.org.springframework.boot.spring.boot.starter.flyway) + "testImplementation"(rootProject.libs.org.testcontainers.testcontainers.junit.jupiter) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-loan"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(project(":payguard-shared")) + } + } +} + +configure(listOf( + project(":payguard-gateway"), + project(":payguard-identity"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.validation) + } + } +} + +configure(listOf( + project(":payguard-collateral"), + project(":payguard-loan"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.data.jpa) + } + } +} + +configure(listOf( + project(":payguard-identity"), + project(":payguard-wallet"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "api"(rootProject.libs.org.springframework.boot.spring.boot.starter.jooq) + "api"(rootProject.libs.org.springframework.modulith.spring.modulith.starter.core) + "testImplementation"(rootProject.libs.org.springframework.modulith.spring.modulith.starter.test) + } + } +} + +configure(listOf( + project(":payguard-proto"), + project(":payguard-shared"), +)) { + pluginManager.withPlugin("java-library") { + dependencies { + "testImplementation"(rootProject.libs.org.springframework.boot.spring.boot.starter.test) + } + } +} + +configure(listOf(project(":payguard-identity"), project(":payguard-shared"))) { + pluginManager.withPlugin("java-library") { + dependencies { + "testImplementation"(rootProject.libs.org.postgresql.postgresql) + } + } +} + +tasks.named("check") { + dependsOn(subprojects.map { "${it.path}:check" }) +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..0187847 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Support convention plugins written in Kotlin. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. + `kotlin-dsl` +} + +repositories { + // Use the plugin portal to apply community plugins in convention plugins. + gradlePluginPortal() +} diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts new file mode 100644 index 0000000..4234c0c --- /dev/null +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -0,0 +1,33 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + `java-library` + `maven-publish` +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml new file mode 100644 index 0000000..cbe4355 --- /dev/null +++ b/config/spotbugs/exclude.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/docs/infrastructure.md b/docs/infrastructure.md index d4e47d7..09b178c 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -166,7 +166,7 @@ Build the committed service modules with JDK 25, then migrate each database expl This branch retains the existing module names and application persistence implementations: ```bash -./mvnw -B -ntp -pl payguard-identity,payguard-wallet,payguard-loan,payguard-collateral -am package +bash ./gradlew --build-cache :payguard-identity:bootJar :payguard-wallet:bootJar :payguard-loan:bootJar :payguard-collateral:bootJar for service in identity wallet loan collateral; do docker compose run --rm --no-deps operations infrastructure/operations/migrate.yml -e service="$service" done diff --git a/git-hook/install.sh b/git-hook/install.sh new file mode 100644 index 0000000..37abec6 --- /dev/null +++ b/git-hook/install.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" +hook_path="$(git rev-parse --git-path hooks/pre-push)" +mkdir -p "$(dirname "$hook_path")" +cat > "$hook_path" <<'HOOK' +#!/usr/bin/env bash +set -euo pipefail +exec bash "$(git rev-parse --show-toplevel)/git-hook/pre-push.sh" "$@" +HOOK +chmod +x "$hook_path" +echo "Installed pre-push loader at $hook_path" diff --git a/git-hook/pre-push.sh b/git-hook/pre-push.sh index a7d97c7..fe33b5f 100755 --- a/git-hook/pre-push.sh +++ b/git-hook/pre-push.sh @@ -1,29 +1,162 @@ #!/usr/bin/env bash +# Validate exactly the commits Git is about to push, not staged/uncommitted files. set -euo pipefail repository_root="$(git rev-parse --show-toplevel)" cd "$repository_root" +remote_name="${1:-origin}" -if ! command -v act >/dev/null 2>&1; then - echo "ERROR: act is required before pushing. Install nektos/act and Docker." >&2 - exit 1 -fi +scratch_directory="$(mktemp -d "${TMPDIR:-/tmp}/payguard-pre-push.XXXXXX")" +checkout_directory="$scratch_directory/checkout" -if ! docker info >/dev/null 2>&1; then - echo "ERROR: Docker must be running before pushing because act executes the PR workflow locally." >&2 - exit 1 -fi +cleanup() { + git worktree remove --force "$checkout_directory" >/dev/null 2>&1 || true + rm -rf -- "$scratch_directory" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM -echo "Running every pull-request workflow locally before push..." -temporary_worktree="$(mktemp -d "${TMPDIR:-/tmp}/payguard-pre-push.XXXXXX")" +# For a new remote branch, compare its commits with the remote's default branch. +# If that baseline is unavailable, use the full workflow instead of guessing. +find_new_branch_base() { + local pushed_commit="$1" + local candidate_ref candidate_commit -cleanup() { - git worktree remove --force "$temporary_worktree" >/dev/null 2>&1 || true + for candidate_ref in \ + "refs/remotes/$remote_name/HEAD" \ + "refs/remotes/$remote_name/main" \ + "refs/remotes/$remote_name/master"; do + candidate_commit="$(git rev-parse -q --verify "${candidate_ref}^{commit}" 2>/dev/null || true)" + if [[ -n "$candidate_commit" ]]; then + if git merge-base "$candidate_commit" "$pushed_commit"; then + return 0 + fi + fi + done + + return 1 } -trap cleanup EXIT INT TERM +run_full_workflow() { + if ! command -v act >/dev/null 2>&1; then + echo "ERROR: act is required for full validation." >&2 + return 1 + fi + if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then + echo "ERROR: Docker must be running for the full act workflow." >&2 + return 1 + fi + + # Leave act's cache server enabled for workflows that use actions/cache. + (cd "$checkout_directory" && act pull_request --container-architecture linux/amd64) +} + +# Git supplies one line per ref: local-ref local-oid remote-ref remote-oid. +declare -a local_refs=() local_commits=() remote_commits=() +while read -r local_ref local_commit remote_ref remote_commit; do + [[ -n "${local_commit:-}" ]] || continue + if [[ "$local_ref" == "(delete)" || "$local_commit" =~ ^0+$ ]]; then + echo "Skipping deleted ref $remote_ref." + continue + fi + + local_refs+=("$local_ref") + local_commits+=("$local_commit") + remote_commits+=("$remote_commit") +done + +if [[ ${#local_commits[@]} -eq 0 ]]; then + echo "No commits to validate." + exit 0 +fi + +for index in "${!local_commits[@]}"; do + local_ref="${local_refs[$index]}" + local_commit="${local_commits[$index]}" + remote_commit="${remote_commits[$index]}" + full_reason="" + base_commit="" + + echo "Checking pushed ref $local_ref ($local_commit)..." + + if [[ "${PAYGUARD_FULL_CHECK:-0}" == "1" ]]; then + full_reason="explicit full-validation request" + elif [[ "$local_ref" != refs/heads/* && "$local_ref" != "HEAD" ]]; then + full_reason="tag or non-branch ref" + elif [[ "$remote_commit" =~ ^0+$ ]]; then + base_commit="$(find_new_branch_base "$local_commit")" || full_reason="no reliable baseline for the new remote branch" + elif git cat-file -e "${remote_commit}^{commit}" 2>/dev/null; then + base_commit="$(git rev-parse "${remote_commit}^{commit}")" + else + full_reason="remote commit is not available locally" + fi + + declare -A changed_modules=() + if [[ -z "$full_reason" ]]; then + if ! git diff --no-renames --name-only -z "$base_commit" "$local_commit" > "$scratch_directory/changes"; then + full_reason="unable to calculate pushed changes" + else + while IFS= read -r -d '' changed_path; do + case "$changed_path" in + payguard-wallet/*|payguard-identity/*|payguard-loan/*|payguard-collateral/*|payguard-gateway/*) + changed_modules["${changed_path%%/*}"]=1 + ;; + payguard-shared/*|payguard-proto/*) + full_reason="shared code or protocol definitions changed" + ;; + *) + full_reason="repository-wide or unclassified change: $changed_path" + ;; + esac + done < "$scratch_directory/changes" + fi + fi + + # Fixed ordering keeps the log and task invocation deterministic. + tasks=() + for module in payguard-identity payguard-wallet payguard-loan payguard-collateral payguard-gateway; do + if [[ -n "${changed_modules[$module]:-}" ]]; then + tasks+=(":${module}:check") + fi + done + + if [[ -z "$full_reason" && ${#tasks[@]} -eq 0 ]]; then + echo "No changed files requiring validation." + continue + fi + + # A detached worktree guarantees that checks use the pushed commit, not any + # additional modifications sitting in the user's current working tree. + git worktree add --detach "$checkout_directory" "$local_commit" >/dev/null + + if [[ ! -f "$checkout_directory/build.gradle.kts" ]]; then + echo "ERROR: pushed commit $local_commit has no root build.gradle.kts. Commit the Gradle migration and workflow changes before pushing." >&2 + exit 1 + fi + + if [[ -z "$full_reason" ]]; then + if [[ ! -f "$checkout_directory/settings.gradle.kts" && ! -f "$checkout_directory/settings.gradle" ]]; then + full_reason="no root multi-project Gradle settings file" + elif [[ ! -f "$checkout_directory/gradlew" ]] && ! command -v gradle >/dev/null 2>&1; then + full_reason="no Gradle wrapper or Gradle executable" + fi + fi + + if [[ -n "$full_reason" ]]; then + echo "Running full PR workflow ($full_reason)." + run_full_workflow + else + echo "Running selected Gradle checks with the local build cache: ${tasks[*]}" + if [[ -f "$checkout_directory/gradlew" ]]; then + (cd "$checkout_directory" && bash ./gradlew --build-cache "${tasks[@]}") + else + (cd "$checkout_directory" && gradle --build-cache "${tasks[@]}") + fi + fi + + git worktree remove --force "$checkout_directory" >/dev/null +done -git worktree add --detach "$temporary_worktree" HEAD >/dev/null -cd "$temporary_worktree" -act pull_request --container-architecture linux/amd64 --no-cache-server +echo "Pre-push validation passed." diff --git a/git-hook/test_pre_push.py b/git-hook/test_pre_push.py new file mode 100644 index 0000000..9064b29 --- /dev/null +++ b/git-hook/test_pre_push.py @@ -0,0 +1,91 @@ +"""Exercise the real hook in disposable repositories without Docker or Gradle.""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +HOOK_DIRECTORY = Path(__file__).resolve().parent + + +class PrePushTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="payguard-hook-test-") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.log = self.root / "calls.log" + self.env = dict(os.environ, HOOK_TEST_LOG=str(self.log)) + self.git("init", "-q") + self.git("config", "user.name", "Hook Test") + self.git("config", "user.email", "hook@example.invalid") + self.write("build.gradle.kts", "// fixture\n") + self.write("settings.gradle.kts", "// fixture\n") + self.write("payguard-wallet/change.txt", "committed\n") + self.write("git-hook/pre-push.sh", (HOOK_DIRECTORY / "pre-push.sh").read_text()) + self.write("gradlew", '#!/bin/bash\nprintf "gradle %s\\n" "$*" >> "$HOOK_TEST_LOG"\ncat payguard-wallet/change.txt >> "$HOOK_TEST_LOG"\nexit "${HOOK_TEST_EXIT:-0}"\n') + self.write("bin/act", '#!/bin/bash\nprintf "act %s\\n" "$*" >> "$HOOK_TEST_LOG"\nexit "${HOOK_TEST_EXIT:-0}"\n') + self.write("bin/docker", "#!/bin/bash\nexit 0\n") + for name in ("act", "docker"): + (self.root / "bin" / name).chmod(0o755) + self.env["PATH"] = str(self.root / "bin") + os.pathsep + os.environ["PATH"] + self.base = self.commit() + + def write(self, name, content): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + + def git(self, *arguments): + return subprocess.check_output(["git", *arguments], cwd=self.root, env=self.env, text=True).strip() + + def commit(self): + self.git("add", ".") + self.git("-c", "commit.gpgsign=false", "commit", "-qm", "fixture") + return self.git("rev-parse", "HEAD") + + def run_hook(self, commit, base=None): + return subprocess.run( + ["bash", str(HOOK_DIRECTORY / "pre-push.sh"), "origin"], + cwd=self.root, env=self.env, text=True, capture_output=True, + input=f"refs/heads/topic {commit} refs/heads/topic {base or self.base}\n", + ) + + def test_scoped_checks_use_pushed_commit_and_ignore_uncommitted_files(self): + self.write("payguard-wallet/change.txt", "pushed\n") + commit = self.commit() + self.write("payguard-wallet/change.txt", "uncommitted\n") + result = self.run_hook(commit) + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual("gradle --build-cache :payguard-wallet:check\npushed\n", self.log.read_text()) + self.assertEqual("uncommitted\n", (self.root / "payguard-wallet/change.txt").read_text()) + + def test_shared_changes_run_full_workflow(self): + self.write("payguard-shared/change.txt", "shared\n") + result = self.run_hook(self.commit()) + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("act pull_request --container-architecture linux/amd64", self.log.read_text()) + + def test_missing_committed_gradle_build_fails_with_actionable_message(self): + (self.root / "build.gradle.kts").unlink() + result = self.run_hook(self.commit()) + self.assertNotEqual(0, result.returncode) + self.assertIn("Commit the Gradle migration", result.stderr) + self.assertFalse(self.log.exists()) + + def test_failed_gradle_check_blocks_push(self): + self.write("payguard-wallet/change.txt", "failing\n") + commit = self.commit() + self.env["HOOK_TEST_EXIT"] = "7" + result = self.run_hook(commit) + self.assertEqual(7, result.returncode, result.stderr) + self.assertNotIn("Pre-push validation passed", result.stdout) + + def test_installer_delegates_to_tracked_hook(self): + subprocess.run(["bash", str(HOOK_DIRECTORY / "install.sh")], cwd=self.root, env=self.env, check=True, capture_output=True) + result = subprocess.run([str(self.root / ".git/hooks/pre-push"), "origin"], cwd=self.root, env=self.env, input="", text=True, capture_output=True) + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("No commits to validate", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..c797ad1 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,133 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format + +[versions] +spotless-plugin = "8.11.0" +spotbugs-plugin = "6.5.13" +spotbugs = "4.10.5" +pmd = "7.29.0" +checkstyle = "14.4.0" +google-java-format = "1.36.1" +jetbrains-annotations = "26.1.0" +kotlin = "2.3.21" +evidence-spring-boot = "4.1.2" +spring-dependency-management = "1.1.7" +aws-sdk-kotlin = "1.9.28" +protobuf-plugin = "0.10.0" +com-google-protobuf-protobuf-java = "3.25.5" +com-google-protobuf-protobuf-java-util = "3.25.5" +com-redis-testcontainers-redis = "2.2.4" +io-grpc-grpc-netty-shaded = "1.83.1" +io-grpc-grpc-protobuf = "1.83.1" +io-grpc-grpc-stub = "1.83.1" +javax-annotation-javax-annotation-api = "1.3.2" +org-aspectj-aspectjweaver = "1.9.25.1" +org-flywaydb-flyway-core = "12.4.0" +org-flywaydb-flyway-database-postgresql = "12.4.0" +org-jspecify-jspecify = "1.0.1" +org-jooq = "3.21.7" +org-junit-platform-junit-platform-console-standalone = "6.0.3" +org-postgresql-postgresql = "42.7.13" +org-slf4j-slf4j-api = "2.0.18" +org-springdoc-springdoc-openapi-starter-webmvc-ui = "3.1.0" +org-springframework-boot-spring-boot-autoconfigure = "4.1.1" +org-springframework-boot-spring-boot-devtools = "4.1.1" +org-springframework-boot-spring-boot-starter-actuator = "4.1.1" +org-springframework-boot-spring-boot-starter-data-jpa = "4.1.1" +org-springframework-boot-spring-boot-starter-data-redis = "4.1.1" +org-springframework-boot-spring-boot-starter-flyway = "4.1.1" +org-springframework-boot-spring-boot-starter-jackson = "4.1.1" +org-springframework-boot-spring-boot-starter-jdbc = "4.1.1" +org-springframework-boot-spring-boot-starter-jooq = "4.1.1" +org-springframework-boot-spring-boot-starter-jooq-test = "4.1.1" +org-springframework-boot-spring-boot-starter-kafka = "4.1.1" +org-springframework-boot-spring-boot-starter-oauth2-resource-server = "4.1.1" +org-springframework-boot-spring-boot-starter-security = "4.1.1" +org-springframework-boot-spring-boot-starter-test = "4.1.1" +org-springframework-boot-spring-boot-starter-validation = "4.1.1" +org-springframework-boot-spring-boot-starter-webmvc = "4.1.1" +org-springframework-boot-spring-boot-starter-webmvc-test = "4.1.1" +org-springframework-grpc-spring-grpc-core = "1.1.1" +org-springframework-kafka-spring-kafka = "4.1.1" +org-springframework-modulith-spring-modulith-starter-core = "2.1.1" +org-springframework-modulith-spring-modulith-starter-core-x1 = "2.0.8" +org-springframework-modulith-spring-modulith-starter-test = "2.1.1" +org-springframework-modulith-spring-modulith-starter-test-x1 = "2.0.8" +org-springframework-security-spring-security-test = "7.1.1" +org-springframework-spring-context = "7.0.9" +org-testcontainers-testcontainers-junit-jupiter = "2.0.5" +org-testcontainers-testcontainers-postgresql = "2.0.5" +tools-jackson-core-jackson-databind = "3.1.5" + +[libraries] +org-junit-platform-junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "org-junit-platform-junit-platform-console-standalone" } +jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } +protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "com-google-protobuf-protobuf-java" } +grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "io-grpc-grpc-stub" } +aws-sdk-kotlin-s3 = { module = "aws.sdk.kotlin:s3", version.ref = "aws-sdk-kotlin" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } +kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor" } +spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" } +spring-boot-starter-r2dbc = { module = "org.springframework.boot:spring-boot-starter-r2dbc" } +evidence-spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } +evidence-jooq = { module = "org.jooq:jooq" } +jooq-kotlin-coroutines = { module = "org.jooq:jooq-kotlin-coroutines" } +r2dbc-postgresql = { module = "org.postgresql:r2dbc-postgresql" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +com-google-protobuf-protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "com-google-protobuf-protobuf-java" } +com-google-protobuf-protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "com-google-protobuf-protobuf-java-util" } +com-redis-testcontainers-redis = { module = "com.redis:testcontainers-redis", version.ref = "com-redis-testcontainers-redis" } +io-grpc-grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "io-grpc-grpc-netty-shaded" } +io-grpc-grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "io-grpc-grpc-protobuf" } +io-grpc-grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "io-grpc-grpc-stub" } +javax-annotation-javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-javax-annotation-api" } +org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj-aspectjweaver" } +org-flywaydb-flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "org-flywaydb-flyway-core" } +org-flywaydb-flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "org-flywaydb-flyway-database-postgresql" } +org-jspecify-jspecify = { module = "org.jspecify:jspecify", version.ref = "org-jspecify-jspecify" } +org-jooq-jooq = { module = "org.jooq:jooq", version.ref = "org-jooq" } +org-jooq-meta-extensions = { module = "org.jooq:jooq-meta-extensions", version.ref = "org-jooq" } +org-junit-platform-junit-platform-console-standalone = { module = "org.junit.platform:junit-platform-console-standalone", version.ref = "org-junit-platform-junit-platform-console-standalone" } +org-postgresql-postgresql = { module = "org.postgresql:postgresql", version.ref = "org-postgresql-postgresql" } +org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j-slf4j-api" } +org-springdoc-springdoc-openapi-starter-webmvc-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "org-springdoc-springdoc-openapi-starter-webmvc-ui" } +org-springframework-boot-spring-boot-autoconfigure = { module = "org.springframework.boot:spring-boot-autoconfigure", version.ref = "org-springframework-boot-spring-boot-autoconfigure" } +org-springframework-boot-spring-boot-devtools = { module = "org.springframework.boot:spring-boot-devtools", version.ref = "org-springframework-boot-spring-boot-devtools" } +org-springframework-boot-spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "org-springframework-boot-spring-boot-starter-actuator" } +org-springframework-boot-spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "org-springframework-boot-spring-boot-starter-data-jpa" } +org-springframework-boot-spring-boot-starter-data-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis", version.ref = "org-springframework-boot-spring-boot-starter-data-redis" } +org-springframework-boot-spring-boot-starter-flyway = { module = "org.springframework.boot:spring-boot-starter-flyway", version.ref = "org-springframework-boot-spring-boot-starter-flyway" } +org-springframework-boot-spring-boot-starter-jackson = { module = "org.springframework.boot:spring-boot-starter-jackson", version.ref = "org-springframework-boot-spring-boot-starter-jackson" } +org-springframework-boot-spring-boot-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "org-springframework-boot-spring-boot-starter-jdbc" } +org-springframework-boot-spring-boot-starter-jooq = { module = "org.springframework.boot:spring-boot-starter-jooq", version.ref = "org-springframework-boot-spring-boot-starter-jooq" } +org-springframework-boot-spring-boot-starter-jooq-test = { module = "org.springframework.boot:spring-boot-starter-jooq-test", version.ref = "org-springframework-boot-spring-boot-starter-jooq-test" } +org-springframework-boot-spring-boot-starter-kafka = { module = "org.springframework.boot:spring-boot-starter-kafka", version.ref = "org-springframework-boot-spring-boot-starter-kafka" } +org-springframework-boot-spring-boot-starter-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server", version.ref = "org-springframework-boot-spring-boot-starter-oauth2-resource-server" } +org-springframework-boot-spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "org-springframework-boot-spring-boot-starter-security" } +org-springframework-boot-spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "org-springframework-boot-spring-boot-starter-test" } +org-springframework-boot-spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "org-springframework-boot-spring-boot-starter-validation" } +org-springframework-boot-spring-boot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc", version.ref = "org-springframework-boot-spring-boot-starter-webmvc" } +org-springframework-boot-spring-boot-starter-webmvc-test = { module = "org.springframework.boot:spring-boot-starter-webmvc-test", version.ref = "org-springframework-boot-spring-boot-starter-webmvc-test" } +org-springframework-grpc-spring-grpc-core = { module = "org.springframework.grpc:spring-grpc-core", version.ref = "org-springframework-grpc-spring-grpc-core" } +org-springframework-kafka-spring-kafka = { module = "org.springframework.kafka:spring-kafka", version.ref = "org-springframework-kafka-spring-kafka" } +org-springframework-modulith-spring-modulith-starter-core = { module = "org.springframework.modulith:spring-modulith-starter-core", version.ref = "org-springframework-modulith-spring-modulith-starter-core" } +org-springframework-modulith-spring-modulith-starter-core-x1 = { module = "org.springframework.modulith:spring-modulith-starter-core", version.ref = "org-springframework-modulith-spring-modulith-starter-core-x1" } +org-springframework-modulith-spring-modulith-starter-test = { module = "org.springframework.modulith:spring-modulith-starter-test", version.ref = "org-springframework-modulith-spring-modulith-starter-test" } +org-springframework-modulith-spring-modulith-starter-test-x1 = { module = "org.springframework.modulith:spring-modulith-starter-test", version.ref = "org-springframework-modulith-spring-modulith-starter-test-x1" } +org-springframework-security-spring-security-test = { module = "org.springframework.security:spring-security-test", version.ref = "org-springframework-security-spring-security-test" } +org-springframework-spring-context = { module = "org.springframework:spring-context", version.ref = "org-springframework-spring-context" } +org-testcontainers-testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter", version.ref = "org-testcontainers-testcontainers-junit-jupiter" } +org-testcontainers-testcontainers-postgresql = { module = "org.testcontainers:testcontainers-postgresql", version.ref = "org-testcontainers-testcontainers-postgresql" } +tools-jackson-core-jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "tools-jackson-core-jackson-databind" } + +[plugins] +boot-application = { id = "org.springframework.boot", version.ref = "org-springframework-boot-spring-boot-autoconfigure" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless-plugin" } +spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs-plugin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +spring-boot = { id = "org.springframework.boot", version.ref = "evidence-spring-boot" } +spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } +jooq-codegen = { id = "org.jooq.jooq-codegen-gradle", version.ref = "org-jooq" } +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/infrastructure/application.Dockerfile b/infrastructure/application.Dockerfile index 52ce0c5..62c0ee5 100644 --- a/infrastructure/application.Dockerfile +++ b/infrastructure/application.Dockerfile @@ -1,9 +1,9 @@ -# Build the reactor outside this image; credentials never enter the build context. +# Build the executable JARs outside this image; credentials never enter the build context. FROM hashicorp/vault:2.1.2@sha256:c2f666266f383d2cf424d86b8bb8ce7d065562173ffec2b476d762943608bb55 AS vault FROM eclipse-temurin:25-jre-alpine@sha256:b7528962e65d43064d8a8b67ba175367896bc948363ea2d3fcd7f10d34e1f048 COPY --from=vault /bin/vault /usr/local/bin/vault ARG SERVICE WORKDIR /app -COPY payguard-${SERVICE}/target/payguard-${SERVICE}-1.0.0-SNAPSHOT.jar /app/service.jar +COPY payguard-${SERVICE}/build/libs/payguard-${SERVICE}-1.0.0-SNAPSHOT.jar /app/service.jar USER 1000:1000 ENTRYPOINT ["vault", "agent"] diff --git a/infrastructure/tests/test_contracts.py b/infrastructure/tests/test_contracts.py index df743a5..0bae628 100644 --- a/infrastructure/tests/test_contracts.py +++ b/infrastructure/tests/test_contracts.py @@ -5,7 +5,6 @@ import re import tomllib import unittest -import xml.etree.ElementTree as ET import yaml import hcl2 @@ -115,11 +114,13 @@ def test_no_retired_production_tooling(self): for endpoint in ["auth/approle/login", "auth/token/renew-self", "auth/token/revoke-self", "sys/leases"]: self.assertNotIn(endpoint, client) - def test_parent_reactor_modules_exist(self): - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - root = ET.parse(ROOT / "pom.xml").getroot() - for node in root.findall("m:modules/m:module", ns): - self.assertTrue((ROOT / node.text / "pom.xml").is_file()) + def test_gradle_modules_exist(self): + settings = (ROOT / "settings.gradle.kts").read_text() + modules = re.findall(r'^\s*include\(":([^\"]+)"\)\s*$', settings, re.MULTILINE) + self.assertTrue(modules, "No Gradle modules found in settings.gradle.kts") + for module in modules: + with self.subTest(module=module): + self.assertTrue((ROOT / module / "build.gradle.kts").is_file()) def test_production_brokers_require_authentication_tls_and_hostname_checks(self): class Loader(yaml.SafeLoader): diff --git a/payguard-collateral/Dockerfile b/payguard-collateral/Dockerfile index 7da8a1e..9a47a7b 100644 --- a/payguard-collateral/Dockerfile +++ b/payguard-collateral/Dockerfile @@ -2,12 +2,12 @@ FROM eclipse-temurin:25-jdk-alpine@sha256:3fd2d245c4e0eba615fe366a71b8bd25f5db7104f53e4026b24bf508b880bd2a AS build WORKDIR /workspace COPY . . -RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-collateral-service -am -DskipTests package +RUN bash ./gradlew --no-daemon --build-cache :payguard-collateral:bootJar FROM eclipse-temurin:25-jre-alpine@sha256:3c0a9084927a221ccd1d007fcaf614465672c0af37aaa834c5184483afe56d61 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 +COPY --from=build /workspace/payguard-collateral/build/libs/payguard-collateral-1.0.0-SNAPSHOT.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/build.gradle.kts b/payguard-collateral/build.gradle.kts new file mode 100644 index 0000000..48870e6 --- /dev/null +++ b/payguard-collateral/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + alias(libs.plugins.boot.application) + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "payguard-collateral" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/payguard-collateral/gradle.properties b/payguard-collateral/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-collateral/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-collateral/gradle/wrapper/gradle-wrapper.jar b/payguard-collateral/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-collateral/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-collateral/gradle/wrapper/gradle-wrapper.properties b/payguard-collateral/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-collateral/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-collateral/gradlew b/payguard-collateral/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-collateral/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-collateral/gradlew.bat b/payguard-collateral/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-collateral/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-collateral/settings.gradle.kts b/payguard-collateral/settings.gradle.kts new file mode 100644 index 0000000..bb62665 --- /dev/null +++ b/payguard-collateral/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-collateral" diff --git a/payguard-gateway/build.gradle.kts b/payguard-gateway/build.gradle.kts new file mode 100644 index 0000000..9ae93a5 --- /dev/null +++ b/payguard-gateway/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + alias(libs.plugins.boot.application) + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + api(project(":payguard-proto")) + implementation(libs.com.google.protobuf.protobuf.java.util) + api(libs.org.springframework.boot.spring.boot.starter.jackson) + api(libs.org.springframework.boot.spring.boot.starter.security) + api(libs.org.springframework.boot.spring.boot.starter.oauth2.resource.server) + api(libs.io.grpc.grpc.netty.shaded) + testImplementation(libs.org.springframework.security.spring.security.test) +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "payguard-gateway" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/payguard-gateway/gradle.properties b/payguard-gateway/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-gateway/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-gateway/gradle/wrapper/gradle-wrapper.jar b/payguard-gateway/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-gateway/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-gateway/gradle/wrapper/gradle-wrapper.properties b/payguard-gateway/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-gateway/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-gateway/gradlew b/payguard-gateway/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-gateway/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-gateway/gradlew.bat b/payguard-gateway/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-gateway/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-gateway/settings.gradle.kts b/payguard-gateway/settings.gradle.kts new file mode 100644 index 0000000..7bd77f8 --- /dev/null +++ b/payguard-gateway/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-gateway" diff --git a/payguard-identity/Dockerfile b/payguard-identity/Dockerfile index 14a7838..b7c2a11 100644 --- a/payguard-identity/Dockerfile +++ b/payguard-identity/Dockerfile @@ -2,12 +2,12 @@ FROM eclipse-temurin:25-jdk-alpine@sha256:3fd2d245c4e0eba615fe366a71b8bd25f5db7104f53e4026b24bf508b880bd2a AS build WORKDIR /workspace COPY . . -RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-identity-service -am -DskipTests package +RUN bash ./gradlew --no-daemon --build-cache :payguard-identity:bootJar FROM eclipse-temurin:25-jre-alpine@sha256:3c0a9084927a221ccd1d007fcaf614465672c0af37aaa834c5184483afe56d61 WORKDIR /app RUN addgroup -S payguard && adduser -S payguard -G payguard -COPY --from=build /workspace/payguard-identity-service/target/payguard-identity-service-*.jar /app/app.jar +COPY --from=build /workspace/payguard-identity/build/libs/payguard-identity-1.0.0-SNAPSHOT.jar /app/app.jar USER payguard EXPOSE 8084 -ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"] diff --git a/payguard-identity/README.md b/payguard-identity/README.md index 7be3824..cf2bdcc 100644 --- a/payguard-identity/README.md +++ b/payguard-identity/README.md @@ -21,6 +21,29 @@ Runtime credentials come from a service-scoped Vault AppRole and a rotating pool ## Verification and operations +### jOOQ code generation + +The generator is configured in `build.gradle.kts`; no XML file is needed. Its plugin and +runtime version share the `org-jooq` entry in the root `gradle/libs.versions.toml`, separately +from the Spring Boot starter version. + +Generation reads `src/main/resources/db/jooq/*.sql` in Flyway filename order using jOOQ's +`DDLDatabase`, preserving the former Maven profile. No running PostgreSQL instance or Vault +credentials are needed. Ansible, Vault Agent and Flyway CLI continue to own provisioning, +credentials and real database migrations separately. + +From the repository root: + +```bash +./gradlew :payguard-identity:generateJooq +``` + +This explicitly refreshes the existing generated package under `src/main/java`, mapping the +snapshot schema to `payguard`. Review generated changes before committing. Keep the snapshots +consistent with `src/main/resources/db/migration/postgresql` when changing the database schema; +generation does not inspect a live database or apply migrations. Normal compilation uses the +checked-in generated classes and does not run generation. + From the repository root, with JDK 25 and Docker: ```bash diff --git a/payguard-identity/build.gradle.kts b/payguard-identity/build.gradle.kts new file mode 100644 index 0000000..ea56d23 --- /dev/null +++ b/payguard-identity/build.gradle.kts @@ -0,0 +1,100 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + alias(libs.plugins.boot.application) + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) + alias(libs.plugins.jooq.codegen) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + api(libs.org.jooq.jooq) + + jooqCodegen(libs.org.jooq.meta.extensions) + + api(libs.org.springframework.boot.spring.boot.starter.data.redis) + api(libs.org.jspecify.jspecify) + implementation(project(":payguard-shared")) + testImplementation(libs.org.springframework.boot.spring.boot.starter.jooq.test) + testImplementation(libs.com.redis.testcontainers.redis) +} + +val spotbugsAnnotations = configurations.detachedConfiguration(libs.jetbrains.annotations.get()) +tasks.withType().configureEach { + auxClassPaths.from(spotbugsAnnotations) +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "PayGuard Identity Service" +java.sourceCompatibility = JavaVersion.VERSION_25 + + + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +jooq { + configuration { + generator { + database { + name = "org.jooq.meta.extensions.ddl.DDLDatabase" + properties { + property { + key = "scripts" + value = "${projectDir}/src/main/resources/db/jooq/*.sql" + } + property { + key = "sort" + value = "flyway" + } + property { + key = "defaultNameCase" + value = "lower" + } + } + schemata { + schema { + inputSchema = "PUBLIC" + outputSchema = "payguard" + } + } + } + target { + packageName = "dev.amg.payguard.identity.infrastructure.adapter.out.persistence.jooq.generated" + // Refresh the existing checked-in package without creating duplicate classes. + directory = "src/main/java" + } + } + } +} + +tasks.named("jooqCodegen") { + inputs.files(fileTree("src/main/resources/db/jooq") { include("*.sql") }) + // The output shares src/main/java with handwritten sources; never restore it from cache. + outputs.cacheIf { false } +} + +tasks.register("generateJooq") { + group = "code generation" + description = "Regenerates jOOQ sources from the Identity SQL snapshots." + dependsOn(tasks.named("jooqCodegen")) +} diff --git a/payguard-identity/gradle.properties b/payguard-identity/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-identity/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-identity/gradle/wrapper/gradle-wrapper.jar b/payguard-identity/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-identity/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-identity/gradle/wrapper/gradle-wrapper.properties b/payguard-identity/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-identity/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-identity/gradlew b/payguard-identity/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-identity/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-identity/gradlew.bat b/payguard-identity/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-identity/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-identity/pom.xml b/payguard-identity/pom.xml deleted file mode 100644 index 6f9c237..0000000 --- a/payguard-identity/pom.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - - 4.0.0 - - - dev.amg.payguard - payguard-parent - 1.0.0-SNAPSHOT - - - payguard-identity - PayGuard Identity Service - - - 2.1.1 - - - - - - org.springframework.modulith - spring-modulith-bom - ${spring-modulith.version} - pom - import - - - - - - - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.boot - spring-boot-starter-webmvc - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-starter-jooq - - - - org.postgresql - postgresql - runtime - - - - - org.springframework.boot - spring-boot-starter-flyway - test - - - - org.flywaydb - flyway-database-postgresql - test - - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - - org.springframework.boot - spring-boot-starter-kafka - - - - - org.jspecify - jspecify - - - - - org.springframework.modulith - spring-modulith-starter-core - - - - - dev.amg.payguard - payguard-shared - 1.0.0-SNAPSHOT - - - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - - org.springframework.boot - spring-boot-starter-jooq-test - test - - - - org.testcontainers - testcontainers-postgresql - test - - - - com.redis - testcontainers-redis - test - - - - org.testcontainers - testcontainers-junit-jupiter - test - - - - org.springframework.modulith - spring-modulith-starter-test - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - repackage - - repackage - - - - - - - - - jooq-codegen - - - - - org.jooq - jooq-codegen-maven - 3.21.7 - - - - generate-jooq - generate-sources - - generate - - - - - - - org.jooq - jooq-meta-extensions - 3.21.7 - - - - - - - org.jooq.meta.extensions.ddl.DDLDatabase - - - scripts - ${project.basedir}/src/main/resources/db/jooq/*.sql - - - sort - flyway - - - defaultNameCase - lower - - - - - PUBLIC - payguard - - - - - - dev.amg.payguard.identity.infrastructure.adapter.out.persistence.jooq.generated - ${project.basedir}/src/main/java - - - - - - - - - diff --git a/payguard-identity/settings.gradle.kts b/payguard-identity/settings.gradle.kts new file mode 100644 index 0000000..712df00 --- /dev/null +++ b/payguard-identity/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-identity" diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/IdentityApp.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/IdentityApp.java index 3e55086..2514ce6 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/IdentityApp.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/IdentityApp.java @@ -6,7 +6,7 @@ @SpringBootApplication public class IdentityApp { - public static void main(String[] args) { - SpringApplication.run(dev.amg.payguard.identity.IdentityApp.class, args); - } + public static void main(String[] args) { + SpringApplication.run(IdentityApp.class, args); + } } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/IdentityOutbox.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/IdentityOutbox.java index d89a90d..df0812b 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/IdentityOutbox.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/IdentityOutbox.java @@ -103,6 +103,16 @@ public Class getRecordType() { */ public final TableField ATTEMPT_COUNT = createField(DSL.name("attempt_count"), SQLDataType.INTEGER.nullable(false).defaultValue(DSL.field(DSL.raw("0"), SQLDataType.INTEGER)), this, ""); + /** + * The column payguard.identity_outbox.claimed_by. + */ + public final TableField CLAIMED_BY = createField(DSL.name("claimed_by"), SQLDataType.UUID, this, ""); + + /** + * The column payguard.identity_outbox.claim_until. + */ + public final TableField CLAIM_UNTIL = createField(DSL.name("claim_until"), SQLDataType.TIMESTAMPWITHTIMEZONE(6), this, ""); + private IdentityOutbox(Name alias, Table aliased) { this(alias, aliased, (Field[]) null, null); } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/records/IdentityOutboxRecord.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/records/IdentityOutboxRecord.java index 568be59..bc9739c 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/records/IdentityOutboxRecord.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/infrastructure/adapter/out/persistence/jooq/generated/tables/records/IdentityOutboxRecord.java @@ -148,6 +148,34 @@ public Integer getAttemptCount() { return (Integer) get(8); } + /** + * Setter for payguard.identity_outbox.claimed_by. + */ + public void setClaimedBy(UUID value) { + set(9, value); + } + + /** + * Getter for payguard.identity_outbox.claimed_by. + */ + public UUID getClaimedBy() { + return (UUID) get(9); + } + + /** + * Setter for payguard.identity_outbox.claim_until. + */ + public void setClaimUntil(OffsetDateTime value) { + set(10, value); + } + + /** + * Getter for payguard.identity_outbox.claim_until. + */ + public OffsetDateTime getClaimUntil() { + return (OffsetDateTime) get(10); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -171,7 +199,7 @@ public IdentityOutboxRecord() { /** * Create a detached, initialised IdentityOutboxRecord */ - public IdentityOutboxRecord(UUID eventId, String aggregateId, String eventType, JSON payload, OffsetDateTime occurredAt, OffsetDateTime createdAt, OffsetDateTime publishedAt, OffsetDateTime nextAttemptAt, Integer attemptCount) { + public IdentityOutboxRecord(UUID eventId, String aggregateId, String eventType, JSON payload, OffsetDateTime occurredAt, OffsetDateTime createdAt, OffsetDateTime publishedAt, OffsetDateTime nextAttemptAt, Integer attemptCount, UUID claimedBy, OffsetDateTime claimUntil) { super(IdentityOutbox.IDENTITY_OUTBOX); setEventId(eventId); @@ -183,6 +211,8 @@ public IdentityOutboxRecord(UUID eventId, String aggregateId, String eventType, setPublishedAt(publishedAt); setNextAttemptAt(nextAttemptAt); setAttemptCount(attemptCount); + setClaimedBy(claimedBy); + setClaimUntil(claimUntil); resetTouchedOnNotNull(); } } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/issue_otp/IssueOtpCommandResponse.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/issue_otp/IssueOtpCommandResponse.java index 0d309ef..3ce3ffd 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/issue_otp/IssueOtpCommandResponse.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/issue_otp/IssueOtpCommandResponse.java @@ -15,7 +15,7 @@ public record IssueOtpCommandResponse( String sourceIp, Instant expiresAt) { public IssueOtpCommandResponse { - Objects.requireNonNull(otpId, "otpId must not be null"); + Objects.requireNonNull(otpId, "challengeId must not be null"); Objects.requireNonNull(purpose, "purpose must not be null"); Objects.requireNonNull(channel, "channel must not be null"); Objects.requireNonNull(expiresAt, "expiresAt must not be null"); diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationCommand.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationCommand.java index b56bb7b..82e551b 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationCommand.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationCommand.java @@ -17,7 +17,7 @@ public record OtpVerificationCommand( String sourceIp) { public OtpVerificationCommand { - requirePresent(otpId, "otpId"); + requirePresent(otpId, "challengeId"); requirePresent(customerId, "customerId"); requirePresent(purpose, "purpose"); @@ -52,7 +52,7 @@ public static OtpVerificationCommand of( @Override public String toString() { - return "OtpVerificationCommand[otpId=" + return "OtpVerificationCommand[challengeId=" + otpId + ", code=[REDACTED], deviceFingerprint=[REDACTED], sourceIp=[REDACTED]]"; } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationResponse.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationResponse.java index 6076a50..91a4357 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationResponse.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/command/otp_verification/OtpVerificationResponse.java @@ -13,7 +13,7 @@ public record OtpVerificationResponse(OtpId challengeId, OtpPurpose purpose, Instant verifiedAt) { public OtpVerificationResponse { - Objects.requireNonNull(challengeId, "otpId must not be null"); + Objects.requireNonNull(challengeId, "challengeId must not be null"); Objects.requireNonNull(purpose, "purpose must not be null"); Objects.requireNonNull(verifiedAt, "verifiedAt must not be null"); } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/OtpDeliveryRequest.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/OtpDeliveryRequest.java index 25d1728..ef25f49 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/OtpDeliveryRequest.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/OtpDeliveryRequest.java @@ -17,6 +17,6 @@ public record OtpDeliveryRequest( String code) { @Override public String toString() { - return "OtpDeliveryRequest[otpId=" + otpId + ", code=[REDACTED]]"; + return "OtpDeliveryRequest[challengeId=" + otpId + ", code=[REDACTED]]"; } } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAudit.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAudit.java index 57d9290..fbd1c93 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAudit.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAudit.java @@ -1,8 +1,5 @@ package dev.amg.payguard.identity.verification.application.port; -import org.springframework.stereotype.Service; - -@Service @FunctionalInterface public interface VerificationAudit { /** Persists synchronously; a persistence failure propagates to the caller. */ diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAuditEntry.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAuditEntry.java index b6af4ac..55a210a 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAuditEntry.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/application/port/VerificationAuditEntry.java @@ -9,9 +9,10 @@ * context is currently supplied by the inbound contract. */ public record VerificationAuditEntry( - OtpId challengeId, - CustomerId customerId, - VerificationAuditAction verificationAuditAction, - String reasonCode, - String sourceIp, - Instant occurredAt) {} + OtpId challengeId, // The generated OTP. + CustomerId customerId, // The person that OTP generated for + VerificationAuditAction verificationAuditAction, // What action happened to the OTP? + String reasonCode, // Code that identifies why an OTP was requested, generated, or validated. + String sourceIp, // IP address from which the OTP request originated. + Instant occurredAt +) {} diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpEvent.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpEvent.java index b4f1df8..26e10bf 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpEvent.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpEvent.java @@ -6,8 +6,10 @@ import dev.amg.payguard.identity.verification.domain.enums.OtpPurpose; import dev.amg.payguard.identity.verification.domain.enums.OtpVerificationResult; import java.time.Instant; +import java.util.UUID; public record OtpEvent( + UUID id, String type, OtpId challengeId, CustomerId customerId, diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpExpiredEvent.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpExpiredEvent.java index a8ee55b..e286585 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpExpiredEvent.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpExpiredEvent.java @@ -13,7 +13,7 @@ public record OtpExpiredEvent( implements DomainEvent { public OtpExpiredEvent { - Objects.requireNonNull(otpId, "otpId must not be null"); + Objects.requireNonNull(otpId, "challengeId must not be null"); Objects.requireNonNull(customerId, "customerId must not be null"); Objects.requireNonNull(purpose, "purpose must not be null"); Objects.requireNonNull(occurredOn, "occurredOn must not be null"); diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpInvalidatedEvent.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpInvalidatedEvent.java index 52d307a..aada3f2 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpInvalidatedEvent.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpInvalidatedEvent.java @@ -13,7 +13,7 @@ public record OtpInvalidatedEvent( implements DomainEvent { public OtpInvalidatedEvent { - Objects.requireNonNull(otpId, "otpId must not be null"); + Objects.requireNonNull(otpId, "challengeId must not be null"); Objects.requireNonNull(customerId, "customerId must not be null"); Objects.requireNonNull(purpose, "purpose must not be null"); Objects.requireNonNull(occurredOn, "occurredOn must not be null"); diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpLockedEvent.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpLockedEvent.java index c51c738..b2b3fff 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpLockedEvent.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/event/OtpLockedEvent.java @@ -13,7 +13,7 @@ public record OtpLockedEvent( implements DomainEvent { public OtpLockedEvent { - Objects.requireNonNull(otpId, "otpId must not be null"); + Objects.requireNonNull(otpId, "challengeId must not be null"); Objects.requireNonNull(customerId, "customerId must not be null"); Objects.requireNonNull(purpose, "purpose must not be null"); Objects.requireNonNull(occurredOn, "occurredOn must not be null"); diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpChannel.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpChannel.java similarity index 93% rename from payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpChannel.java rename to payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpChannel.java index 99299e4..d3ef231 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpChannel.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpChannel.java @@ -6,7 +6,7 @@ public sealed interface OtpChannel permits OtpChannel.Sms, OtpChannel.Email, OtpChannel.Push { OtpChannel SMS = new Sms(); OtpChannel EMAIL = new Email(); - OtpChannel PUSH = new Push(); + OtpChannel PUSH_NOTIFICATION = new Push(); @JsonValue String name(); @@ -16,7 +16,7 @@ static OtpChannel valueOf(String name) { return switch (name) { case "SMS" -> SMS; case "EMAIL" -> EMAIL; - case "PUSH" -> PUSH; + case "PUSH" -> PUSH_NOTIFICATION; default -> throw new IllegalArgumentException("Unknown OtpChannel: " + name); }; } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpPurpose.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpPurpose.java similarity index 85% rename from payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpPurpose.java rename to payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpPurpose.java index 97c8b63..bebab08 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpPurpose.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpPurpose.java @@ -5,14 +5,14 @@ public sealed interface OtpPurpose permits OtpPurpose.Login, - OtpPurpose.TransferConfirm, - OtpPurpose.LoanDisburse, + OtpPurpose.TransferConfirmation, + OtpPurpose.LoanDisbursement, OtpPurpose.CollateralRelease, OtpPurpose.CardIssuance, OtpPurpose.ProfileChange { OtpPurpose LOGIN = new Login(); - OtpPurpose TRANSFER_CONFIRM = new TransferConfirm(); - OtpPurpose LOAN_DISBURSE = new LoanDisburse(); + OtpPurpose TRANSFER_CONFIRM = new TransferConfirmation(); + OtpPurpose LOAN_DISBURSE = new LoanDisbursement(); OtpPurpose COLLATERAL_RELEASE = new CollateralRelease(); OtpPurpose CARD_ISSUANCE = new CardIssuance(); OtpPurpose PROFILE_CHANGE = new ProfileChange(); @@ -47,8 +47,8 @@ public String toString() { } } - final class TransferConfirm implements OtpPurpose { - private TransferConfirm() {} + final class TransferConfirmation implements OtpPurpose { + private TransferConfirmation() {} @Override public String name() { @@ -61,8 +61,8 @@ public String toString() { } } - final class LoanDisburse implements OtpPurpose { - private LoanDisburse() {} + final class LoanDisbursement implements OtpPurpose { + private LoanDisbursement() {} @Override public String name() { diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpStatus.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpStatus.java similarity index 100% rename from payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/enums/OtpStatus.java rename to payguard-identity/src/main/java/dev/amg/payguard/identity/verification/domain/value_object/OtpStatus.java diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/IdentityOutboxRelay.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/IdentityOutboxRelay.java new file mode 100644 index 0000000..047fb3e --- /dev/null +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/IdentityOutboxRelay.java @@ -0,0 +1,304 @@ +package dev.amg.payguard.identity.verification.infrastructure.adapter.out.kafka; + +import static dev.amg.payguard.identity.infrastructure.adapter.out.persistence.jooq.generated.tables.IdentityOutbox.IDENTITY_OUTBOX; +import static org.jooq.impl.DSL.currentOffsetDateTime; +import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.select; +import static org.jooq.impl.DSL.val; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.jooq.CommonTableExpression; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record; +import org.jooq.Record1; +import org.jooq.impl.SQLDataType; +import org.jooq.types.DayToSecond; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * Publishes committed Identity outbox events to Kafka. + * + *

Events are written to PostgreSQL by the originating business + * transaction. This relay claims eligible events, publishes them + * to Kafka, and marks them as published after broker acknowledgment. + * + *

Each claim uses a database-backed lease and PostgreSQL's + * {@code FOR UPDATE SKIP LOCKED} to support concurrent relay instances + * without intentionally processing the same row simultaneously. + * + *

Delivery is at least once, not exactly once. Consumers must + * deduplicate events by their stable event ID. + * + *

Kafka operations are never performed inside a database transaction. + * Failed or interrupted deliveries remain recoverable from the outbox. + * + *

Requires the {@code claimed_by} and {@code claim_until} columns + * in {@code payguard.identity_outbox}. + */ +@Component +public final class IdentityOutboxRelay { + + private static final Logger LOGGER = + LoggerFactory.getLogger(IdentityOutboxRelay.class); + + private static final int MAX_RETRY_DELAY_SECONDS = 300; + + private static final Field PAYLOAD_TEXT = + IDENTITY_OUTBOX.PAYLOAD.cast(SQLDataType.CLOB).as("payload"); + + private final DSLContext dsl; + private final KafkaTemplate kafka; + private final String topic; + private final Duration leaseDuration; + private final Duration sendTimeout; + private final int maxEventsPerPoll; + + public IdentityOutboxRelay( + DSLContext dsl, + KafkaTemplate kafka, + @Value("${identity.kafka.topic:identity.events.v1}") + String topic, + @Value("${identity.kafka.outbox.lease:PT2M}") + Duration leaseDuration, + @Value("${identity.kafka.outbox.send-timeout:PT10S}") + Duration sendTimeout, + @Value("${identity.kafka.outbox.max-events-per-poll:50}") + int maxEventsPerPoll) { + + this.dsl = Objects.requireNonNull(dsl); + this.kafka = Objects.requireNonNull(kafka); + this.topic = Objects.requireNonNull(topic); + this.leaseDuration = Objects.requireNonNull(leaseDuration); + this.sendTimeout = Objects.requireNonNull(sendTimeout); + + if (topic.isBlank()) { + throw new IllegalArgumentException("Kafka topic must not be blank"); + } + if (sendTimeout.toMillis() <= 0) { + throw new IllegalArgumentException("Send timeout must be positive"); + } + if (leaseDuration.compareTo(sendTimeout) <= 0) { + throw new IllegalArgumentException( + "Lease duration must exceed send timeout"); + } + if (maxEventsPerPoll <= 0) { + throw new IllegalArgumentException( + "Maximum events per poll must be positive"); + } + + this.maxEventsPerPoll = maxEventsPerPoll; + } + + @Scheduled( + fixedDelayString = "${identity.kafka.outbox.poll-delay-ms:1000}") + public void publishPending() { + + for (int index = 0; index < maxEventsPerPoll; index++) { + + if (Thread.currentThread().isInterrupted()) { + return; + } + + ClaimedEvent event = claimNext(); + + if (event == null) { + return; + } + + if (!publish(event)) { + return; + } + } + } + + /** + * Atomically claims the next eligible outbox event. + * + *

A CTE locks one candidate using {@code SKIP LOCKED}; the + * {@code UPDATE ... FROM} then assigns the claim token and lease. + * The token acts as a fencing value for later updates. + */ + private ClaimedEvent claimNext() { + + UUID claimToken = UUID.randomUUID(); + + CommonTableExpression> nextEvent = + name("next_event").as( + select(IDENTITY_OUTBOX.EVENT_ID) + .from(IDENTITY_OUTBOX) + .where(IDENTITY_OUTBOX.PUBLISHED_AT.isNull()) + .and(IDENTITY_OUTBOX.NEXT_ATTEMPT_AT + .le(currentOffsetDateTime())) + .and( + IDENTITY_OUTBOX.CLAIM_UNTIL.isNull() + .or(IDENTITY_OUTBOX.CLAIM_UNTIL + .le(currentOffsetDateTime()))) + .orderBy( + IDENTITY_OUTBOX.CREATED_AT, + IDENTITY_OUTBOX.EVENT_ID) + .limit(1) + .forUpdate() + .skipLocked()); + + Field nextEventId = nextEvent.field(IDENTITY_OUTBOX.EVENT_ID); + + Record record = + dsl.with(nextEvent) + .update(IDENTITY_OUTBOX) + .set(IDENTITY_OUTBOX.CLAIMED_BY, claimToken) + .set(IDENTITY_OUTBOX.CLAIM_UNTIL, after(leaseDuration)) + .from(nextEvent) + .where(IDENTITY_OUTBOX.EVENT_ID.eq(nextEventId)) + .returning( + IDENTITY_OUTBOX.EVENT_ID, + IDENTITY_OUTBOX.AGGREGATE_ID, + IDENTITY_OUTBOX.EVENT_TYPE, + PAYLOAD_TEXT, + IDENTITY_OUTBOX.OCCURRED_AT, + IDENTITY_OUTBOX.ATTEMPT_COUNT) + .fetchOne(); + + if (record == null) { + return null; + } + + return new ClaimedEvent( + record.get(IDENTITY_OUTBOX.EVENT_ID), + claimToken, + record.get(IDENTITY_OUTBOX.AGGREGATE_ID), + record.get(IDENTITY_OUTBOX.EVENT_TYPE), + record.get(PAYLOAD_TEXT), + record.get(IDENTITY_OUTBOX.OCCURRED_AT), + record.get(IDENTITY_OUTBOX.ATTEMPT_COUNT)); + } + + private boolean publish(ClaimedEvent event) { + + ProducerRecord message = + new ProducerRecord<>(topic, event.aggregateId(), event.payload()); + + message.headers() + .add("event-id", utf8(event.eventId().toString())) + .add("event-type", utf8(event.eventType())) + .add("occurred-on", + utf8(event.occurredAt().toInstant().toString())); + + try { + kafka.send(message) + .get(sendTimeout.toMillis(), TimeUnit.MILLISECONDS); + + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + LOGGER.warn( + "Interrupted while publishing outbox event {}", + event.eventId()); + // The existing lease will expire and permit recovery. + return false; + + } catch (ExecutionException | TimeoutException | RuntimeException exception) { + scheduleRetry(event, exception); + return false; + } + + markPublished(event); + return true; + } + + /** Marks an acknowledged event as published, guarded by the claim token. */ + private void markPublished(ClaimedEvent event) { + + int updated = + dsl.update(IDENTITY_OUTBOX) + .set(IDENTITY_OUTBOX.PUBLISHED_AT, currentOffsetDateTime()) + .setNull(IDENTITY_OUTBOX.CLAIMED_BY) + .setNull(IDENTITY_OUTBOX.CLAIM_UNTIL) + .where(IDENTITY_OUTBOX.EVENT_ID.eq(event.eventId())) + .and(IDENTITY_OUTBOX.CLAIMED_BY.eq(event.claimToken())) + .and(IDENTITY_OUTBOX.PUBLISHED_AT.isNull()) + .execute(); + + if (updated == 0) { + LOGGER.warn( + "Outbox event {} was acknowledged by Kafka, " + + "but its claim was superseded", + event.eventId()); + return; + } + + if (updated != 1) { + throw new IllegalStateException( + "Unexpected publication update count: " + updated); + } + + LOGGER.debug("Published outbox event {} to Kafka", event.eventId()); + } + + /** Schedules a retry with exponential backoff, guarded by the claim token. */ + private void scheduleRetry(ClaimedEvent event, Exception failure) { + + int exponent = (int) Math.min(9L, (long) event.attemptCount() + 1L); + long delaySeconds = Math.min(MAX_RETRY_DELAY_SECONDS, 1L << exponent); + + int updated = + dsl.update(IDENTITY_OUTBOX) + .set(IDENTITY_OUTBOX.ATTEMPT_COUNT, + IDENTITY_OUTBOX.ATTEMPT_COUNT.plus(1)) + .set(IDENTITY_OUTBOX.NEXT_ATTEMPT_AT, + after(Duration.ofSeconds(delaySeconds))) + .setNull(IDENTITY_OUTBOX.CLAIMED_BY) + .setNull(IDENTITY_OUTBOX.CLAIM_UNTIL) + .where(IDENTITY_OUTBOX.EVENT_ID.eq(event.eventId())) + .and(IDENTITY_OUTBOX.CLAIMED_BY.eq(event.claimToken())) + .and(IDENTITY_OUTBOX.PUBLISHED_AT.isNull()) + .execute(); + + if (updated == 0) { + LOGGER.warn( + "Could not schedule retry for outbox event {} " + + "because claim ownership was lost", + event.eventId()); + return; + } + + LOGGER.warn( + "Kafka publication failed for outbox event {}. " + + "Retry scheduled after {} seconds", + event.eventId(), + delaySeconds, + failure); + } + + /** Database-side "now + duration", evaluated on the DB clock. */ + private static Field after(Duration duration) { + return currentOffsetDateTime() + .add(val(DayToSecond.valueOf(duration))); + } + + private static byte[] utf8(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private record ClaimedEvent( + UUID eventId, + UUID claimToken, + String aggregateId, + String eventType, + String payload, + OffsetDateTime occurredAt, + int attemptCount + ) {} +} \ No newline at end of file diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/KafkaOtpEventPublisher.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/KafkaOtpEventPublisher.java index 5278e4c..2730fb8 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/KafkaOtpEventPublisher.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/KafkaOtpEventPublisher.java @@ -2,55 +2,92 @@ import dev.amg.payguard.identity.verification.domain.event.OtpEvent; import dev.amg.payguard.identity.verification.domain.event.OtpEventPublisher; +import dev.amg.payguard.identity.verification.infrastructure.adapter.out.kafka.contracts.OtpEventV1; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; +import tools.jackson.core.JacksonException; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.util.Objects; @Component public class KafkaOtpEventPublisher implements OtpEventPublisher { - private final KafkaTemplate kafka; - private final ObjectMapper objectMapper; - private final String topic; + private static final Logger log = LoggerFactory.getLogger(KafkaOtpEventPublisher.class); + + // Dedicated mapper: the wire format no longer depends on the app-wide ObjectMapper config. + private static final ObjectMapper MAPPER = JsonMapper.builder().build(); - public KafkaOtpEventPublisher( - KafkaTemplate kafka, - ObjectMapper objectMapper, + private final KafkaTemplate kafkaTemplate; + private final String topic; + + public KafkaOtpEventPublisher(KafkaTemplate kafkaTemplate, @Value("${identity.kafka.topic:identity.events.v1}") String topic) { - this.kafka = kafka; - this.objectMapper = objectMapper; - this.topic = topic; + this.kafkaTemplate = Objects.requireNonNull(kafkaTemplate, "kafkaTemplate"); + this.topic = Objects.requireNonNull(topic, "topic"); } @Override public void publish(OtpEvent event) { - try { - kafka.send( - topic, - event.challengeId().toString(), - objectMapper.writeValueAsString( - new EventPayload( - event.type(), - event.challengeId().value(), - event.customerId().getValue(), - event.purpose().name(), - event.channel().name(), - event.result() == null ? null : event.result().name(), - event.occurredAt(), - event.reason()))); - } catch (tools.jackson.core.JacksonException exception) { - throw new IllegalStateException("Unable to serialize OTP event", exception); - } + Objects.requireNonNull(event); + + OtpEventV1 payload = toPayload(event); + final String json = serialize(payload); + + // Key = customerId: all events of one customer land in one partition, so they stay ordered. + kafkaTemplate + .send(topic, payload.customerId(), json) + .whenComplete((result, ex) -> { + if (ex != null) { + log.error("Failed to publish OTP event eventId={} challengeId={} topic={}", + payload.eventId(), + payload.challengeId(), + topic, + ex + ); + } else if (log.isDebugEnabled()) { + log.debug( + "Published OTP event eventId={} partition={} offset={}", + payload.eventId(), + result.getRecordMetadata().partition(), + result.getRecordMetadata().offset()); + } + } + ); + } - private record EventPayload( - String type, - String challengeId, - String userId, - String purpose, - String channel, - String result, - java.time.Instant occurredAt, - String reason) {} + + private OtpEventV1 toPayload(OtpEvent event) { + final String challengeId = event.challengeId().value(); + final String customerId = event.customerId().getValue(); + final String result = event.result() == null ? null : event.result().name(); + + return new OtpEventV1( + event.id().toString(), + OtpEventV1.SCHEMA_VERSION, + event.type(), + challengeId, + customerId, + event.purpose().name(), + event.channel().name(), + result, + event.occurredAt().toString(), + event.reason() + ); + } + + private String serialize(OtpEventV1 payload) { + try { + return MAPPER.writeValueAsString(payload); + } catch (JacksonException exception) { + // Deterministic bug (not a transient error): fail fast, a retry would never help. + throw new IllegalStateException("Unable to serialize OTP event " + payload.eventId(), exception); + } + } + } diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/contracts/OtpEventV1.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/contracts/OtpEventV1.java new file mode 100644 index 0000000..b3ec482 --- /dev/null +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/kafka/contracts/OtpEventV1.java @@ -0,0 +1,26 @@ +package dev.amg.payguard.identity.verification.infrastructure.adapter.out.kafka.contracts; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Wire contract for OTP events on the identity topic (schema version 1). + * + *

This is a public contract: do not rename or remove fields. Additive changes only; breaking + * changes require a new class (OtpEventV2) and a new topic version. + */ +@JsonInclude +public record OtpEventV1( + String eventId, + int schemaVersion, + String type, + String challengeId, + String customerId, + String purpose, + String channel, + String result, + String occurredAt, // ISO-8601 UTC string, independent of any ObjectMapper date setting + String reason) { + + public static final int SCHEMA_VERSION = 1; + +} diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditLogAdapter.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditLogAdapter.java deleted file mode 100644 index c359de8..0000000 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditLogAdapter.java +++ /dev/null @@ -1,3 +0,0 @@ -package dev.amg.payguard.identity.verification.infrastructure.adapter.out.persistence.jooq; - -public class JooqOtpAuditLogAdapter {} diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditOutboxAdapter.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditOutboxAdapter.java new file mode 100644 index 0000000..339577f --- /dev/null +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JooqOtpAuditOutboxAdapter.java @@ -0,0 +1,89 @@ +package dev.amg.payguard.identity.verification.infrastructure.adapter.out.persistence.jooq; + +import dev.amg.payguard.identity.verification.application.port.VerificationAudit; +import dev.amg.payguard.identity.verification.application.port.VerificationAuditEntry; +import org.jooq.DSLContext; +import org.jooq.JSON; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Objects; +import java.util.UUID; + +import static dev.amg.payguard.identity.infrastructure.adapter.out.persistence.jooq.generated.tables.IdentityOutbox.IDENTITY_OUTBOX; + +@Repository +public class JooqOtpAuditOutboxAdapter implements VerificationAudit { + private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + + private final DSLContext dsl; + + public JooqOtpAuditOutboxAdapter(DSLContext dsl) { + this.dsl = Objects.requireNonNull(dsl); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void recordAudit(VerificationAuditEntry entry) { + Objects.requireNonNull(entry); + final String payload; + + try { + final AuditPayload audit = new AuditPayload( + entry.challengeId().value(), + entry.customerId().getValue(), + entry.verificationAuditAction().name(), + entry.reasonCode(), + entry.sourceIp(), + entry.occurredAt() + ); + payload = MAPPER.writeValueAsString(audit); + } catch (JacksonException exception) { + throw new IllegalStateException( + "Failed to serialize OTP audit event", exception + ); + } + + int inserted = dsl.insertInto(IDENTITY_OUTBOX) + .set(IDENTITY_OUTBOX.EVENT_ID, UUID.randomUUID()) + .set( + IDENTITY_OUTBOX.AGGREGATE_ID, + entry.challengeId().value() + ) + .set( + IDENTITY_OUTBOX.EVENT_TYPE, + "OtpAuditRecordedV1" + ) + .set( + IDENTITY_OUTBOX.PAYLOAD, + JSON.valueOf(payload) + ) + .set( + IDENTITY_OUTBOX.OCCURRED_AT, + entry.occurredAt().atOffset(ZoneOffset.UTC) + ) + .execute(); + + if (inserted != 1) { + throw new IllegalStateException( + "Failed to persist OTP audit outbox event" + ); + } + } + + private record AuditPayload( + String challengeId, + String customerId, + String action, + String reasonCode, + String sourceIp, + Instant occurredAt + ) { + } +} diff --git a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JsonOtpOutboxEncoder.java b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JsonOtpOutboxEncoder.java index f5969c5..dcda90a 100644 --- a/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JsonOtpOutboxEncoder.java +++ b/payguard-identity/src/main/java/dev/amg/payguard/identity/verification/infrastructure/adapter/out/persistence/jooq/JsonOtpOutboxEncoder.java @@ -13,7 +13,7 @@ @Component public final class JsonOtpOutboxEncoder implements OtpOutboxEncoder { - private static final String OTP_ID = "otpId"; + private static final String OTP_ID = "challengeId"; private static final String CUSTOMER_ID = "customerId"; private static final String PURPOSE = "purpose"; private static final String OCCURRED_ON = "occurredOn"; diff --git a/payguard-identity/src/main/resources/db/jooq/V5__add_claim_until_and_claimed_by_in_outbox.sql b/payguard-identity/src/main/resources/db/jooq/V5__add_claim_until_and_claimed_by_in_outbox.sql new file mode 100644 index 0000000..b534865 --- /dev/null +++ b/payguard-identity/src/main/resources/db/jooq/V5__add_claim_until_and_claimed_by_in_outbox.sql @@ -0,0 +1,3 @@ +ALTER TABLE identity_outbox + ADD COLUMN claimed_by uuid, + ADD COLUMN claim_until timestamptz(6); \ No newline at end of file diff --git a/payguard-loan/Dockerfile b/payguard-loan/Dockerfile index e9db8bf..f835430 100644 --- a/payguard-loan/Dockerfile +++ b/payguard-loan/Dockerfile @@ -2,12 +2,12 @@ FROM eclipse-temurin:25-jdk-alpine@sha256:3fd2d245c4e0eba615fe366a71b8bd25f5db7104f53e4026b24bf508b880bd2a AS build WORKDIR /workspace COPY . . -RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-loan-service -am -DskipTests package +RUN bash ./gradlew --no-daemon --build-cache :payguard-loan:bootJar FROM eclipse-temurin:25-jre-alpine@sha256:3c0a9084927a221ccd1d007fcaf614465672c0af37aaa834c5184483afe56d61 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 +COPY --from=build /workspace/payguard-loan/build/libs/payguard-loan-1.0.0-SNAPSHOT.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/build.gradle.kts b/payguard-loan/build.gradle.kts new file mode 100644 index 0000000..1e11fd6 --- /dev/null +++ b/payguard-loan/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + alias(libs.plugins.boot.application) + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "payguard-loan" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/payguard-loan/gradle.properties b/payguard-loan/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-loan/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-loan/gradle/wrapper/gradle-wrapper.jar b/payguard-loan/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-loan/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-loan/gradle/wrapper/gradle-wrapper.properties b/payguard-loan/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-loan/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-loan/gradlew b/payguard-loan/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-loan/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-loan/gradlew.bat b/payguard-loan/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-loan/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-loan/settings.gradle.kts b/payguard-loan/settings.gradle.kts new file mode 100644 index 0000000..d79e8c8 --- /dev/null +++ b/payguard-loan/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-loan" diff --git a/payguard-proto/build.gradle.kts b/payguard-proto/build.gradle.kts new file mode 100644 index 0000000..e116701 --- /dev/null +++ b/payguard-proto/build.gradle.kts @@ -0,0 +1,67 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) + alias(libs.plugins.protobuf) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + api(libs.com.google.protobuf.protobuf.java) + api(libs.io.grpc.grpc.protobuf) + api(libs.io.grpc.grpc.stub) + api(libs.javax.annotation.javax.annotation.api) +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "payguard-proto" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} + + +protobuf { + protoc { + artifact = libs.protobuf.protoc.get().toString() + } + + plugins { + create("grpc") { + artifact = libs.grpc.protoc.gen.java.get().toString() + } + } + + generateProtoTasks { + all().configureEach { + plugins { + create("grpc") + } + } + } +} diff --git a/payguard-proto/gradle.properties b/payguard-proto/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-proto/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-proto/gradle/wrapper/gradle-wrapper.jar b/payguard-proto/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-proto/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-proto/gradle/wrapper/gradle-wrapper.properties b/payguard-proto/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-proto/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-proto/gradlew b/payguard-proto/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-proto/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-proto/gradlew.bat b/payguard-proto/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-proto/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-proto/pom.xml b/payguard-proto/pom.xml deleted file mode 100644 index fcbe987..0000000 --- a/payguard-proto/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - - - dev.amg.payguard - payguard-parent - 1.0.0-SNAPSHOT - - - payguard-proto - - - - com.google.protobuf - protobuf-java - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - javax.annotation - javax.annotation-api - 1.3.2 - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.xolstice.maven.plugins - protobuf-maven-plugin - ${protobuf.maven.plugin.version} - - - com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} - - grpc-java - - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - - - - - - compile - compile-custom - - - - - - - - diff --git a/payguard-proto/settings.gradle.kts.disabled b/payguard-proto/settings.gradle.kts.disabled new file mode 100644 index 0000000..f9ebd33 --- /dev/null +++ b/payguard-proto/settings.gradle.kts.disabled @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-proto" diff --git a/payguard-shared/build.gradle.kts b/payguard-shared/build.gradle.kts new file mode 100644 index 0000000..18e0962 --- /dev/null +++ b/payguard-shared/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + api(libs.org.springframework.boot.spring.boot.starter.jdbc) + api(libs.tools.jackson.core.jackson.databind) + api(libs.org.springframework.kafka.spring.kafka) + api(libs.org.slf4j.slf4j.api) + api(libs.org.springframework.grpc.spring.grpc.core) + api(libs.org.springframework.spring.context) + api(libs.org.springframework.boot.spring.boot.autoconfigure) + testImplementation(libs.org.flywaydb.flyway.core) + testImplementation(libs.org.junit.platform.junit.platform.console.standalone) +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "payguard-shared" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/payguard-shared/gradle.properties b/payguard-shared/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-shared/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-shared/gradle/wrapper/gradle-wrapper.jar b/payguard-shared/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-shared/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-shared/gradle/wrapper/gradle-wrapper.properties b/payguard-shared/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-shared/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-shared/gradlew b/payguard-shared/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-shared/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-shared/gradlew.bat b/payguard-shared/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-shared/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-shared/settings.gradle.kts b/payguard-shared/settings.gradle.kts new file mode 100644 index 0000000..fc5482f --- /dev/null +++ b/payguard-shared/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-shared" diff --git a/payguard-wallet/Dockerfile b/payguard-wallet/Dockerfile index 25a064c..d6e8a45 100644 --- a/payguard-wallet/Dockerfile +++ b/payguard-wallet/Dockerfile @@ -2,12 +2,12 @@ FROM eclipse-temurin:25-jdk-alpine@sha256:3fd2d245c4e0eba615fe366a71b8bd25f5db7104f53e4026b24bf508b880bd2a AS build WORKDIR /workspace COPY . . -RUN chmod +x mvnw && ./mvnw --batch-mode --no-transfer-progress -pl :payguard-wallet-service -am -DskipTests package +RUN bash ./gradlew --no-daemon --build-cache :payguard-wallet:bootJar FROM eclipse-temurin:25-jre-alpine@sha256:3c0a9084927a221ccd1d007fcaf614465672c0af37aaa834c5184483afe56d61 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 +COPY --from=build /workspace/payguard-wallet/build/libs/payguard-wallet-1.0.0-SNAPSHOT.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/build.gradle.kts b/payguard-wallet/build.gradle.kts new file mode 100644 index 0000000..787ac45 --- /dev/null +++ b/payguard-wallet/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + alias(libs.plugins.boot.application) + `java-library` + `maven-publish` + checkstyle + pmd + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } +} + +dependencies { + api(libs.org.aspectj.aspectjweaver) + api(libs.org.springdoc.springdoc.openapi.starter.webmvc.ui) + runtimeOnly(libs.org.springframework.boot.spring.boot.devtools) +} + +group = "dev.amg.payguard" +version = "1.0.0-SNAPSHOT" +description = "Wallet Service" +java.sourceCompatibility = JavaVersion.VERSION_25 + +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} diff --git a/payguard-wallet/gradle.properties b/payguard-wallet/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/payguard-wallet/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/payguard-wallet/gradle/wrapper/gradle-wrapper.jar b/payguard-wallet/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9fa7f28 Binary files /dev/null and b/payguard-wallet/gradle/wrapper/gradle-wrapper.jar differ diff --git a/payguard-wallet/gradle/wrapper/gradle-wrapper.properties b/payguard-wallet/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..91c4fc6 --- /dev/null +++ b/payguard-wallet/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/payguard-wallet/gradlew b/payguard-wallet/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/payguard-wallet/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/payguard-wallet/gradlew.bat b/payguard-wallet/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/payguard-wallet/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/payguard-wallet/settings.gradle.kts b/payguard-wallet/settings.gradle.kts new file mode 100644 index 0000000..9ac50e1 --- /dev/null +++ b/payguard-wallet/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-wallet" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 2462671..0000000 --- a/pom.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - 4.0.0 - - dev.amg.payguard - payguard-parent - 1.0.0-SNAPSHOT - pom - - PayGuard Platform - - - payguard-shared - payguard-proto - payguard-gateway - payguard-wallet - payguard-loan - payguard-collateral - payguard-identity - - - - 25 - 25 - UTF-8 - 4.1.1 - 1.68.0 - 3.25.5 - 3.21.7 - 0.6.1 - 1.7.1 - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - io.grpc - grpc-bom - ${grpc.version} - pom - import - - - com.google.protobuf - protobuf-java - ${protobuf.version} - - - com.google.protobuf - protobuf-java-util - ${protobuf.version} - - - org.jooq - jooq - ${jooq.version} - - - - - - - kr.motd.maven - os-maven-plugin - 1.7.1 - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - - - com.diffplug.spotless - spotless-maven-plugin - 3.10.0 - - - - - src/*/java/**/*.java - - - - **/jooq/generated/**/*.java - - - - 1.30.0 - - - - - - - - - - - - spotless-check - validate - - - check - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - - - com.puppycrawl.tools - checkstyle - 14.0.0 - - - - - - ${session.executionRootDirectory}/config/checkstyle/checkstyle.xml - - true - true - false - - - - - checkstyle-check - validate - - - check - - - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.28.0 - - - ${java.version} - true - true - - ${project.build.directory}/generated-sources - - - - category/java/errorprone.xml - category/java/bestpractices.xml - - - - - - pmd-check - verify - - - check - cpd-check - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.10.4.0 - - - Max - High - true - false - true - - - - - spotbugs-check - verify - - - check - - - - - - - - - install-git-hooks - - - - - org.apache.maven.plugins - maven-antrun-plugin - 3.2.0 - false - - - - install-git-hooks - initialize - - - run - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..613156d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = "payguard-parent" +include(":payguard-proto") +include(":payguard-identity") +include(":payguard-loan") +include(":payguard-wallet") +include(":payguard-shared") +include(":payguard-gateway") +include(":payguard-collateral")