diff --git a/.github/workflows/build-cucumber.yml b/.github/workflows/build-cucumber.yml index 4b267d12420..3e1fa3385f7 100644 --- a/.github/workflows/build-cucumber.yml +++ b/.github/workflows/build-cucumber.yml @@ -9,7 +9,6 @@ permissions: jobs: cucumber: runs-on: ubuntu-24.04 - timeout-minutes: 60 env: TZ: Asia/Kolkata diff --git a/.github/workflows/build-push-sms-gateway.yml b/.github/workflows/build-push-sms-gateway.yml new file mode 100644 index 00000000000..8aa92f3d26c --- /dev/null +++ b/.github/workflows/build-push-sms-gateway.yml @@ -0,0 +1,93 @@ +name: Build & Push SMS Gateway Native Image + +on: + push: + branches: + - develop + - main + tags: + - 'v*' + paths: + - 'fineract-adorsys-sms-gateway/**' + - '.github/workflows/build-push-sms-gateway.yml' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/sms-gateway + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: "0" + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build with Maven + run: | + cd fineract-adorsys-sms-gateway + mvn clean package -DskipTests + + - name: Set up QEMU (for cross‑architecture emulation) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx (multi‑arch builder) + uses: docker/setup-buildx-action@v3 + + - name: Lowercase the repository name + id: string + uses: AsZc/change-string-case-action@v6 + with: + string: ${{ env.IMAGE_NAME }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=short + type=sha,format=long + type=ref,event=branch + type=schedule,pattern={{date 'YYYYMMDD-hhmmss' tz='Africa/Douala'}} + type=semver,pattern={{version}},enable=true,priority=900 + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + + flavor: | + latest=false + images: "${{ env.REGISTRY }}/${{ steps.string.outputs.lowercase }}" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & push multi‑arch image + uses: docker/build-push-action@v6 + with: + context: fineract-adorsys-sms-gateway + file: fineract-adorsys-sms-gateway/Dockerfile + platforms: linux/amd64,linux/arm64 + push: "true" + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + cache-from: type=gha,scope=shared + cache-to: type=gha,mode=max,scope=shared diff --git a/.github/workflows/config-cli-build.yml b/.github/workflows/config-cli-build.yml new file mode 100644 index 00000000000..708b4ea80cf --- /dev/null +++ b/.github/workflows/config-cli-build.yml @@ -0,0 +1,112 @@ +name: Config-CLI Build and Test + +on: + push: + branches: + - main + - develop + paths: + - 'fineract-adorsys-data-collection/fineract-config-cli/**' + - '.github/workflows/config-cli-build.yml' + pull_request: + branches: + - main + - develop + paths: + - 'fineract-adorsys-data-collection/fineract-config-cli/**' + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + + defaults: + run: + working-directory: fineract-adorsys-data-collection/fineract-config-cli + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build with Maven + run: mvn clean package -DskipTests -B + + - name: Run Spotless check + run: mvn spotless:check + + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + with: + name: fineract-config-cli-jar + path: fineract-adorsys-data-collection/fineract-config-cli/target/fineract-config-cli.jar + retention-days: 7 + + docker-build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: fineract-adorsys-data-collection/fineract-config-cli + file: fineract-adorsys-data-collection/fineract-config-cli/Dockerfile + push: false + tags: fineract-config-cli:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Test Docker image + run: | + docker run --rm fineract-config-cli:${{ github.sha }} --version || true + + code-quality: + name: Code Quality Analysis + runs-on: ubuntu-latest + + defaults: + run: + working-directory: fineract-adorsys-data-collection/fineract-config-cli + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Run Spotless check + run: mvn spotless:check + + - name: Check code formatting + run: | + mvn spotless:check || { + echo "Code formatting issues found. Run 'mvn spotless:apply' locally to fix." + exit 1 + } diff --git a/.github/workflows/config-cli-docker-publish.yml b/.github/workflows/config-cli-docker-publish.yml new file mode 100644 index 00000000000..3efa699cb9b --- /dev/null +++ b/.github/workflows/config-cli-docker-publish.yml @@ -0,0 +1,74 @@ +name: Config-CLI Docker Image CI + +on: + push: + branches: + - main + - develop + - fix/config-cli-workflow + paths: + - 'fineract-adorsys-data-collection/fineract-config-cli/src/**' + - 'fineract-adorsys-data-collection/fineract-config-cli/pom.xml' + - 'fineract-adorsys-data-collection/fineract-config-cli/Dockerfile' + - '.github/workflows/config-cli-docker-publish.yml' + + pull_request: + branches: + - main + - develop + paths: + - 'fineract-adorsys-data-collection/fineract-config-cli/Dockerfile' + + schedule: + # Run weekly on Monday at 00:00 UTC + - cron: '0 0 * * 1' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/fineract-config-cli + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.3.0 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + type=raw,value=snapshot,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5.4.0 + with: + context: fineract-adorsys-data-collection/fineract-config-cli + file: fineract-adorsys-data-collection/fineract-config-cli/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index ef498aae35b..b010c0656ad 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -1,8 +1,7 @@ name: Fineract PR Compliance on: - pull_request: - types: [opened, edited, reopened, synchronize] + workflow_dispatch: permissions: pull-requests: read diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml new file mode 100644 index 00000000000..f03cdfa5ce0 --- /dev/null +++ b/.github/workflows/publish-ghcr.yml @@ -0,0 +1,73 @@ +name: Fineract Publish to GitHub Container Registry +on: + push: + branches: + - develop +permissions: + contents: read + packages: write +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Checkout Source Code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5 + with: + java-version: '21' + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + + - name: Get Git Hashes + run: | + echo "short_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "long_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "owner_lowercase=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + id: git_hashes + + - name: Prepare Pentaho Plugin + run: | + echo "Preparing Pentaho Plugin..." + mkdir -p pentaho-dist/plugins + mkdir -p pentaho-dist/reports + + # Unzip the plugin + unzip -o fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1.zip -d pentaho-temp + + # Copy JARs (Handling potential internal folder structure) + if [ -d "pentaho-temp/MifosSecurityPlugin-1.12.1/lib" ]; then + echo "Found lib directory, copying JARs..." + cp pentaho-temp/MifosSecurityPlugin-1.12.1/lib/*.jar pentaho-dist/plugins/ + else + echo "Copying JARs from root..." + cp pentaho-temp/MifosSecurityPlugin-1.12.1/*.jar pentaho-dist/plugins/ || echo "No JARs found in root either!" + fi + + # Copy Reports (From the repo folder using Postgresql) + echo "Copying Reports..." + cp -r fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1/Postgresql pentaho-dist/reports/ + + echo "Pentaho preparation complete." + ls -R pentaho-dist + + - name: Build the Apache Fineract image + run: | + TAGS="${{ steps.git_hashes.outputs.short_hash }},${{ steps.git_hashes.outputs.long_hash }}" + if [ "${{ github.ref_name }}" == "develop" ]; then + TAGS="$TAGS,latest" + fi + ./gradlew --no-daemon --console=plain :custom:docker:jib -x test -x cucumber \ + -Djib.from.platforms=linux/amd64,linux/arm64 \ + -Djib.to.auth.username=${{ github.actor }} \ + -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} \ + -Djib.to.image=ghcr.io/${{ steps.git_hashes.outputs.owner_lowercase }}/fineract \ + -Djib.to.tags=$TAGS \ + -Djib.sendTimeout=300000 \ + -Djib.readTimeout=300000 diff --git a/.github/workflows/smoke-messaging.yml b/.github/workflows/smoke-messaging.yml index 1d48d1e1706..8f974da4a7e 100644 --- a/.github/workflows/smoke-messaging.yml +++ b/.github/workflows/smoke-messaging.yml @@ -10,7 +10,6 @@ jobs: smoke-test: name: Smoke Test with ${{ matrix.messaging }} runs-on: ubuntu-24.04 - timeout-minutes: 60 strategy: fail-fast: false matrix: diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000000..cf5bba94683 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,152 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# + +name: Sync Upstream Apache Fineract + +on: + schedule: + - cron: '0 6 * * 1' # every monday at 06:00 UTC + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + sync: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout fork + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/apache/fineract.git || true + git fetch upstream develop + + - name: Compute branch name + id: branch + run: | + BRANCH_NAME="sync/upstream-$(date +%Y-%m)" + echo "name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + + - name: Check if sync is needed + id: check + run: | + BEHIND=$(git rev-list --count develop..upstream/develop) + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + if [ "$BEHIND" -eq 0 ]; then + echo "Already up to date with upstream. Nothing to do." + else + echo "Fork is $BEHIND commits behind upstream." + fi + + - name: Create sync branch and merge + if: steps.check.outputs.behind != '0' + id: merge + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "${{ steps.branch.outputs.name }}" develop + + if git merge upstream/develop --no-edit; then + echo "status=success" >> "$GITHUB_OUTPUT" + git push -u origin "${{ steps.branch.outputs.name }}" + else + echo "status=conflicts" >> "$GITHUB_OUTPUT" + # Capture conflicting files for the issue + CONFLICTS=$(git diff --name-only --diff-filter=U | head -30) + echo "conflicts<> "$GITHUB_OUTPUT" + echo "$CONFLICTS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + git merge --abort + fi + + - name: Create PR on successful merge + if: steps.check.outputs.behind != '0' && steps.merge.outputs.status == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --title "chore: sync upstream Apache Fineract (${{ steps.check.outputs.behind }} commits)" \ + --body "$(cat <<'BODY' + ## Upstream Sync + + This PR merges the latest changes from [apache/fineract](https://github.com/apache/fineract) `develop` branch. + + - **Commits behind:** ${{ steps.check.outputs.behind }} + - **Branch:** `${{ steps.branch.outputs.name }}` + - **Auto-merged:** Yes (no conflicts detected) + + ### Review checklist + - [ ] Custom plugins compile (`custom/adorsys/`) + - [ ] Docker images build correctly + - [ ] Config CLI still works + - [ ] Database migrations apply cleanly + - [ ] CI pipeline passes + + --- + *Auto-generated by the monthly upstream sync workflow.* + BODY + )" \ + --base develop \ + --head "${{ steps.branch.outputs.name }}" \ + --label "upstream-sync" + + - name: Create issue on merge conflicts + if: steps.check.outputs.behind != '0' && steps.merge.outputs.status == 'conflicts' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh issue create \ + --title "Upstream sync failed: merge conflicts detected ($(date +%Y-%m))" \ + --body "$(cat < if('build' != companyDir.name && 'docker' != companyDir.name) { diff --git a/config/docker/env/fineract-adorsys.env b/config/docker/env/fineract-adorsys.env new file mode 100644 index 00000000000..ce4d978b0a7 --- /dev/null +++ b/config/docker/env/fineract-adorsys.env @@ -0,0 +1,49 @@ +# Fineract Service Variables +FINERACT_WEB_HOOK_URL=http://sms-gateway:8080/sms/ +FINERACT_PENTAHO_REPORTS_PATH=/pentahoReports/MariaDB/ +JAVA_TOOL_OPTIONS=-Dloader.path=/app/plugins/ + +# Keycloak Service Variables +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KC_HEALTH_ENABLED=true +KC_THEME_DEFAULT=adorsys-tm-01 +KC_HOSTNAME_STRICT=false +KC_HTTP_ENABLED=true + +# Fineract Config CLI Variables +FINERACT_BASE_URL=https://fineract:8443/fineract-provider +FINERACT_AUTH_TYPE=basic +FINERACT_AUTH_OAUTH2_TOKEN_URL=http://keycloak:8080/realms/fineract/protocol/openid-connect/token +FINERACT_AUTH_OAUTH2_CLIENT_ID=setup-app-client +FINERACT_AUTH_OAUTH2_CLIENT_SECRET=********** +FINERACT_AUTH_OAUTH2_GRANT_TYPE=password +FINERACT_AUTH_OAUTH2_SCOPE= +FINERACT_TENANT=default +FINERACT_USERNAME=mifos +FINERACT_PASSWORD=password +FINERACT_SSL_VERIFY=false +JAVA_OPTS=-Xmx512m -Xms256m +SPRING_PROFILES_ACTIVE=dev +IMPORT_VALIDATE=true +IMPORT_FORCE=false +IMPORT_DRY_RUN=false +IMPORT_PARALLEL=false +IMPORT_REMOTE_STATE_ENABLED=true +IMPORT_REMOTE_STATE_CHECKSUM_BEHAVIOR=continue +IMPORT_AUTO_IMPORT_ENABLED=true +IMPORT_EXIT_AFTER_IMPORT=true +IMPORT_FAIL_ON_ERROR=false + +# Keycloak Config CLI Variables +KEYCLOAK_URL=http://keycloak:8080/ +KEYCLOAK_USER=admin +# KEYCLOAK_PASSWORD is also used by keycloak-config-cli, distinct from KEYCLOAK_ADMIN_PASSWORD used by keycloak +# Note: KEYCLOAK_PASSWORD=admin collision with existing variable if any? No, keycloak uses KEYCLOAK_ADMIN_PASSWORD. +KEYCLOAK_PASSWORD=admin +IMPORT_FILES_LOCATIONS=/config/*.json + +# Fineract Keycloak Variables +FINERACT_SERVER_OAUTH_RESOURCE_URL=http://keycloak:8080/realms/fineract +FINERACT_SECURITY_BASICAUTH_ENABLED=true +FINERACT_SECURITY_OAUTH_ENABLED=false diff --git a/config/docker/env/fineract-common.env b/config/docker/env/fineract-common.env index 78ac9951f6e..7e48db7d67d 100644 --- a/config/docker/env/fineract-common.env +++ b/config/docker/env/fineract-common.env @@ -18,8 +18,8 @@ # # ... following variables are optional; "application.properties" contains reasonable defaults (same as here) -FINERACT_USER=1000 -FINERACT_GROUP=1000 +FINERACT_USER=1001 +FINERACT_GROUP=1001 FINERACT_HIKARI_MINIMUM_IDLE=3 FINERACT_HIKARI_MAXIMUM_POOL_SIZE=10 FINERACT_HIKARI_IDLE_TIMEOUT=60000 diff --git a/config/fineract-adorsys-apache/Dockerfile b/config/fineract-adorsys-apache/Dockerfile new file mode 100644 index 00000000000..470e0d6f56d --- /dev/null +++ b/config/fineract-adorsys-apache/Dockerfile @@ -0,0 +1,16 @@ +FROM httpd:2.4 + +# Install the pre-packaged OpenID Connect module for Apache +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libapache2-mod-auth-openidc \ + ca-certificates && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + ln -s /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/mod_auth_openidc.so + +# # The httpd.conf file is mounted via docker-compose, so no COPY is needed here. +# # The modules are enabled via LoadModule directives in the mounted httpd.conf. + +# # The httpd.conf file is mounted via docker-compose, so no COPY is needed here. +# # The modules are enabled via LoadModule directives in the mounted httpd.conf. \ No newline at end of file diff --git a/config/fineract-adorsys-apache/httpd.conf b/config/fineract-adorsys-apache/httpd.conf new file mode 100644 index 00000000000..3d7bdb56b9e --- /dev/null +++ b/config/fineract-adorsys-apache/httpd.conf @@ -0,0 +1,228 @@ +# Apache HTTP Configuration for Fineract Docker Compose +# Based on the Kubernetes production configuration + +# Load required Apache modules +LoadModule mpm_event_module modules/mod_mpm_event.so +LoadModule auth_openidc_module modules/mod_auth_openidc.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule ssl_module modules/mod_ssl.so +LoadModule proxy_connect_module modules/mod_proxy_connect.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so +LoadModule headers_module modules/mod_headers.so + +# Additional modules from K8s setup for enhanced functionality +LoadModule status_module modules/mod_status.so +LoadModule dir_module modules/mod_dir.so +LoadModule alias_module modules/mod_alias.so +LoadModule mime_module modules/mod_mime.so +LoadModule setenvif_module modules/mod_setenvif.so + +ServerName apache-proxy + +User daemon +Group daemon + +Listen 80 + +# Logging configuration +LogLevel warn auth_openidc:debug headers:trace5 proxy:trace5 rewrite:trace8 +ErrorLog /proc/self/fd/2 + + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + CustomLog /proc/self/fd/1 combined + + +# Health check endpoints (before VirtualHost) + + SetHandler server-status + Require all granted + + + + SetHandler server-status + Require all granted + + + + ServerName localhost + ProxyPreserveHost On + + # SSL Proxy configuration for Fineract API + SSLProxyEngine on + SSLProxyVerify none + SSLProxyCheckPeerCN off + SSLProxyCheckPeerName off + SSLProxyCheckPeerExpire off + + #============================================================================ + # OIDC CONFIGURATION (Based on K8s setup) + #============================================================================ + + OIDCCryptoPassphrase a-very-secret-passphrase + + # Apache uses Docker service name to connect to Keycloak for metadata + OIDCProviderMetadataURL http://keycloak:8080/realms/fineract/.well-known/openid-configuration + + # Override issuer to use keycloak.local:9000 which is accessible from both browser and Fineract + OIDCProviderIssuer http://keycloak.local:9000/realms/fineract + OIDCProviderAuthorizationEndpoint http://keycloak.local:9000/realms/fineract/protocol/openid-connect/auth + OIDCProviderTokenEndpoint http://keycloak:8080/realms/fineract/protocol/openid-connect/token + OIDCProviderTokenEndpointAuth client_secret_basic + OIDCProviderUserInfoEndpoint http://keycloak:8080/realms/fineract/protocol/openid-connect/userinfo + OIDCProviderJwksUri http://keycloak:8080/realms/fineract/protocol/openid-connect/certs + + OIDCClientID web-client + OIDCClientSecret ********** + + # Single callback URL for all apps + OIDCRedirectURI http://localhost/oauth2callback + + # Session configuration from K8s + OIDCSessionType server-cache + OIDCSessionMaxDuration 14400 + OIDCSessionInactivityTimeout 1800 + + # Cache configuration + OIDCCacheType shm + + # Cookie configuration + OIDCCookiePath / + OIDCCookieSameSite On + + # Scope and claims configuration + OIDCScope "openid profile email roles" + OIDCRemoteUserClaim preferred_username + + # Pass claims as both headers AND environment for maximum compatibility + OIDCPassClaimsAs both + OIDCPassIDTokenAs payload + OIDCPassRefreshToken On + + # Prefix used for exported claims in environment vars + OIDCClaimPrefix "OIDC_CLAIM_" + + #============================================================================ + # SECURITY HEADERS (From K8s setup) + #============================================================================ + + # CORS Headers + Header always set Access-Control-Allow-Origin "*" + Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" + Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-Fineract-Platform-TenantId" + Header always set Access-Control-Max-Age "3600" + + # Security Headers + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + #============================================================================ + # FINERACT API BACKEND (Simple routing for development) + #============================================================================ + + ProxyPass /fineract-provider/ https://fineract:8443/fineract-provider/ + ProxyPassReverse /fineract-provider/ https://fineract:8443/fineract-provider/ + + # Inject OIDC token into Fineract API requests + + AuthType openid-connect + Require valid-user + + # Pass the access token in Authorization header + # mod_auth_openidc sets the access token as OIDC_access_token environment variable + # Use RewriteRule to set it as Authorization header + RewriteEngine On + RewriteCond %{ENV:OIDC_access_token} ^(.+)$ + RewriteRule .* - [E=AUTH_HEADER:Bearer\ %1] + RequestHeader set Authorization "%{AUTH_HEADER}e" + + # Also set the tenant ID header (backup method) + RequestHeader set Fineract-Platform-TenantId "default" + + + #============================================================================ + # OAUTH2 CALLBACK HANDLER + #============================================================================ + + # OAuth2 callback endpoint - handled by mod_auth_openidc + # This MUST come before any ProxyPass directives + + AuthType openid-connect + Require valid-user + + + #============================================================================ + # FRONTEND WEB APPLICATIONS (Protected by OIDC) + # Using Docker service names and correct internal ports + #============================================================================ + + # Admin App - /admin/* (Commented out - image not available) + # + # AuthType openid-connect + # Require valid-user + # ProxyPass http://admin-app:80/ + # ProxyPassReverse http://admin-app:80/ + # + + # Account Manager App - /account/* + # The app is built to be served at /account/ path (not /account-manager/) + + AuthType openid-connect + Require valid-user + ProxyPass http://accountmanager-app:80/account/ + ProxyPassReverse http://accountmanager-app:80/account/ + + # Export JWT claims + RequestHeader set X-Staff-Id "%{OIDC_CLAIM_staffId}e" + Header set Set-Cookie "staffId=%{OIDC_CLAIM_staffId}e; Path=/; SameSite=Lax" + + + # Branch Manager App - /branchmanager/* + # The app is built to be served at /branchmanager/ path (not /branch-manager/) + + AuthType openid-connect + Require valid-user + ProxyPass http://branchmanager-app:80/branchmanager/ + ProxyPassReverse http://branchmanager-app:80/branchmanager/ + + # Export JWT claims + RequestHeader set X-Staff-Id "%{OIDC_CLAIM_staffId}e" + Header set Set-Cookie "staffId=%{OIDC_CLAIM_staffId}e; Path=/; SameSite=Lax" + + + # Cashier App - /cashier/* + # The app is built to be served at /cashier/ path + + AuthType openid-connect + Require valid-user + ProxyPass http://cashier-app:80/cashier/ + ProxyPassReverse http://cashier-app:80/cashier/ + + # Export JWT claims + RequestHeader set X-Staff-Id "%{OIDC_CLAIM_staffId}e" + Header set Set-Cookie "staffId=%{OIDC_CLAIM_staffId}e; Path=/; SameSite=Lax" + + + # Mifos Web App - /mifos/* (Commented out - not compatible with OAuth mode) + # + # AuthType openid-connect + # Require valid-user + # ProxyPass http://community-app:80/ + # ProxyPassReverse http://community-app:80/ + # + + # Root redirect - redirect to cashier app by default + RedirectMatch ^/$ /cashier + + diff --git a/custom/README.md b/custom/README.md new file mode 100644 index 00000000000..80b5fb6bf34 --- /dev/null +++ b/custom/README.md @@ -0,0 +1,106 @@ +# Fineract Custom Modules + + +> **Note:** This document is a summary of the official Fineract documentation for custom modules. For the complete and most up-to-date information, please refer to the [official documentation](https://fineract.apache.org/docs/current/#_custom_modules). +This document provides a guide for creating custom modules to extend or override Fineract's default functionality, based on the official documentation. + +> **Note:** Currently, custom modules are a proof of concept feature in Fineract. + +## Introduction + +Custom modules in Fineract allow for easy customization of services. The approach is designed to work with future "clean room" module guidelines and avoids the need for major refactorings. The key principle is a folder structure convention that prevents extensions from clashing with Fineract's internal code, making downstream forks easier to sync. Editing core sources in `fineract-provider` is not recommended. + +Currently, the primary services prepared for overriding are `NoteReadPlatformService` and `NoteWritePlatformService`. For other services, it is recommended to consult the developer mailing list. + +## Benefits of the Convention + +Following the recommended folder structure provides significant advantages: +- You do not need to edit `settings.gradle` to include your new custom modules; they are picked up automatically. +- Your modules will be automatically included in custom Fineract Docker image builds. + +## Folder Structure and Instructions + +The process involves creating a specific directory structure and configuration files. + +### Step 1: Create Company and Domain Folders + +1. Inside the `custom` directory, create a folder named after your company or organization (e.g., `acme`). This acts as a namespace to prevent clashes with modules from other organizations. +2. Inside your company folder, create a folder for the category or domain your module targets (e.g., `note`, `loan`, `client`). + +An example structure would be `custom/acme/note`. + +### Step 2: Set Up Module Libraries + +Within your domain folder, create subdirectories for your actual libraries. Common libraries include: +- `service`: For your custom service implementations that extend or replace existing Fineract services. +- `core`: If you need to add additional Data Transfer Objects (DTOs) or other core components. +- `starter`: A mandatory Spring Boot auto-configuration library to ensure your module is integrated seamlessly. + +### Step 3: Configure the Build Files + +Each module library (`service`, `core`, `starter`) must have its own `build.gradle` and `dependencies.gradle` files. + +A typical `build.gradle` would define the project's description, group, and archive name. A `dependencies.gradle` file would declare dependencies on other Fineract modules like `fineract-core` and `fineract-provider`. + +**Important:** Do not add your custom module as a dependency in `fineract-provider`'s `dependencies.gradle` file, as this will create a circular dependency and fail the build. + +### Step 4: Implement the Starter and Auto-Configuration + +The `starter` library is crucial for integrating your module. It uses Spring Boot's auto-configuration capabilities. +1. Create a Java configuration class annotated with `@Configuration`. This class will define the beans for your custom services. To replace a default Fineract service, you can use the `@ConditionalOnMissingBean` annotation on your bean definition. This tells Spring to only create your bean if one doesn't already exist, allowing your custom module to override the default one. +2. Create a file at `src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`. +3. In this file, add the fully qualified name of your auto-configuration class, for example: `com.acme.fineract.portfolio.note.starter.AcmeNoteAutoConfiguration`. + +This setup allows Fineract to automatically discover and load your custom module without any manual configuration. + +## Custom Business Steps for Close of Business (COB) + +You can also add custom business steps to Fineract's default COB processing. +1. Create a custom module as described above (e.g., `custom/acme/steps`). +2. Create a class that implements the `org.apache.fineract.cob.COBBusinessStep` interface. +3. Provide a custom database migration script to add the necessary information about your new business step into the `m_batch_business_steps` table. + +## Custom Database Migration + +If your customizations require database changes, you can add your own migration scripts by following these conventions: + +1. **Create Changelog Folders**: In one of your module's `resources` folders (the `starter` library is recommended), create the directory path `db/custom-changelog`. +2. **Create a Changelog File**: Inside `db/custom-changelog`, create an XML changelog file (e.g., `changelog-acme-note.xml`). It's best to use a consistent naming convention to avoid classpath conflicts. +3. **Create a 'parts' Folder**: Inside `db/custom-changelog`, create a `parts` folder. This is where your specific changelog scripts will be placed. + +By following this structure, Fineract will automatically pick up and run your custom database migration scripts. + +## Deploying Custom Modules + +Once your custom modules are built, they need to be deployed into your Fineract instance. + +### JAR Deployment + +If you are running Fineract from the Spring Boot JAR file, you can simply drop your custom module JAR files into Fineract's `libs` folder. Dynamic loading of external JARs has been supported since Fineract version 1.5.0. + +### Docker Deployment + +For convenience, a separate Docker image module is provided that automatically includes your custom modules. You can find this in the `custom/docker` directory. + +To build the custom Docker image, run the following command from the Fineract root directory: + +```bash +./gradlew :custom:docker:jibDockerBuild +``` + +The resulting Docker image, which includes your custom modules, will be named `fineract-custom`. + +## Custom Batch Jobs + +Fineract provides extension points to define custom batch jobs using the module system. This allows you to define and configure custom jobs that run alongside Fineract's default batch jobs to extend or customize batch processing. + +Batch jobs in Fineract are implemented using **Spring Batch**, and automatic scheduling is handled by the **Quartz Scheduler**. It is also possible to trigger batch jobs via regular APIs. + +### Defining a Custom Job + +To define a custom batch job, follow these steps: + +1. **Create a Custom Module**: First, create a custom module for your job (e.g., `custom/acme/loan/job`), following the standard instructions for creating a custom module. +2. **Create a Job Configuration**: Create a job configuration class to register the job, its steps, and its tasklet with the job builder factory (e.g., `com.acme.fineract.loan.job.AcmeNoopJobConfiguration`). +3. **Create a Tasklet**: Create a tasklet class that contains the execution functionality for the job (e.g., `com.acme.fineract.loan.job.AcmeNoopJobTasklet`). +4. **Provide a Custom Database Migration**: Provide a custom database migration to add the necessary information about your job in the `job` table. diff --git a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml b/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml index b3a8f5443fb..e87238eb330 100644 --- a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml +++ b/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml @@ -26,6 +26,7 @@ + @@ -42,10 +43,4 @@ - - - - name='Acme Noop Job' - - diff --git a/custom/adorsys/batch/service/build.gradle b/custom/adorsys/batch/service/build.gradle new file mode 100644 index 00000000000..f6dc91d7bf9 --- /dev/null +++ b/custom/adorsys/batch/service/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Custom Batch API Strategies Service' + +group = 'com.adorsys.fineract.batch' + +base { + archivesName = 'adorsys-fineract-batch-service' +} + +apply from: 'dependencies.gradle' diff --git a/custom/adorsys/batch/service/dependencies.gradle b/custom/adorsys/batch/service/dependencies.gradle new file mode 100644 index 00000000000..d2a27a0e15c --- /dev/null +++ b/custom/adorsys/batch/service/dependencies.gradle @@ -0,0 +1,11 @@ +dependencies { + implementation(project(':fineract-core')) + implementation(project(':fineract-provider')) + implementation(project(':fineract-savings')) + implementation(project(':fineract-accounting')) + implementation('org.apache.httpcomponents:httpcore') + implementation('com.google.guava:guava') + implementation('com.google.code.gson:gson') + compileOnly('org.springframework.boot:spring-boot-autoconfigure') + compileOnly('jakarta.ws.rs:jakarta.ws.rs-api') +} diff --git a/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ActivateSavingsAccountCommandStrategy.java b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ActivateSavingsAccountCommandStrategy.java new file mode 100644 index 00000000000..3dc01f5c2c4 --- /dev/null +++ b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ActivateSavingsAccountCommandStrategy.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.batch.command; + +import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion; + +import com.google.common.base.Splitter; +import jakarta.ws.rs.core.UriInfo; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Batch command strategy for activating savings accounts. Delegates to + * {@link SavingsAccountsApiResource#update(Long, String, String)} with command=activate. + */ +@Component +@RequiredArgsConstructor +public class ActivateSavingsAccountCommandStrategy implements CommandStrategy { + + private final SavingsAccountsApiResource savingsAccountsApiResource; + + @Override + public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) { + String relativeUrl = relativeUrlWithoutVersion(request); + // URL: savingsaccounts/{id}?command=activate + String pathPart = relativeUrl.contains("?") ? relativeUrl.substring(0, relativeUrl.indexOf('?')) : relativeUrl; + List pathParameters = Splitter.on('/').splitToList(pathPart); + Long savingsAccountId = Long.parseLong(pathParameters.get(1)); + + String responseBody = savingsAccountsApiResource.update(savingsAccountId, request.getBody(), "activate"); + + return new BatchResponse().setRequestId(request.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody) + .setHeaders(request.getHeaders()); + } +} diff --git a/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/AdorsysBatchStrategyRegistrar.java b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/AdorsysBatchStrategyRegistrar.java new file mode 100644 index 00000000000..d45fe06b8f9 --- /dev/null +++ b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/AdorsysBatchStrategyRegistrar.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.batch.command; + +import static jakarta.ws.rs.HttpMethod.POST; + +import java.util.Map; +import org.apache.fineract.batch.command.CommandContext; +import org.apache.fineract.batch.command.CommandStrategyRegistrar; +import org.springframework.stereotype.Component; + +/** + * Registers batch command strategies for operations not supported by upstream Apache Fineract: + * + */ +@Component +public class AdorsysBatchStrategyRegistrar implements CommandStrategyRegistrar { + + private static final String NUMBER_REGEX = "\\d+"; + private static final String OPTIONAL_COMMAND_PARAM_REGEX = "(\\?command=[\\w\\-]+)?"; + + @Override + public void register(Map strategies) { + // Journal entries: POST /v1/journalentries or POST /v1/journalentries?command=... + strategies.put(CommandContext.resource("v1\\/journalentries" + OPTIONAL_COMMAND_PARAM_REGEX).method(POST).build(), + "createJournalEntryCommandStrategy"); + + // Account transfers: POST /v1/accounttransfers + strategies.put(CommandContext.resource("v1\\/accounttransfers").method(POST).build(), "createAccountTransferCommandStrategy"); + + // Savings account approval: POST /v1/savingsaccounts/{id}?command=approve + // Fineract's batch API convention uses POST for state-transition commands + // (same as activateClientCommandStrategy, approveLoanCommandStrategy in core). + // The strategy delegates to SavingsAccountsApiResource.update() directly. + strategies.put(CommandContext.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\?command=approve").method(POST).build(), + "approveSavingsAccountCommandStrategy"); + + // Savings account activation: POST /v1/savingsaccounts/{id}?command=activate + strategies.put(CommandContext.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\?command=activate").method(POST).build(), + "activateSavingsAccountCommandStrategy"); + } +} diff --git a/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ApproveSavingsAccountCommandStrategy.java b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ApproveSavingsAccountCommandStrategy.java new file mode 100644 index 00000000000..6ad93cbff1b --- /dev/null +++ b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/ApproveSavingsAccountCommandStrategy.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.batch.command; + +import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion; + +import com.google.common.base.Splitter; +import jakarta.ws.rs.core.UriInfo; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Batch command strategy for approving savings accounts. Delegates to + * {@link SavingsAccountsApiResource#update(Long, String, String)} with command=approve. + */ +@Component +@RequiredArgsConstructor +public class ApproveSavingsAccountCommandStrategy implements CommandStrategy { + + private final SavingsAccountsApiResource savingsAccountsApiResource; + + @Override + public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) { + String relativeUrl = relativeUrlWithoutVersion(request); + // URL: savingsaccounts/{id}?command=approve + String pathPart = relativeUrl.contains("?") ? relativeUrl.substring(0, relativeUrl.indexOf('?')) : relativeUrl; + List pathParameters = Splitter.on('/').splitToList(pathPart); + Long savingsAccountId = Long.parseLong(pathParameters.get(1)); + + String responseBody = savingsAccountsApiResource.update(savingsAccountId, request.getBody(), "approve"); + + return new BatchResponse().setRequestId(request.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody) + .setHeaders(request.getHeaders()); + } +} diff --git a/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/CreateJournalEntryCommandStrategy.java b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/CreateJournalEntryCommandStrategy.java new file mode 100644 index 00000000000..f8a426e413a --- /dev/null +++ b/custom/adorsys/batch/service/src/main/java/com/adorsys/fineract/batch/command/CreateJournalEntryCommandStrategy.java @@ -0,0 +1,58 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.batch.command; + +import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion; + +import jakarta.ws.rs.core.UriInfo; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.journalentry.api.JournalEntriesApiResource; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.command.CommandStrategyUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Batch command strategy for creating GL journal entries. Delegates to + * {@link JournalEntriesApiResource#createGLJournalEntry(String, String)}. + */ +@Component +@RequiredArgsConstructor +public class CreateJournalEntryCommandStrategy implements CommandStrategy { + + private final JournalEntriesApiResource journalEntriesApiResource; + + @Override + public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) { + String relativeUrl = relativeUrlWithoutVersion(request); + String command = null; + if (relativeUrl.indexOf('?') > 0) { + Map queryParameters = CommandStrategyUtils.getQueryParameters(relativeUrl); + command = queryParameters.get("command"); + } + + String responseBody = journalEntriesApiResource.createGLJournalEntry(request.getBody(), command); + + return new BatchResponse().setRequestId(request.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody) + .setHeaders(request.getHeaders()); + } +} diff --git a/custom/adorsys/batch/starter/build.gradle b/custom/adorsys/batch/starter/build.gradle new file mode 100644 index 00000000000..da733b77758 --- /dev/null +++ b/custom/adorsys/batch/starter/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Custom Batch API Strategies Starter' + +group = 'com.adorsys.fineract.batch' + +base { + archivesName = 'adorsys-fineract-batch-starter' +} + +apply from: 'dependencies.gradle' diff --git a/custom/adorsys/batch/starter/dependencies.gradle b/custom/adorsys/batch/starter/dependencies.gradle new file mode 100644 index 00000000000..c823010b356 --- /dev/null +++ b/custom/adorsys/batch/starter/dependencies.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation(project(':custom:adorsys:batch:service')) + implementation('org.springframework.boot:spring-boot-starter') +} diff --git a/custom/adorsys/batch/starter/src/main/java/com/adorsys/fineract/batch/starter/AdorsysBatchAutoConfiguration.java b/custom/adorsys/batch/starter/src/main/java/com/adorsys/fineract/batch/starter/AdorsysBatchAutoConfiguration.java new file mode 100644 index 00000000000..16cae8932ea --- /dev/null +++ b/custom/adorsys/batch/starter/src/main/java/com/adorsys/fineract/batch/starter/AdorsysBatchAutoConfiguration.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.batch.starter; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.ComponentScan; + +@AutoConfiguration +@ComponentScan("com.adorsys.fineract.batch") +@ConditionalOnProperty(name = "adorsys.batch.enabled", havingValue = "true", matchIfMissing = true) +public class AdorsysBatchAutoConfiguration {} diff --git a/custom/adorsys/batch/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/custom/adorsys/batch/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..bff5f6aa028 --- /dev/null +++ b/custom/adorsys/batch/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.adorsys.fineract.batch.starter.AdorsysBatchAutoConfiguration diff --git a/custom/adorsys/monetary/service/build.gradle b/custom/adorsys/monetary/service/build.gradle new file mode 100644 index 00000000000..e5d108f4d3e --- /dev/null +++ b/custom/adorsys/monetary/service/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Custom Currency Service' + +group = 'com.adorsys.fineract.monetary' + +base { + archivesName = 'adorsys-fineract-currency-service' +} + +apply from: 'dependencies.gradle' diff --git a/custom/adorsys/monetary/service/dependencies.gradle b/custom/adorsys/monetary/service/dependencies.gradle new file mode 100644 index 00000000000..a4561188b2d --- /dev/null +++ b/custom/adorsys/monetary/service/dependencies.gradle @@ -0,0 +1,12 @@ +dependencies { + implementation(project(':fineract-core')) + implementation(project(':fineract-provider')) + implementation(project(':fineract-loan')) + implementation(project(':fineract-savings')) + implementation(project(':fineract-charge')) + compileOnly('org.springframework.boot:spring-boot-autoconfigure') + compileOnly('org.springframework.boot:spring-boot-starter-data-jpa') + compileOnly('org.eclipse.persistence:eclipselink:4.0.6') + compileOnly('jakarta.ws.rs:jakarta.ws.rs-api') + compileOnly('io.swagger.core.v3:swagger-annotations-jakarta') +} diff --git a/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/api/CustomCurrencyApiResource.java b/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/api/CustomCurrencyApiResource.java new file mode 100644 index 00000000000..af5a526467f --- /dev/null +++ b/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/api/CustomCurrencyApiResource.java @@ -0,0 +1,48 @@ +package com.adorsys.fineract.organisation.monetary.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository; +import org.apache.fineract.organisation.monetary.exception.CurrencyNotFoundException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * REST endpoint for managing custom (non-ISO) currencies. Provides DELETE for cleanup during asset provisioning + * rollback. + */ +@Slf4j +@Path("/v1/currencies/custom") +@Component +@RequiredArgsConstructor +@ConditionalOnProperty("adorsys.currency.enabled") +@Tag(name = "Custom Currency", description = "Manage custom token currencies for asset tokenization.") +public class CustomCurrencyApiResource { + + private final ApplicationCurrencyRepository currencyRepository; + + @DELETE + @Path("/{currencyCode}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Transactional + @Operation(summary = "Delete Custom Currency", description = "Removes a custom currency from the reference table. Used during asset rollback.") + public void deleteCustomCurrency(@PathParam("currencyCode") final String currencyCode) { + final ApplicationCurrency currency = currencyRepository.findOneByCode(currencyCode); + if (currency == null) { + throw new CurrencyNotFoundException(currencyCode); + } + currencyRepository.delete(currency); + log.info("Deleted custom currency: {}", currencyCode); + } +} diff --git a/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/service/CustomCurrencyWritePlatformService.java b/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/service/CustomCurrencyWritePlatformService.java new file mode 100644 index 00000000000..83f3f548c0a --- /dev/null +++ b/custom/adorsys/monetary/service/src/main/java/com/adorsys/fineract/organisation/monetary/service/CustomCurrencyWritePlatformService.java @@ -0,0 +1,86 @@ +package com.adorsys.fineract.organisation.monetary.service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository; +import org.apache.fineract.organisation.monetary.domain.OrganisationCurrency; +import org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepository; +import org.apache.fineract.organisation.monetary.exception.CurrencyInUseException; +import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService; +import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; +import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; +import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Custom CurrencyWritePlatformService that auto-creates unknown currencies in the m_currency reference table when they + * are submitted via PUT /currencies. This enables tokenized asset platforms to register custom currency codes (e.g. + * DTT, YMT) without needing direct database access. + */ +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty("adorsys.currency.enabled") +public class CustomCurrencyWritePlatformService implements CurrencyWritePlatformService, InitializingBean { + + private final ApplicationCurrencyRepository currencyRepository; + private final OrganisationCurrencyRepository organisationCurrencyRepository; + private final LoanProductReadPlatformService loanProductService; + private final SavingsProductReadPlatformService savingsProductService; + private final ChargeReadPlatformService chargeService; + + @Override + public void afterPropertiesSet() { + log.info("Custom Currency Write Service active: auto-creation of unknown currencies enabled"); + } + + @Transactional + @Override + public CurrencyUpdateResponse updateAllowedCurrencies(final CurrencyUpdateRequest request) { + final var currencies = request.getCurrencies(); + + final List allowedCurrencyCodes = new ArrayList<>(); + final Set allowedCurrencies = new HashSet<>(); + + for (final String currencyCode : currencies) { + ApplicationCurrency currency = currencyRepository.findOneByCode(currencyCode); + + if (currency == null) { + // Auto-create the currency in the reference table + currency = new ApplicationCurrency(currencyCode, currencyCode, 0, 1, "currency." + currencyCode, currencyCode); + currency = currencyRepository.save(currency); + log.info("Auto-created custom currency in m_currency: {}", currencyCode); + } + + final OrganisationCurrency allowedCurrency = currency.toOrganisationCurrency(); + allowedCurrencyCodes.add(currencyCode); + allowedCurrencies.add(allowedCurrency); + } + + // Validate that currencies being removed are not in use + for (OrganisationCurrency priorCurrency : organisationCurrencyRepository.findAll()) { + if (!allowedCurrencyCodes.contains(priorCurrency.getCode())) { + if (!loanProductService.retrieveAllLoanProductsForCurrency(priorCurrency.getCode()).isEmpty() + || !savingsProductService.retrieveAllForCurrency(priorCurrency.getCode()).isEmpty() + || !chargeService.retrieveAllChargesForCurrency(priorCurrency.getCode()).isEmpty()) { + throw new CurrencyInUseException(priorCurrency.getCode()); + } + } + } + + organisationCurrencyRepository.deleteAll(); + organisationCurrencyRepository.saveAll(allowedCurrencies); + + return CurrencyUpdateResponse.builder().currencies(allowedCurrencyCodes).build(); + } +} diff --git a/custom/adorsys/monetary/starter/build.gradle b/custom/adorsys/monetary/starter/build.gradle new file mode 100644 index 00000000000..5ba2ae2b979 --- /dev/null +++ b/custom/adorsys/monetary/starter/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Custom Currency Starter' + +group = 'com.adorsys.fineract.monetary' + +base { + archivesName = 'adorsys-fineract-currency-starter' +} + +apply from: 'dependencies.gradle' diff --git a/custom/adorsys/monetary/starter/dependencies.gradle b/custom/adorsys/monetary/starter/dependencies.gradle new file mode 100644 index 00000000000..fd1e222d8a9 --- /dev/null +++ b/custom/adorsys/monetary/starter/dependencies.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation(project(':custom:adorsys:monetary:service')) + implementation('org.springframework.boot:spring-boot-starter') +} diff --git a/custom/adorsys/monetary/starter/src/main/java/com/adorsys/fineract/organisation/monetary/starter/AdorsysCurrencyAutoConfiguration.java b/custom/adorsys/monetary/starter/src/main/java/com/adorsys/fineract/organisation/monetary/starter/AdorsysCurrencyAutoConfiguration.java new file mode 100644 index 00000000000..4dc225c9185 --- /dev/null +++ b/custom/adorsys/monetary/starter/src/main/java/com/adorsys/fineract/organisation/monetary/starter/AdorsysCurrencyAutoConfiguration.java @@ -0,0 +1,10 @@ +package com.adorsys.fineract.organisation.monetary.starter; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.ComponentScan; + +@AutoConfiguration +@ComponentScan("com.adorsys.fineract.organisation.monetary") +@ConditionalOnProperty("adorsys.currency.enabled") +public class AdorsysCurrencyAutoConfiguration {} diff --git a/custom/adorsys/monetary/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/custom/adorsys/monetary/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..efe15eb2a3e --- /dev/null +++ b/custom/adorsys/monetary/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.adorsys.fineract.organisation.monetary.starter.AdorsysCurrencyAutoConfiguration diff --git a/custom/adorsys/userandstaff/api/build.gradle b/custom/adorsys/userandstaff/api/build.gradle new file mode 100644 index 00000000000..d8241808f86 --- /dev/null +++ b/custom/adorsys/userandstaff/api/build.gradle @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law_ 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. + */ +description = 'Adorsys Fineract User and Staff API' + +group = 'com.adorsys.fineract' + +base { + archivesName = 'adorsys-fineract-userandstaff-api' +} + +apply from: 'dependencies.gradle' \ No newline at end of file diff --git a/custom/adorsys/userandstaff/api/dependencies.gradle b/custom/adorsys/userandstaff/api/dependencies.gradle new file mode 100644 index 00000000000..2f7211250e7 --- /dev/null +++ b/custom/adorsys/userandstaff/api/dependencies.gradle @@ -0,0 +1,20 @@ +dependencies { + implementation project(':fineract-command') + implementation project(':fineract-branch') + implementation project(':fineract-core') + implementation project(':fineract-provider') + implementation project(':custom:adorsys:userandstaff:data') + implementation project(':custom:adorsys:userandstaff:service') + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0' + implementation 'io.swagger.core.v3:swagger-annotations:2.2.20' + implementation('org.springframework.boot:spring-boot-starter-data-jpa') { + exclude group: 'org.hibernate' + } + implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') { + exclude group: 'org.eclipse.persistence', module: 'jakarta.persistence' + } + implementation('org.springframework.boot:spring-boot-starter-security') + implementation project(':fineract-provider') + implementation 'com.google.code.gson:gson' +} diff --git a/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResource.java b/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResource.java new file mode 100644 index 00000000000..512031e7273 --- /dev/null +++ b/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResource.java @@ -0,0 +1,105 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.api; + +import com.adorsys.fineract.userandstaff.data.EmployeeData; +import com.adorsys.fineract.userandstaff.service.AdorsysUserAndStaffReadPlatformService; +import com.adorsys.fineract.userandstaff.service.AdorsysUserAndStaffWritePlatformService; +import com.google.gson.JsonElement; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.stereotype.Component; + +@Path("/v1/adorsys/employees") +@Component +@Tag(name = "Employees", description = "Unified API for managing employees (Staff and AppUser)") +@RequiredArgsConstructor +public class AdorsysUserAndStaffApiResource { + + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "EMPLOYEE"; + + private final PlatformSecurityContext context; + private final AdorsysUserAndStaffWritePlatformService adorsysUserAndStaffWritePlatformService; + private final AdorsysUserAndStaffReadPlatformService adorsysUserAndStaffReadPlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final FromJsonHelper fromJsonHelper; + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Create an employee", description = "Creates an employee.\n\nMandatory Fields:\nofficeId, firstname, lastname, username, roles\n\nOptional Fields:\nisLoanOfficer, mobileNo, externalId, joiningDate") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = AdorsysUserAndStaffApiResourceSwagger.CreateEmployeeRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AdorsysUserAndStaffApiResourceSwagger.CreateEmployeeResponse.class))) }) + public CommandProcessingResult createEmployee( + @Parameter(hidden = true) final AdorsysUserAndStaffApiResourceSwagger.CreateEmployeeRequest request) { + this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME_FOR_PERMISSIONS); + final String json = this.toApiJsonSerializer.serialize(request); + final JsonElement jsonElement = this.fromJsonHelper.parse(json); + final JsonCommand command = JsonCommand.fromJsonElement(null, jsonElement, this.fromJsonHelper); + return this.adorsysUserAndStaffWritePlatformService.createEmployee(command); + } + + @GET + @Path("{userId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Retrieve an Employee", description = "Returns the details of an Employee.") + public EmployeeData retrieveEmployee(@PathParam("userId") @Parameter(description = "userId") final Long userId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return this.adorsysUserAndStaffReadPlatformService.retrieveEmployee(userId); + } + + @PUT + @Path("{userId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Update an Employee", description = "Updates the details of an employee.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = AdorsysUserAndStaffApiResourceSwagger.PutEmployeeRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AdorsysUserAndStaffApiResourceSwagger.PutEmployeeResponse.class))) }) + public CommandProcessingResult updateEmployee(@PathParam("userId") @Parameter(description = "userId") final Long userId, + @Parameter(hidden = true) final AdorsysUserAndStaffApiResourceSwagger.PutEmployeeRequest request) { + this.context.authenticatedUser().validateHasUpdatePermission(RESOURCE_NAME_FOR_PERMISSIONS); + final String json = this.toApiJsonSerializer.serialize(request); + final JsonElement jsonElement = this.fromJsonHelper.parse(json); + final JsonCommand command = JsonCommand.fromJsonElement(userId, jsonElement, this.fromJsonHelper); + return this.adorsysUserAndStaffWritePlatformService.updateEmployee(userId, command); + } +} diff --git a/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResourceSwagger.java b/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResourceSwagger.java new file mode 100644 index 00000000000..9ae1e7f69a0 --- /dev/null +++ b/custom/adorsys/userandstaff/api/src/main/java/com/adorsys/fineract/userandstaff/api/AdorsysUserAndStaffApiResourceSwagger.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +final class AdorsysUserAndStaffApiResourceSwagger { + + private AdorsysUserAndStaffApiResourceSwagger() {} + + @Schema(description = "CreateEmployeeRequest") + public static final class CreateEmployeeRequest { + + private CreateEmployeeRequest() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "John") + public String firstname; + @Schema(example = "Doe") + public String lastname; + @Schema(example = "01 January 2023") + public String joiningDate; + @Schema(example = "1234567890") + public String mobileNo; + @Schema(example = "true") + public boolean isLoanOfficer; + @Schema(example = "JD001") + public String externalId; + @Schema(example = "johndoe") + public String username; + @Schema(example = "johndoe@example.com") + public String email; + @Schema(example = "[1]") + public Long[] roles; + } + + @Schema(description = "CreateEmployeeResponse") + public static final class CreateEmployeeResponse { + + private CreateEmployeeResponse() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "1") + public Long resourceId; + } + + @Schema(description = "PutEmployeeRequest") + public static final class PutEmployeeRequest { + + private PutEmployeeRequest() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "John") + public String firstname; + @Schema(example = "Doe") + public String lastname; + @Schema(example = "1234567890") + public String mobileNo; + @Schema(example = "true") + public boolean isLoanOfficer; + @Schema(example = "JD001") + public String externalId; + @Schema(example = "[1]") + public Long[] roles; + } + + @Schema(description = "PutEmployeeResponse") + public static final class PutEmployeeResponse { + + private PutEmployeeResponse() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "1") + public Long resourceId; + public PutEmployeeResponseChanges changes; + + public static final class PutEmployeeResponseChanges { + + private PutEmployeeResponseChanges() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "John") + public String firstname; + @Schema(example = "Doe") + public String lastname; + } + } +} diff --git a/custom/adorsys/userandstaff/data/build.gradle b/custom/adorsys/userandstaff/data/build.gradle new file mode 100644 index 00000000000..c9f2e0f2a06 --- /dev/null +++ b/custom/adorsys/userandstaff/data/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Fineract User and Staff Data' + +group = 'com.adorsys.fineract' + +base { + archivesName = 'adorsys-fineract-userandstaff-data' +} + +apply from: 'dependencies.gradle' \ No newline at end of file diff --git a/custom/adorsys/userandstaff/data/dependencies.gradle b/custom/adorsys/userandstaff/data/dependencies.gradle new file mode 100644 index 00000000000..b55b787e71b --- /dev/null +++ b/custom/adorsys/userandstaff/data/dependencies.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation(project(':fineract-core')) + implementation(project(':fineract-provider')) +} diff --git a/custom/adorsys/userandstaff/data/src/main/java/com/adorsys/fineract/userandstaff/data/EmployeeData.java b/custom/adorsys/userandstaff/data/src/main/java/com/adorsys/fineract/userandstaff/data/EmployeeData.java new file mode 100644 index 00000000000..6f760d41b90 --- /dev/null +++ b/custom/adorsys/userandstaff/data/src/main/java/com/adorsys/fineract/userandstaff/data/EmployeeData.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.data; + +import java.util.Collection; +import lombok.Data; +import org.apache.fineract.organisation.office.data.OfficeData; +import org.apache.fineract.organisation.staff.data.StaffData; +import org.apache.fineract.useradministration.data.RoleData; + +@Data +public class EmployeeData { + + private final Long id; + private final String username; + private final Long officeId; + private final String officeName; + private final String firstname; + private final String lastname; + private final String email; + private final Boolean passwordNeverExpires; + private final Collection allowedOffices; + private final Collection availableRoles; + private final Collection selectedRoles; + private final StaffData staff; + private final String mobileNo; + private final boolean isLoanOfficer; + private final String externalId; + + // The constructor is now public and will be called by the EmployeeDataMapper. + public EmployeeData(Long id, String username, Long officeId, String officeName, String firstname, String lastname, String email, + Boolean passwordNeverExpires, Collection allowedOffices, Collection availableRoles, + Collection selectedRoles, StaffData staff, String mobileNo, boolean isLoanOfficer, String externalId) { + this.id = id; + this.username = username; + this.officeId = officeId; + this.officeName = officeName; + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + this.passwordNeverExpires = passwordNeverExpires; + this.allowedOffices = allowedOffices; + this.availableRoles = availableRoles; + this.selectedRoles = selectedRoles; + this.staff = staff; + this.mobileNo = mobileNo; + this.isLoanOfficer = isLoanOfficer; + this.externalId = externalId; + } +} diff --git a/custom/adorsys/userandstaff/service/build.gradle b/custom/adorsys/userandstaff/service/build.gradle new file mode 100644 index 00000000000..30ccd50a623 --- /dev/null +++ b/custom/adorsys/userandstaff/service/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Fineract User and Staff Service' + +group = 'com.adorsys.fineract' + +base { + archivesName = 'adorsys-fineract-userandstaff-service' +} + +apply from: 'dependencies.gradle' \ No newline at end of file diff --git a/custom/adorsys/userandstaff/service/dependencies.gradle b/custom/adorsys/userandstaff/service/dependencies.gradle new file mode 100644 index 00000000000..de7b9835dd6 --- /dev/null +++ b/custom/adorsys/userandstaff/service/dependencies.gradle @@ -0,0 +1,13 @@ +dependencies { + implementation(project(':fineract-security')) + implementation(project(':fineract-core')) + implementation(project(':custom:adorsys:userandstaff:data')) + implementation('com.google.code.gson:gson') + implementation(project(':fineract-provider')) + implementation(project(':fineract-branch')) + implementation('org.springframework.boot:spring-boot-starter-jdbc') + implementation('org.springframework.boot:spring-boot-starter-data-jpa') + implementation('org.springframework.boot:spring-boot-starter-security') + implementation('org.eclipse.persistence:org.eclipse.persistence.jpa') + compileOnly('org.springframework.boot:spring-boot-autoconfigure') +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformService.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformService.java new file mode 100644 index 00000000000..9c9f75c04b0 --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformService.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.service; + +import com.adorsys.fineract.userandstaff.data.EmployeeData; + +public interface AdorsysUserAndStaffReadPlatformService { + + EmployeeData retrieveEmployee(Long userId); +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformServiceImpl.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformServiceImpl.java new file mode 100644 index 00000000000..f2953726bf0 --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffReadPlatformServiceImpl.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.service; + +import com.adorsys.fineract.userandstaff.data.EmployeeData; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.apache.fineract.organisation.staff.data.StaffData; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.service.StaffReadService; +import org.apache.fineract.useradministration.data.AppUserData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserRepository; +import org.apache.fineract.useradministration.exception.UserNotFoundException; +import org.apache.fineract.useradministration.service.AppUserReadPlatformService; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class AdorsysUserAndStaffReadPlatformServiceImpl implements AdorsysUserAndStaffReadPlatformService { + + private final AppUserReadPlatformService appUserReadPlatformService; + private final StaffReadService staffReadPlatformService; + private final AppUserRepository appUserRepository; + private final EmployeeDataMapper employeeDataMapper; + + @Override + public EmployeeData retrieveEmployee(Long userId) { + // 1. Retrieve the base user data DTO + final AppUserData userData = this.appUserReadPlatformService.retrieveUser(userId); + + // 2. Retrieve the full user entity to get the reliable staff link + final AppUser user = this.appUserRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + final Staff staff = user.getStaff(); + + // 3. Retrieve staff data if a link exists + StaffData staffData = null; + if (staff != null) { + staffData = this.staffReadPlatformService.retrieveStaff(staff.getId()); + } + + // 4. Map all data into the final EmployeeData object + Map mappedData = this.employeeDataMapper.map(userData, staffData); + return (EmployeeData) mappedData.get("employeeData"); + } +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformService.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformService.java new file mode 100644 index 00000000000..cc256fbeff4 --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformService.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface AdorsysUserAndStaffWritePlatformService { + + CommandProcessingResult createEmployee(JsonCommand command); + + CommandProcessingResult updateEmployee(Long userId, JsonCommand command); +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformServiceImpl.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformServiceImpl.java new file mode 100644 index 00000000000..3672b8b1043 --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/AdorsysUserAndStaffWritePlatformServiceImpl.java @@ -0,0 +1,193 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.service; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.organisation.staff.data.StaffCreateRequest; +import org.apache.fineract.organisation.staff.data.StaffCreateResponse; +import org.apache.fineract.organisation.staff.data.StaffUpdateRequest; +import org.apache.fineract.organisation.staff.data.StaffUpdateResponse; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.service.StaffWriteService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserRepository; +import org.apache.fineract.useradministration.exception.UserNotFoundException; +import org.apache.fineract.useradministration.service.AppUserWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@AllArgsConstructor +public class AdorsysUserAndStaffWritePlatformServiceImpl implements AdorsysUserAndStaffWritePlatformService { + + private final StaffWriteService staffWriteService; + private final AppUserWritePlatformService appUserWritePlatformService; + private final AppUserRepository appUserRepository; + private final FromJsonHelper fromJsonHelper; + + private static final String OFFICE_ID = "officeId"; + private static final String FIRSTNAME = "firstname"; + private static final String LASTNAME = "lastname"; + private static final String JOINING_DATE = "joiningDate"; + private static final String MOBILE_NO = "mobileNo"; + private static final String IS_LOAN_OFFICER = "isLoanOfficer"; + private static final String EXTERNAL_ID = "externalId"; + private static final String USERNAME = "username"; + private static final String EMAIL = "email"; + private static final String ROLES = "roles"; + + @Override + @Transactional + public CommandProcessingResult createEmployee(JsonCommand command) { + final JsonObject jsonObject = command.parsedJson().getAsJsonObject(); + + final Long officeId = jsonObject.get(OFFICE_ID).getAsLong(); + final String firstname = jsonObject.get(FIRSTNAME).getAsString(); + final String lastname = jsonObject.get(LASTNAME).getAsString(); + final String joiningDate = jsonObject.get(JOINING_DATE).getAsString(); + final String mobileNo = jsonObject.has(MOBILE_NO) ? jsonObject.get(MOBILE_NO).getAsString() : null; + final boolean isLoanOfficer = jsonObject.has(IS_LOAN_OFFICER) && jsonObject.get(IS_LOAN_OFFICER).getAsBoolean(); + final String externalId = jsonObject.has(EXTERNAL_ID) ? jsonObject.get(EXTERNAL_ID).getAsString() : null; + + StaffCreateRequest request = StaffCreateRequest.builder().officeId(officeId).firstname(firstname).lastname(lastname) + .joiningDate(joiningDate).mobileNo(mobileNo).isLoanOfficer(isLoanOfficer).externalId(externalId).dateFormat("dd MMMM yyyy") + .locale("en").build(); + + final StaffCreateResponse staffResult = this.staffWriteService.createStaff(request); + + final JsonObject userObject = new JsonObject(); + userObject.add(OFFICE_ID, jsonObject.get(OFFICE_ID)); + userObject.add(USERNAME, jsonObject.get(USERNAME)); + userObject.add(FIRSTNAME, jsonObject.get(FIRSTNAME)); + userObject.add(LASTNAME, jsonObject.get(LASTNAME)); + userObject.add(EMAIL, jsonObject.get(EMAIL)); + userObject.add(ROLES, jsonObject.get(ROLES)); + userObject.addProperty("staffId", staffResult.getResourceId()); + userObject.addProperty("sendPasswordToEmail", false); + final String userJson = this.fromJsonHelper.toJson(userObject); + final JsonElement userJsonElement = this.fromJsonHelper.parse(userJson); + final JsonCommand userCommand = JsonCommand.from(userJson, userJsonElement, fromJsonHelper, "USER", null, null, null, null, null, + null, null, null, null, null, null, null, null); + return this.appUserWritePlatformService.createUser(userCommand); + } + + @Override + @Transactional + public CommandProcessingResult updateEmployee(Long userId, JsonCommand command) { + CommandProcessingResult result = updateUser(userId, command); + + CommandProcessingResult staffResult = updateStaff(userId, command); + if (staffResult != null) { + result = staffResult; + } + + return result; + } + + private CommandProcessingResult updateUser(Long userId, JsonCommand command) { + final JsonObject jsonObject = command.parsedJson().getAsJsonObject(); + final JsonObject userObject = new JsonObject(); + if (jsonObject.has(USERNAME)) { + userObject.add(USERNAME, jsonObject.get(USERNAME)); + } + if (jsonObject.has(FIRSTNAME)) { + userObject.add(FIRSTNAME, jsonObject.get(FIRSTNAME)); + } + if (jsonObject.has(LASTNAME)) { + userObject.add(LASTNAME, jsonObject.get(LASTNAME)); + } + if (jsonObject.has(EMAIL)) { + userObject.add(EMAIL, jsonObject.get(EMAIL)); + } + if (jsonObject.has(ROLES)) { + userObject.add(ROLES, jsonObject.get(ROLES)); + } + if (jsonObject.has(OFFICE_ID)) { + userObject.add(OFFICE_ID, jsonObject.get(OFFICE_ID)); + } + + if (userObject.size() == 0) { + return CommandProcessingResult.empty(); + } + + final String userJson = this.fromJsonHelper.toJson(userObject); + final JsonElement userJsonElement = this.fromJsonHelper.parse(userJson); + final JsonCommand userCommand = JsonCommand.from(userJson, userJsonElement, fromJsonHelper, "USER", userId, null, null, null, null, + null, null, null, null, null, null, null, null); + return this.appUserWritePlatformService.updateUser(userId, userCommand); + } + + private CommandProcessingResult updateStaff(Long userId, JsonCommand command) { + final JsonObject jsonObject = command.parsedJson().getAsJsonObject(); + final StaffUpdateRequest request = new StaffUpdateRequest(); + boolean staffUpdateRequired = false; + + if (jsonObject.has(OFFICE_ID)) { + request.setOfficeId(jsonObject.get(OFFICE_ID).getAsLong()); + staffUpdateRequired = true; + } + if (jsonObject.has(FIRSTNAME)) { + request.setFirstname(jsonObject.get(FIRSTNAME).getAsString()); + staffUpdateRequired = true; + } + if (jsonObject.has(LASTNAME)) { + request.setLastname(jsonObject.get(LASTNAME).getAsString()); + staffUpdateRequired = true; + } + if (jsonObject.has(JOINING_DATE)) { + request.setJoiningDate(jsonObject.get(JOINING_DATE).getAsString()); + staffUpdateRequired = true; + } + if (jsonObject.has(MOBILE_NO)) { + request.setMobileNo(jsonObject.get(MOBILE_NO).getAsString()); + staffUpdateRequired = true; + } + if (jsonObject.has(IS_LOAN_OFFICER)) { + request.setIsLoanOfficer(jsonObject.get(IS_LOAN_OFFICER).getAsBoolean()); + staffUpdateRequired = true; + } + if (jsonObject.has(EXTERNAL_ID)) { + request.setExternalId(jsonObject.get(EXTERNAL_ID).getAsString()); + staffUpdateRequired = true; + } + + if (staffUpdateRequired) { + final AppUser user = this.appUserRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + final Staff staff = user.getStaff(); + if (staff != null) { + final Long staffId = staff.getId(); + request.setId(staffId); + final StaffUpdateResponse staffResult = this.staffWriteService.updateStaff(request); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(staffResult.getResourceId()) // + .withOfficeId(staffResult.getOfficeId()) // + .with(staffResult.getChanges()) // + .build(); + } + } + return null; + } +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/CustomTellerManagementReadPlatformServiceImpl.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/CustomTellerManagementReadPlatformServiceImpl.java new file mode 100644 index 00000000000..e2869b29ca0 --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/CustomTellerManagementReadPlatformServiceImpl.java @@ -0,0 +1,122 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +package com.adorsys.fineract.userandstaff.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.PaginationHelper; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; +import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; +import org.apache.fineract.organisation.staff.service.StaffReadService; +import org.apache.fineract.organisation.teller.data.CashierData; +import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +@Primary +public class CustomTellerManagementReadPlatformServiceImpl extends TellerManagementReadPlatformServiceImpl { + + private static final Logger LOG = LoggerFactory.getLogger(CustomTellerManagementReadPlatformServiceImpl.class); + private final JdbcTemplate jdbcTemplate; + + public CustomTellerManagementReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, final PlatformSecurityContext context, + final OfficeReadPlatformService officeReadPlatformService, final StaffReadService staffReadPlatformService, + final CurrencyReadPlatformService currencyReadPlatformService, final DatabaseSpecificSQLGenerator sqlGenerator, + final PaginationHelper paginationHelper, final SqlValidator sqlValidator) { + super(jdbcTemplate, context, officeReadPlatformService, staffReadPlatformService, currencyReadPlatformService, sqlGenerator, + paginationHelper, sqlValidator); + this.jdbcTemplate = jdbcTemplate; + LOG.info("Custom Teller Management Service has been initialized and is overriding the default implementation."); + } + + @Override + public Collection getCashierData(Long officeId, Long tellerId, Long staffId, LocalDate date) { + LOG.info("Executing custom getCashierData method with officeId: {}, tellerId: {}, staffId: {}, date: {}", officeId, tellerId, + staffId, date); + final CashierMapper cm = new CashierMapper(); + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select ").append(cm.schema()).append(" where 1=1 "); + + final List params = new ArrayList<>(); + + if (officeId != null) { + sqlBuilder.append(" and t.office_id = ? "); + params.add(officeId); + } + if (tellerId != null) { + sqlBuilder.append(" and c.teller_id = ? "); + params.add(tellerId); + } + if (staffId != null) { + sqlBuilder.append(" and c.staff_id = ? "); + params.add(staffId); + } + if (date != null) { + sqlBuilder.append(" and ? between c.start_date and c.end_date "); + params.add(date); + } + + return this.jdbcTemplate.query(sqlBuilder.toString(), cm, params.toArray()); + } + + private static final class CashierMapper implements RowMapper { + + public String schema() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("c.id as id,c.teller_id as teller_id, t.name as teller_name, c.description as description, "); + sqlBuilder.append("c.staff_id as staff_id, s.display_name as staff_name, "); + sqlBuilder.append("c.start_date as start_date, c.end_date as end_date, "); + sqlBuilder.append("c.full_day as full_day, c.start_time as start_time, c.end_time as end_time "); + sqlBuilder.append("from m_cashiers c "); + sqlBuilder.append("join m_tellers t on t.id = c.teller_id "); + sqlBuilder.append("join m_staff s on s.id = c.staff_id "); + return sqlBuilder.toString(); + } + + @Override + public CashierData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long tellerId = rs.getLong("teller_id"); + final String tellerName = rs.getString("teller_name"); + final Long staffId = rs.getLong("staff_id"); + final String staffName = rs.getString("staff_name"); + final String description = rs.getString("description"); + final LocalDate startDate = JdbcSupport.getLocalDate(rs, "start_date"); + final LocalDate endDate = JdbcSupport.getLocalDate(rs, "end_date"); + final Boolean fullDay = rs.getBoolean("full_day"); + final String startTime = rs.getString("start_time"); + final String endTime = rs.getString("end_time"); + return CashierData.instance(id, null, null, staffId, staffName, tellerId, tellerName, description, startDate, endDate, fullDay, + startTime, endTime); + } + } +} diff --git a/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/EmployeeDataMapper.java b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/EmployeeDataMapper.java new file mode 100644 index 00000000000..7eed5416a7e --- /dev/null +++ b/custom/adorsys/userandstaff/service/src/main/java/com/adorsys/fineract/userandstaff/service/EmployeeDataMapper.java @@ -0,0 +1,87 @@ +package com.adorsys.fineract.userandstaff.service; + +import com.adorsys.fineract.userandstaff.data.EmployeeData; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; +import org.apache.fineract.organisation.staff.data.StaffData; +import org.apache.fineract.useradministration.data.AppUserData; +import org.apache.fineract.useradministration.data.RoleData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class EmployeeDataMapper { + + private final ToApiJsonSerializer appUserJsonSerializer; + private final ToApiJsonSerializer staffJsonSerializer; + private final Gson gson = new Gson(); + + @Autowired + public EmployeeDataMapper(final ToApiJsonSerializer appUserJsonSerializer, + final ToApiJsonSerializer staffJsonSerializer) { + this.appUserJsonSerializer = appUserJsonSerializer; + this.staffJsonSerializer = staffJsonSerializer; + } + + public Map map(AppUserData userData, StaffData staffData) { + // Serialize AppUserData to JSON to access its fields + String appUserJson = this.appUserJsonSerializer.serialize(userData); + Type typeOfMap = new TypeToken>() {}.getType(); + Map appUserMap = this.gson.fromJson(appUserJson, typeOfMap); + + // Extract the required fields from the map + Long id = ((Number) appUserMap.get("id")).longValue(); + String username = (String) appUserMap.get("username"); + Long officeId = ((Number) appUserMap.get("officeId")).longValue(); + String officeName = (String) appUserMap.get("officeName"); + String firstname = (String) appUserMap.get("firstname"); + String lastname = (String) appUserMap.get("lastname"); + String email = (String) appUserMap.get("email"); + Boolean passwordNeverExpires = (Boolean) appUserMap.get("passwordNeverExpires"); + Number staffIdNumber = (Number) appUserMap.get("staffId"); + Long staffId = staffIdNumber != null ? staffIdNumber.longValue() : null; + + // Extract roles using a similar deserialization approach + Collection availableRoles = null; + if (appUserMap.containsKey("availableRoles")) { + String rolesJson = this.gson.toJson(appUserMap.get("availableRoles")); + Type typeOfRoles = new TypeToken>() {}.getType(); + availableRoles = this.gson.fromJson(rolesJson, typeOfRoles); + } + + Collection selectedRoles = null; + if (appUserMap.containsKey("selectedRoles")) { + String rolesJson = this.gson.toJson(appUserMap.get("selectedRoles")); + Type typeOfRoles = new TypeToken>() {}.getType(); + selectedRoles = this.gson.fromJson(rolesJson, typeOfRoles); + } + + // Extract staff-related fields + String mobileNo = null; + boolean isLoanOfficer = false; + String externalId = null; + + if (staffData != null) { + String staffJson = this.staffJsonSerializer.serialize(staffData); + Map staffMap = this.gson.fromJson(staffJson, typeOfMap); + mobileNo = (String) staffMap.get("mobileNo"); + isLoanOfficer = (Boolean) staffMap.get("isLoanOfficer"); + externalId = (String) staffMap.get("externalId"); + } + + // The collections are not exposed via getters, so we cannot access them directly. + // For now, we will pass null. A more complete solution would require a custom query. + // Create and return the new EmployeeData object + EmployeeData employeeData = new EmployeeData(id, username, officeId, officeName, firstname, lastname, email, passwordNeverExpires, + null, availableRoles, selectedRoles, staffData, mobileNo, isLoanOfficer, externalId); + Map result = new HashMap<>(); + result.put("employeeData", employeeData); + result.put("staffId", staffId); + return result; + } +} diff --git a/custom/adorsys/userandstaff/starter/build.gradle b/custom/adorsys/userandstaff/starter/build.gradle new file mode 100644 index 00000000000..6e29c91000f --- /dev/null +++ b/custom/adorsys/userandstaff/starter/build.gradle @@ -0,0 +1,9 @@ +description = 'Adorsys Fineract User and Staff Starter' + +group = 'com.adorsys.fineract' + +base { + archivesName = 'adorsys-fineract-userandstaff-starter' +} + +apply from: 'dependencies.gradle' \ No newline at end of file diff --git a/custom/adorsys/userandstaff/starter/dependencies.gradle b/custom/adorsys/userandstaff/starter/dependencies.gradle new file mode 100644 index 00000000000..e0e41e8981f --- /dev/null +++ b/custom/adorsys/userandstaff/starter/dependencies.gradle @@ -0,0 +1,7 @@ +dependencies { + implementation project(':fineract-provider') + implementation project(':custom:adorsys:userandstaff:service') + implementation project(':custom:adorsys:userandstaff:api') + implementation 'org.springframework.boot:spring-boot-starter' + compileOnly('org.springframework.boot:spring-boot-autoconfigure') +} diff --git a/custom/adorsys/userandstaff/starter/src/main/java/com/adorsys/fineract/userandstaff/starter/AdorsysUserAndStaffAutoConfiguration.java b/custom/adorsys/userandstaff/starter/src/main/java/com/adorsys/fineract/userandstaff/starter/AdorsysUserAndStaffAutoConfiguration.java new file mode 100644 index 00000000000..1e3143b717a --- /dev/null +++ b/custom/adorsys/userandstaff/starter/src/main/java/com/adorsys/fineract/userandstaff/starter/AdorsysUserAndStaffAutoConfiguration.java @@ -0,0 +1,17 @@ +package com.adorsys.fineract.userandstaff.starter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +@AutoConfiguration +@ComponentScan({ "com.adorsys.fineract.userandstaff.service", "com.adorsys.fineract.userandstaff.api" }) +public class AdorsysUserAndStaffAutoConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger(AdorsysUserAndStaffAutoConfiguration.class); + + public AdorsysUserAndStaffAutoConfiguration() { + LOG.info("Custom User and Staff Auto-Configuration has been loaded."); + } +} diff --git a/custom/adorsys/userandstaff/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/custom/adorsys/userandstaff/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..4ef80ca80b0 --- /dev/null +++ b/custom/adorsys/userandstaff/starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.adorsys.fineract.userandstaff.starter.AdorsysUserAndStaffAutoConfiguration \ No newline at end of file diff --git a/custom/docker/build.gradle b/custom/docker/build.gradle index 2822fe5376a..a1c4e87294a 100644 --- a/custom/docker/build.gradle +++ b/custom/docker/build.gradle @@ -53,6 +53,23 @@ jib { ports = ['8080/tcp', '8443/tcp'] labels = [maintainer: 'Aleksandar Vidakovic '] user = 'nobody:nogroup' + extraClasspath = ['/app/plugins/*'] + environment = [ + 'FINERACT_PENTAHO_REPORTS_PATH': '/pentahoReports/Postgresql/' + ] + } + + extraDirectories { + paths { + path { + from = file("${rootDir}/fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1") + into = '/app/plugins' + } + path { + from = file("${rootDir}/fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1/Postgresql") + into = '/pentahoReports/Postgresql' + } + } } allowInsecureRegistries = true diff --git a/docker-compose-adorsys.yml b/docker-compose-adorsys.yml new file mode 100644 index 00000000000..25c26642fd2 --- /dev/null +++ b/docker-compose-adorsys.yml @@ -0,0 +1,200 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# +services: + db: + extends: + file: ./config/docker/compose/mariadb.yml + service: mariadb + networks: + - fineract-network + + fineract: + extends: + file: ./config/docker/compose/fineract.yml + service: fineract + ports: + - "443:8443" + networks: + - fineract-network + depends_on: + db: + condition: service_healthy + keycloak: + condition: service_healthy + env_file: + - ./config/docker/env/fineract.env + - ./config/docker/env/fineract-common.env + - ./config/docker/env/fineract-mariadb.env + - ./config/docker/env/fineract-adorsys.env + volumes: + - ./fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1:/app/plugins + - ./fineract-adorsys-pentaho/MifosSecurityPlugin-1.12.1:/pentahoReports + + keycloak: + image: quay.io/keycloak/keycloak:26.2.5 + command: > + start-dev + --health-enabled=true + --spi-theme-static-max-age=-1 + --spi-theme-cache-themes=false + --spi-theme-cache-templates=false + --hostname-strict=false + --http-enabled=true + env_file: + - ./config/docker/env/fineract-adorsys.env + ports: + - "9000:8080" + - "8180:9000" + volumes: + - ./fineract-adorsys-keycloak-theme/theme-1.0.1.jar:/opt/keycloak/providers/theme-1.0.1.jar:ro + healthcheck: + test: ["CMD-SHELL", "timeout 3 sh -c 'cat < /dev/null > /dev/tcp/localhost/9000' || exit 1"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 60s + networks: + - fineract-network + + + fineract-config-cli: + build: + context: fineract-adorsys-data-collection/fineract-config-cli + dockerfile: Dockerfile + depends_on: + fineract: + condition: service_healthy + env_file: + - ./config/docker/env/fineract-adorsys.env + environment: + - IMPORT_FILES_LOCATIONS=/config/*.yml + volumes: + - ./fineract-adorsys-data-collection/fineract-config-cli/config:/config + - ./fineract-adorsys-data-collection/fineract-config-cli/data:/data + networks: + - fineract-network + restart: "no" + + + keycloak-config-cli: + image: adorsys/keycloak-config-cli:latest + depends_on: + keycloak: + condition: service_healthy + env_file: + - ./config/docker/env/fineract-adorsys.env + volumes: + - ./fineract-adorsys-keycloak-config:/config + networks: + - fineract-network + + keycloak-setup: + build: + context: . + dockerfile: Dockerfile.adorsys-keycloak-setup + depends_on: + keycloak-config-cli: + condition: service_completed_successfully + networks: + - fineract-network + + sms-gateway: + build: + context: fineract-adorsys-sms-gateway + dockerfile: Dockerfile + ports: + - "8080:8080" + depends_on: + fineract: + condition: service_healthy + networks: + - fineract-network + + apache-proxy: + build: + context: config/fineract-adorsys-apache + depends_on: + - fineract + - keycloak + # - admin-app # Commented out - image not available in registry + # - cashier-app + # - branchmanager-app + # - accountmanager-app + # - mifos-app # Commented out - image not available in registry + ports: + - "80:80" + volumes: + - ./config/fineract-adorsys-apache/httpd.conf:/usr/local/apache2/conf/httpd.conf + networks: + - fineract-network + + # admin-app: + # image: ghcr.io/adorsys-gis/fineract-apps/admin-app:latest + # container_name: admin_app_test + # ports: + # - "5172:80" + # networks: + # - fineract-network + + # cashier-app: + # image: ghcr.io/adorsys-gis/fineract-apps/cashier-app:latest + # container_name: cashier_app_test + # ports: + # - "5173:80" + # networks: + # - fineract-network + + # branchmanager-app: + # image: ghcr.io/adorsys-gis/fineract-apps/branchmanager-app:latest + # container_name: branchmanager_app_test + # ports: + # - "5174:80" + # networks: + # - fineract-network + + # accountmanager-app: + # image: ghcr.io/adorsys-gis/fineract-apps/account-manager-app:latest + # container_name: accountmanager_app_test + # ports: + # - "5175:80" + # networks: + # - fineract-network + + # mifos-app: + # image: ghcr.io/adorsys-gis/fineract-apps/mifos-app:latest + # container_name: mifos_app_test + # ports: + # - "5176:80" + # networks: + # - fineract-network + + community-app: + image: openmf/web-app:master + container_name: mifos-web-app + restart: always + ports: + - 4200:80 + environment: + - FINERACT_API_URL=https://localhost + networks: + - fineract-network + +networks: + fineract-network: + driver: bridge + name: fineract-dev-network diff --git a/docker-compose-web-app.yml b/docker-compose-web-app.yml index f7d7031d6f7..6f08a998059 100644 --- a/docker-compose-web-app.yml +++ b/docker-compose-web-app.yml @@ -25,3 +25,5 @@ services: restart: always ports: - 4200:80 + environment: + - FINERACT_API_URL=https://localhost diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md new file mode 100644 index 00000000000..a88c363df51 --- /dev/null +++ b/docs/CONFIGURATION_GUIDE.md @@ -0,0 +1,104 @@ +# Fineract Configuration Management Guide + +This guide explains how to add new configurations to Fineract using the two available methods: the `fineract-config-cli` (with `demo-config-fixed.yml`) and the Python data-loading scripts (with an Excel file). + +## The Relationship Between Excel and YAML + +The Excel file generated by `generate_excel_template.py` serves as the primary source of truth for all configurations. The `demo-config-fixed.yml` file used by the `fineract-config-cli` is a derivative of the Excel file, generated by the `excel_to_yaml.py` conversion script. + +This means that all configuration changes should be made in the Excel file first, and then converted to YAML to ensure both loading methods remain synchronized. + +```mermaid +graph TD + A[generate_excel_template.py] --> B(fineract_demo_data.xlsx); + + subgraph "Excel-Based Workflow" + B --> C[Python Loaders]; + C --> D{Fineract API}; + end + + subgraph "CLI-Based Workflow" + B --> E[excel_to_yaml.py]; + E --> F[demo-config-fixed.yml]; + F --> G[fineract-config-cli]; + G --> D; + end + + style A fill:#f9f,stroke:#333,stroke-width:2px + style B fill:#9f9,stroke:#333,stroke-width:2px + style F fill:#ccf,stroke:#333,stroke-width:2px +``` + +### Step 1: Add the New Configuration to the Excel Template + +1. **Locate the Relevant Sheet-Creation Function:** Open `generate_excel_template.py` and find the function that creates the sheet for your desired configuration. For example: + * **Maker-Checker:** `create_maker_checker_config_sheet` + * **Roles and Permissions:** `create_roles_permissions_sheet` + * **Users:** `create_users_sheet` + +2. **Add the New Configuration:** Add a new dictionary to the `data` list within the relevant function. + + **Example: Enabling Maker-Checker for a New Operation** + + In the `create_maker_checker_config_sheet` function, add a new entry to the `data` list: + + ```python + data = [ + # ... + { + 'task_name': 'Create Savings Account', + 'entity': 'SAVINGSACCOUNT', + 'action': 'CREATE', + 'enabled': True, + 'maker_role': 'Loan Officer', + 'checker_role': 'Branch Manager', + 'description': 'Creating new savings accounts requires manager approval' + }, + # ... + ] + ``` + +### Step 2: Generate the Excel and YAML Files + +1. **Generate the Excel File:** Run `generate_excel_template.py` to create a new Excel file containing your changes. +2. **Convert to YAML:** Use the `excel_to_yaml.py` script to convert the new Excel file into `demo-config-fixed.yml`. + +### Step 3: Run the Loaders + +You can now use either of the following methods to apply your configurations: + +* **Python Loaders:** Run the main Python data-loading script with the newly generated Excel file. +* **fineract-config-cli:** Run the `fineract-config-cli` tool, which will use the updated `demo-config-fixed.yml`. + +## How to Convert the Excel File to YAML + +The `excel_to_yaml.py` script is a command-line tool that reads the generated Excel file and outputs a YAML file that is compatible with the `fineract-config-cli`. + +### Prerequisites + +* Python 3 +* `pandas` and `pyyaml` libraries installed. You can install them with pip: + ```bash + pip install -r requirements.txt + ``` + +### Usage + +1. **Navigate to the scripts directory:** + ```bash + cd fineract-adorsys-data-collection/fineract-demo-data/scripts + ``` + +2. **Run the conversion script:** + ```bash + python3 excel_to_yaml.py -i ../output/.xlsx -o ../../fineract-config-cli/config/demo-config-fixed.yml + ``` + + * Replace `.xlsx` with the name of the Excel file you generated. + * The output path is set to overwrite the existing `demo-config-fixed.yml` file, ensuring the CLI uses the latest configurations. + +### How It Works + +The script reads each sheet from the Excel file and transforms the data into the specific YAML structure expected by the `fineract-config-cli`. It handles data type conversions, such as dates and booleans, and maps the Excel column names to the corresponding YAML keys. + +By following this process, you can maintain a single source of truth in your Excel file while still being able to use both the Python loaders and the `fineract-config-cli` to provision your Fineract instance. diff --git a/docs/Mifos_Repoerting_Plugin_Guide.md b/docs/Mifos_Repoerting_Plugin_Guide.md new file mode 100644 index 00000000000..7e15097067a --- /dev/null +++ b/docs/Mifos_Repoerting_Plugin_Guide.md @@ -0,0 +1,228 @@ +# Mifos Reporting Plugin: A Comprehensive Guide. + +This document provides a complete guide to the Mifos Reporting Plugin for Apache Fineract, including installation, testing, usage, and frontend integration. + +## 1. Understanding the Reporting Plugin + +### 1.1. What is the purpose of this plugin and how does it work? + +The Mifos Reporting Plugin integrates the **Pentaho Business Intelligence (BI) suite** with Apache Fineract. Pentaho is a powerful open-source platform for data integration, reporting, and analytics. + +**High-Level Workflow:** + +1. **Report Templates:** The plugin uses pre-designed report templates created with the Pentaho Report Designer. These templates have the `.prpt` file extension and are stored in the `pentahoReports` directory. Each template defines the layout, data queries, and parameters for a specific report (e.g., "Active Loan Summary"). +2. **Fineract API:** When you request a report through the Fineract API (or the Mifos UI), you specify the report name and any required parameters (like dates, office IDs, etc.). +3. **Pentaho Engine:** Fineract passes this request to the Pentaho reporting engine, which is loaded by the plugin. +4. **Data Fetching:** The Pentaho engine connects to your Fineract database (MariaDB in your case), executes the SQL queries defined in the `.prpt` template, and fetches the required data. +5. **Report Generation:** It then populates the template with the fetched data and generates the final report in the format you requested (e.g., PDF, Excel, CSV). +6. **API Response:** Fineract sends the generated report back to you as the API response. + +In short, the plugin acts as a bridge, allowing you to leverage Pentaho's advanced reporting capabilities to generate professional, data-rich reports from your Fineract data. + +### 1.2. How do I use this in the day-to-day business of my microfinance? + +The reports provided by this plugin are essential for monitoring the health and performance of your microfinance institution. Here are some examples of how different roles can use them: + +* **Loan Officers:** + * `Active Loans - Details`: To get a detailed list of all active loans they manage. + * `Expected Payments By Date`: To plan their collection activities for the upcoming days or weeks. + * `Portfolio at Risk`: To identify clients who are falling behind on their payments and require follow-up. +* **Branch Managers:** + * `Active Loan Summary per Branch`: To get a high-level overview of the branch's loan portfolio. + * `Disbursal Report`: To track the number and amount of loans disbursed by the branch over a specific period. + * `Collection Report`: To monitor the daily or weekly collections and compare them against targets. +* **Accountants & Finance Managers:** + * `Balance Sheet`: To get a snapshot of the institution's financial position. + * `Income Statement`: To track revenues and expenses and assess profitability. + * `Trial Balance`: To ensure the books are balanced and for auditing purposes. +* **Management & Executives:** + * `Aging Summary`: To understand the overall quality of the loan portfolio and identify trends in arrears. + * `Client Listing`: To get demographic and other information about the institution's clients. + +By regularly generating and analyzing these reports, your team can make more informed decisions, improve operational efficiency, and better manage risk. + +## 2. Installation and Setup + +### 2.1. Prerequisites + +- Docker and Docker Compose are installed and running. +- An Apache Fineract instance is running via Docker Compose. + +### 2.2. Step 1: Prepare Plugin Files + +1. **Create a directory for the plugin:** + + ```bash + mkdir fineract-pentaho + ``` + +2. **Download the Mifos Reporting Plugin:** + + Download the appropriate version of the plugin based on your Fineract version. In this guide, we used version 1.12.1 for Fineract 1.12.0. + + ```bash + curl -L -o fineract-pentaho/MifosSecurityPlugin-1.12.1.zip "https://sourceforge.net/projects/mifos/files/mifos-plugins/MifosReportingPlugin/MifosSecurityPlugin-1.12.1.zip/download" + ``` + +3. **Extract the plugin files:** + + ```bash + unzip fineract-pentaho/MifosSecurityPlugin-1.12.1.zip -d fineract-pentaho/ + ``` + +### 2.3. Step 2: Integrate with Docker + +1. **Modify `docker-compose.yml`:** + + Open your `docker-compose.yml` file and add the following volumes and environment variables to the `fineract` service: + + ```yaml + services: + fineract: + # ... existing configuration ... + volumes: + - /path/to/mifos-reporting-plugin/fineract-pentaho/MifosSecurityPlugin-1.12.1:/app/plugins + - /path/to/mifos-reporting-plugin/pentahoReports:/pentahoReports + environment: + - FINERACT_PENTAHO_REPORTS_PATH=/pentahoReports/MariaDB/ + - JAVA_TOOL_OPTIONS=-Dloader.path=/app/plugins/ + ``` + + **Note:** Replace `/path/to/mifos-reporting-plugin` with the path to your `mifos-reporting-plugin` directory. + +2. **Restart the Docker environment:** + + ```bash + docker compose up -d + ``` + +## 3. Testing and Verification + +### 3.1. How can I play around with reports on the Mifos UI for testing purposes? + +The Mifos web UI provides a user-friendly interface for running and viewing reports. Here's how you can typically access them: + +1. **Log in to the Mifos UI:** Open your web browser and navigate to the Mifos UI (likely running on `http://localhost:4200` based on your `docker ps` output). +2. **Navigate to the Reports Section:** Look for a "Reports" or "Reporting" menu item in the main navigation bar, usually on the left side of the screen. +3. **Select a Report:** The reports section will list all the available Pentaho reports. You can browse or search for the report you want to run. +4. **Enter Parameters:** When you select a report, a form will appear asking for the required parameters (e.g., start date, end date, office, loan officer). +5. **Generate the Report:** Fill in the parameters and click "Run Report" or a similar button. The UI will then make an API call to Fineract to generate the report. +6. **View or Download:** Once the report is generated, you can typically view it directly in your browser or download it as a PDF, Excel, or CSV file. + +This interface is a great way to explore the available reports and understand the data they provide without having to use the API directly. + +### 3.2. Testing with the API + +1. **Verify Fineract is running:** + + Check that the `fineract` container is healthy: + + ```bash + docker compose ps + ``` + +2. **Run a test report:** + + Execute the following `curl` command to generate a test report. The output will be saved to `report.pdf`. + + ```bash + curl --location --request GET 'https://localhost:443/fineract-provider/api/v1/runreports/Expected%20Payments%20By%20Date%20-%20Formatted?tenantIdentifier=default&locale=en&dateFormat=dd%20MMMM%20yyyy&R_startDate=01%20January%202022&R_endDate=02%20January%202023&R_officeId=1&output-type=PDF&R_loanOfficerId=-1' \ + --header 'Fineract-Platform-TenantId: default' \ + --header 'Authorization: Basic bWlmb3M6cGFzc3dvcmQ=' -k -o report.pdf + ``` + + If the command is successful, a `report.pdf` file will be created in your current directory. + +## 4. Frontend Integration Tips + +When building a custom frontend for your microfinance institution, here are some tips for integrating the reporting features effectively: + +* **Create a Dedicated Reporting Module:** Design a dedicated section in your application for reports. This will make it easy for users to find and access them. +* **User-Friendly Parameter Selection:** + * Use date pickers for selecting date ranges. + * Provide dropdowns with lists of offices, loan officers, and other filterable options. You can fetch this data from the relevant Fineract APIs. + * Set sensible default values for parameters (e.g., the current date). +* **Asynchronous Report Generation:** Some reports can take a long time to generate. To avoid making the user wait, consider implementing an asynchronous workflow: + 1. The user submits the report request. + 2. Your frontend makes the API call to Fineract and shows a "loading" or "processing" indicator. + 3. Once the report is ready, you can either automatically download it or provide a link for the user to click. +* **Handle Different Output Formats:** Allow users to choose their preferred output format (PDF for printing, Excel/CSV for data analysis). You can do this by changing the `output-type` parameter in the API call. +* **Role-Based Access Control (RBAC):** Not all users should have access to all reports. Implement RBAC to control which reports are visible and accessible to different user roles (e.g., a loan officer should only see reports related to their clients). +* **Displaying Reports:** + * For PDFs, you can embed them in an `