Compare commits
503
Commits
1005b56d0f
...
ea2dfd789d
@@ -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 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.
|
||||
|
||||
# See comment below license in .atr-rat-excludes-src.txt for an explanation of
|
||||
# what this is.
|
||||
|
||||
# if you make changes here, sort this list before committing asciibetically
|
||||
# with en_US.UTF-8 lang/locale
|
||||
.rat-excludes
|
||||
README.md
|
||||
fineract-documentation.pdf
|
||||
|
||||
# vim:ft=conf
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
|
||||
# Path patterns to exclude from RAT checks run by ATR.
|
||||
#
|
||||
# RAT = Release Audit Tool (https://creadur.apache.org/rat/)
|
||||
# ATR = ASF Trusted Releases (https://release-test.apache.org , https://github.com/apache/tooling-trusted-releases/)
|
||||
#
|
||||
# This duplicates exclusions in build.gradle.
|
||||
# This is probably only necessary until both RAT and org.nosphere.apache.rat plugin are using 0.17.
|
||||
# See https://github.com/eskatos/creadur-rat-gradle/issues/28 .
|
||||
# Once both are on 0.17, we should be able to move the RAT excludes used during
|
||||
# the build from build.gradle into a .rat-excludes file shared by both our
|
||||
# build and ATR.
|
||||
# Right now ATR does something custom, see
|
||||
# https://github.com/apache/tooling-trusted-releases/issues/312 .
|
||||
# If we get tired of waiting for the RAT gradle plugin or the ATR to upgrade to
|
||||
# 0.17, we could write our own exclusion lists generator.
|
||||
|
||||
# if you make changes here, sort this list before committing asciibetically
|
||||
# with en_US.UTF-8 lang/locale
|
||||
**/META-INF/fineract-test.config
|
||||
**/legacy-docs/
|
||||
*.adoc
|
||||
*.avsc
|
||||
*.feature
|
||||
*.html
|
||||
*.md
|
||||
*.mustache
|
||||
*.puml
|
||||
*.txt
|
||||
.openapi-generator-ignore
|
||||
/.asf.yaml
|
||||
/.rat-excludes
|
||||
/config/fineractdev-eclipse-preferences.epf
|
||||
/fineract-provider/src/test/resources/results/xbrl.xml
|
||||
/gradle/wrapper/gradle-wrapper.properties
|
||||
org.springframework.boot.autoconfigure.AutoConfiguration.imports
|
||||
|
||||
# vim:ft=conf
|
||||
@@ -0,0 +1,93 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from http.client import HTTPConnection, HTTPException
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
from contextlib import closing
|
||||
from typing import Optional
|
||||
|
||||
WEBSERVER_HOST = "localhost"
|
||||
WEBSERVER_ENDPOINT = "/api/provenance/call"
|
||||
PORT_FILE_SUFFIX = "-provenance-port.txt"
|
||||
|
||||
class ProvenanceHookError(RuntimeError):
|
||||
pass
|
||||
|
||||
def http_request(method, host, port, location, *, body: Optional[bytes] = None, headers={}, timeout=None) -> bytes:
|
||||
with closing(HTTPConnection(host, port, timeout=timeout)) as connection:
|
||||
connection.request(method, location, body=body, headers=headers)
|
||||
|
||||
def get_server_port():
|
||||
claude_root = os.getenv("CLAUDE_PROJECT_DIR")
|
||||
path_hash = hashlib.md5(claude_root.encode('utf-8')).hexdigest()
|
||||
port_file = Path(tempfile.gettempdir()) / (path_hash + PORT_FILE_SUFFIX)
|
||||
|
||||
return int(port_file.read_text("utf-8").strip())
|
||||
|
||||
|
||||
def send_diff_to_webserver(file_path, timestamp_ms):
|
||||
try:
|
||||
port = get_server_port()
|
||||
except FileNotFoundError as e:
|
||||
raise ProvenanceHookError(
|
||||
f"Could not determine API port: {e.filename} does not exist") from e
|
||||
except Exception as e:
|
||||
raise ProvenanceHookError("Could not determine API port") from e
|
||||
|
||||
url = f"http://{WEBSERVER_HOST}:{port}{WEBSERVER_ENDPOINT}"
|
||||
|
||||
try:
|
||||
payload = {"file_path": file_path, "timestamp": timestamp_ms}
|
||||
return http_request(
|
||||
"POST",
|
||||
WEBSERVER_HOST,
|
||||
port=port,
|
||||
location=WEBSERVER_ENDPOINT,
|
||||
body=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=0.5
|
||||
)
|
||||
|
||||
except (HTTPException, OSError, ConnectionError) as e:
|
||||
raise ProvenanceHookError(
|
||||
f"Network error while sending diff to {url}") from e
|
||||
except Exception as e:
|
||||
raise ProvenanceHookError(
|
||||
f"Unknown error while sending diff to {url}") from e
|
||||
|
||||
|
||||
def extract_file_path(tool_name, tool_input):
|
||||
if tool_name in ["Write", "Edit", "MultiEdit"]:
|
||||
return tool_input.get('file_path', 'unknown')
|
||||
if tool_name == "NotebookEdit":
|
||||
return tool_input.get('notebook_path', 'unknown')
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def excepthook(type, value, traceback_):
|
||||
traceback.print_exception(type, value, traceback_, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
data = json.load(sys.stdin)
|
||||
tool_name = data.get('tool_name', 'unknown')
|
||||
|
||||
modification_tools = [
|
||||
"Write", "Edit", "MultiEdit", "NotebookEdit"
|
||||
]
|
||||
|
||||
if tool_name in modification_tools:
|
||||
tool_input = data.get('tool_input', {})
|
||||
file_path = extract_file_path(tool_name, tool_input)
|
||||
if file_path:
|
||||
timestamp_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
send_diff_to_webserver(file_path, timestamp_ms)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.excepthook = excepthook
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1 @@
|
||||
{"sessionId":"0fda6e48-a79d-4980-a9ee-31a6d5f735e1","pid":15548,"acquiredAt":1776168814856}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git remote add self-hosted https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN.git)",
|
||||
"Bash(git push self-hosted develop:main)",
|
||||
"WebFetch(domain:rbi.org.in)",
|
||||
"Bash(xargs grep -l \"@Scheduled\")",
|
||||
"Bash(./gradlew compileJava -x spotlessCheck)",
|
||||
"Bash(./gradlew bootJar -x test -x spotlessCheck)",
|
||||
"Bash(./gradlew :fineract-provider:compileJava -x spotlessCheck)",
|
||||
"Bash(./gradlew :fineract-core:compileJava :fineract-provider:compileJava)",
|
||||
"Bash(curl -sk -o /dev/null -w \"%{http_code}\" https://localhost:8443/fineract-provider/api/v1/rbi-feeds)",
|
||||
"Bash(./gradlew bootRun -x test)",
|
||||
"Bash(ls *.gradle)",
|
||||
"Bash(curl -sk -o /dev/null -w \"%{http_code}\" https://localhost:8443/fineract-provider/api)",
|
||||
"Bash(ls c:/Users/adity/Music/CBS/fineract/fineract-provider/src/main/resources/db/changelog/tenant/parts/0232*)",
|
||||
"Bash(taskkill //F //PID 36040)",
|
||||
"Bash(findstr :8443)",
|
||||
"Bash(findstr LISTENING)",
|
||||
"Bash(taskkill //F //PID 14668)",
|
||||
"Bash(./gradlew --stop)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\rbi-circulars",
|
||||
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\clients\\create-client",
|
||||
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\clients\\client-stepper\\client-general-step",
|
||||
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(taskkill //PID 24168 //F)",
|
||||
"Bash(PGPASSWORD=postgres psql -h localhost -U postgres -d fineract_default -c \"SELECT id, display_name, date_of_birth FROM m_client ORDER BY id DESC LIMIT 5;\")",
|
||||
"Bash(taskkill //PID 10112 //F)",
|
||||
"Bash(taskkill //PID 39420 //F)",
|
||||
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds\")",
|
||||
"Bash(sort -t: -k2 -n)",
|
||||
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds/144\")",
|
||||
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/openproject/work-packages?pageSize=10&offset=1\")",
|
||||
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" -H \"Content-Type: application/json\" -X POST \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds/sync-openproject\" -d '{}')",
|
||||
"Bash(taskkill //PID 36832 //F)",
|
||||
"Bash(mvn clean:*)",
|
||||
"Bash(where mvn.cmd)",
|
||||
"Bash(where mvn:*)",
|
||||
"Read(//c/Program Files/**)",
|
||||
"Read(//c/Users/adity/**)",
|
||||
"Bash(echo \"$MAVEN_HOME\")",
|
||||
"Bash(xargs basename:*)",
|
||||
"Bash(./gradlew :fineract-tax:compileJava)",
|
||||
"Bash(grep -n \"INSERT INTO \\\\`m_role\\\\`\\\\|INSERT INTO m_role\\\\|INSERT INTO \\\\`m_permission\\\\`\\\\|INSERT INTO m_permission\" /c/Users/adity/Music/CBS/fineract/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql)",
|
||||
"Bash(npm start:*)",
|
||||
"Bash(grep -oP \"^\\\\\\\\\\(''''\\([^'''']+\\)''''\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-db\\\\old-schema-files\\\\0003-mifosx-permissions-and-authorisation-utf8.sql\")",
|
||||
"Bash(tasklist)",
|
||||
"Bash(wmic process:*)",
|
||||
"Bash(powershell -Command \"Get-CimInstance Win32_Process -Filter \"\"Name=''java.exe''\"\" | Select-Object ProcessId, @{N=''Cmd'';E={$_.CommandLine.Substring\\(0, [Math]::Min\\(200, $_.CommandLine.Length\\)\\)}} | Format-Table -AutoSize\")",
|
||||
"Bash(psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
|
||||
"Bash(PGPASSWORD=root psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
|
||||
"Bash(PGPASSWORD=root psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''heading_menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
|
||||
"Bash(grep -l \"pan_full_name\\\\|pan_category\\\\|pan_status\\\\|pan_aadhaar\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-provider\\\\src\\\\main\\\\resources\\\\db\\\\changelog\\\\tenant\\\\parts\"/*.xml)",
|
||||
"Bash(grep -l \"m_client_kyc\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-provider\\\\src\\\\main\\\\resources\\\\db\\\\changelog\\\\tenant\\\\parts\"/*.xml)",
|
||||
"Bash(./gradlew devRun -x test -x spotlessCheck)",
|
||||
"Bash(taskkill //F //FI \"IMAGENAME eq java.exe\")",
|
||||
"Bash(./gradlew bootRun)",
|
||||
"Bash(curl -sk -X POST \"https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password\" -H 'Fineract-Platform-TenantId: default' -H 'Content-Type: application/json')",
|
||||
"Bash(curl -sk \"https://localhost:8443/fineract-provider/api/v1/clients?limit=5\" -H \"Fineract-Platform-TenantId: default\" -u \"mifos:password\")",
|
||||
"Bash(curl -sk -X POST \"https://localhost:8443/fineract-provider/api/v1/authentication\" -H \"Fineract-Platform-TenantId: default\" -H \"Content-Type: application/json\" -d '{\"\"username\"\":\"\"mifos\"\",\"\"password\"\":\"\"password\"\"}')",
|
||||
"Bash(curl -sk \"https://localhost:8443/fineract-provider/api/v1/clients?limit=5\" -H \"Fineract-Platform-TenantId: default\" -H \"Authorization: Basic bWlmb3M6cGFzc3dvcmQ=\")",
|
||||
"Bash(./gradlew :fineract-provider:devRun --console=plain)",
|
||||
"Bash(curl -k -o /tmp/test-helper.jar -w \"HTTP %{http_code} size=%{size_download} bytes\\\\n\" https://localhost:8443/fineract-provider/api/v1/dsc/helper.jar)",
|
||||
"Bash(powershell -Command \"Get-Process java -ErrorAction SilentlyContinue | Select-Object Id, StartTime | Format-List\")",
|
||||
"Bash(./gradlew :fineract-provider:compileJava --console=plain)",
|
||||
"Bash(./gradlew :fineract-provider:compileJava :fineract-security:compileJava -q)",
|
||||
"Bash(./gradlew :fineract-provider:compileJava -q)",
|
||||
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalPort, OwningProcess, @{Name='ProcessName';Expression={\\(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue\\).ProcessName}}, @{Name='StartTime';Expression={\\(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue\\).StartTime}})",
|
||||
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -ErrorAction SilentlyContinue | Format-Table LocalPort, State, OwningProcess, @{Name='ProcessName';Expression={\\(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue\\).ProcessName}} -AutoSize; netstat -ano | Select-String \":8443\")",
|
||||
"PowerShell(Get-Process -Id 9340 -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, StartTime, @{Name='MemMB';Expression={[math]::Round\\($_.WorkingSet64/1MB,0\\)}}, Path | Format-List)",
|
||||
"Bash(git -c core.editor=true rebase --continue)",
|
||||
"Bash(git rebase *)",
|
||||
"Bash(rm -rf .git/rebase-merge; git status -sb | head -3)",
|
||||
"Bash(git -c commit.gpgsign=false commit -m ' *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)",
|
||||
"Bash(git ls-remote *)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api",
|
||||
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\assets\\translations",
|
||||
"C:\\Users\\adity\\Music\\CBS\\fineract\\.git\\rebase-merge"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-call.py\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
## Description
|
||||
|
||||
Describe the changes made and why they were made.
|
||||
|
||||
Ignore if these details are present on the associated [Apache Fineract JIRA ticket](https://github.com/apache/fineract/pull/1284).
|
||||
Describe the changes made and why they were made. (Ignore if these details are present on the associated Apache Fineract JIRA ticket.)
|
||||
|
||||
## Checklist
|
||||
|
||||
Please make sure these boxes are checked before submitting your pull request - thanks!
|
||||
|
||||
- [ ] Write the commit message as per https://github.com/apache/fineract/#pull-requests
|
||||
- [ ] Write the commit message as per [our guidelines](https://github.com/apache/fineract/blob/develop/CONTRIBUTING.md#pull-requests)
|
||||
- [ ] Acknowledge that we will not review PRs that are not passing the build _("green")_ - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
|
||||
- [ ] Create/update unit or integration tests for verifying the changes made.
|
||||
- [ ] Follow coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions.
|
||||
- [ ] Create/update [unit or integration tests](https://fineract.apache.org/docs/current/#_testing) for verifying the changes made.
|
||||
- [ ] Follow our [coding conventions](https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions).
|
||||
- [ ] Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes
|
||||
- [ ] Submission is not a "code dump". (Large changes can be made "in repository" via a branch. Ask on the developer mailing list for guidance, if required.)
|
||||
- [ ] [This PR must not be a "code dump"](https://cwiki.apache.org/confluence/display/FINERACT/Pull+Request+Size+Limit). Large changes can be made in a branch, with assistance. Ask for help on the [developer mailing list](https://fineract.apache.org/#contribute).
|
||||
|
||||
FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.
|
||||
Your assigned reviewer(s) will follow our [guidelines for code reviews](https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide).
|
||||
|
||||
@@ -27,19 +27,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: test-results-${{ matrix.task }}
|
||||
path: |
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
|
||||
- name: Archive Progressive Loan JAR
|
||||
if: matrix.job_type == 'progressive-loan' && always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: progressive-loan-jar
|
||||
path: ${{ env.EMBEDDABLE_JAR_FILE }}
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Archive server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: server-logs-${{ matrix.task }}
|
||||
path: '**/build/cargo/'
|
||||
|
||||
@@ -26,18 +26,18 @@ jobs:
|
||||
IMAGE_NAME: fineract
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
|
||||
- name: Build the image
|
||||
run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber
|
||||
|
||||
@@ -10,23 +10,23 @@ jobs:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Congfigure vega-cli
|
||||
run: npm i -g vega-cli --unsafe
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
uses: gradle/actions/wrapper-validation@v5.0.1
|
||||
- name: Install additional software
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -33,18 +33,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
|
||||
- name: Make scripts executable
|
||||
run: chmod +x scripts/split-features.sh
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: allure-results-shard-${{ matrix.shard_index }}
|
||||
path: |
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Upload Allure Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: allure-report-shard-${{ matrix.shard_index }}
|
||||
path: allure-report-shard-${{ matrix.shard_index }}
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: logs-shard-${{ matrix.shard_index }}
|
||||
path: |
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
options: --health-cmd="healthcheck.sh --su-mysql --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
mock-oauth2-server:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
||||
image: ghcr.io/navikt/mock-oauth2-server:3.0.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
env:
|
||||
@@ -44,19 +44,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: test-results-${{ matrix.task }}
|
||||
path: '**/build/reports/'
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
- name: Archive server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: server-logs-${{ matrix.task }}
|
||||
path: '**/build/cargo/'
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
mock-oauth2-server:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
||||
image: ghcr.io/navikt/mock-oauth2-server:3.0.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
env:
|
||||
@@ -44,19 +44,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: test-results-${{ matrix.task }}
|
||||
path: '**/build/reports/'
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
- name: Archive server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: server-logs-${{ matrix.task }}
|
||||
path: '**/build/cargo/'
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:17.4
|
||||
image: postgres:18.2
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
options: --health-cmd="pg_isready -q -d postgres -U root" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
mock-oauth2-server:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
||||
image: ghcr.io/navikt/mock-oauth2-server:3.0.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
env:
|
||||
@@ -45,19 +45,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: test-results-${{ matrix.task }}
|
||||
path: '**/build/reports/'
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
- name: Archive server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: server-logs-${{ matrix.task }}
|
||||
path: '**/build/cargo/'
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Fineract Liquibase Only mode - PostgreSQL
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:18.2
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: --health-cmd="pg_isready -q -d postgres -U root" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
env:
|
||||
TZ: Asia/Kolkata
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
|
||||
- name: Verify PostgreSQL connection
|
||||
run: |
|
||||
while ! pg_isready -d postgres -U root -h 127.0.0.1 -p 5432 ; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Initialise databases
|
||||
run: |
|
||||
./gradlew --no-daemon -q createPGDB -PdbName=fineract_tenants
|
||||
./gradlew --no-daemon -q createPGDB -PdbName=fineract_default
|
||||
|
||||
- name: Run Fineract in Liquibase only mode
|
||||
env:
|
||||
FINERACT_DEFAULT_TENANTDB_CONN_PARAMS: ""
|
||||
FINERACT_DEFAULT_TENANTDB_DESCRIPTION: "Default Demo Tenant"
|
||||
FINERACT_DEFAULT_TENANTDB_HOSTNAME: "localhost"
|
||||
FINERACT_DEFAULT_TENANTDB_IDENTIFIER: "default"
|
||||
FINERACT_DEFAULT_TENANTDB_NAME: "fineract_default"
|
||||
FINERACT_DEFAULT_TENANTDB_PORT: "5432"
|
||||
FINERACT_DEFAULT_TENANTDB_PWD: "postgres"
|
||||
FINERACT_DEFAULT_TENANTDB_TIMEZONE: "Asia/Kolkata"
|
||||
FINERACT_DEFAULT_TENANTDB_UID: "root"
|
||||
FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME: "org.postgresql.Driver"
|
||||
FINERACT_HIKARI_JDBC_URL: "jdbc:postgresql://localhost:5432/fineract_tenants"
|
||||
FINERACT_HIKARI_PASSWORD: "postgres"
|
||||
FINERACT_HIKARI_USERNAME: "root"
|
||||
SPRING_PROFILES_ACTIVE: "liquibase-only"
|
||||
run:
|
||||
./gradlew fineract-provider:bootRun
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish Fineract client to Mifos Artifactory
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop-mifos
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate build number
|
||||
uses: onyxmueller/build-tag-number@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
- name: Build the image
|
||||
run: ./gradlew publish -Pfineract.config.username=$ARTIFACTORY_USERNAME -Pfineract.config.password=$ARTIFACTORY_PASSWORD -Pfineract.release.version=${BUILD_NUMBER}
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Fineract PR Compliance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
verify-title:
|
||||
name: Validate Jira Ticket ID
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Verify Title Format
|
||||
run: |
|
||||
title="${{ github.event.pull_request.title }}"
|
||||
regex="^FINERACT-[0-9]+: "
|
||||
|
||||
if [[ ! "$title" =~ $regex ]]; then
|
||||
echo "::error::PR title '$title' is invalid."
|
||||
echo "::error::It must start with a Jira Ticket ID (e.g., 'FINERACT-123: Description...')."
|
||||
exit 1
|
||||
fi
|
||||
echo "Success: PR title matches the required format."
|
||||
@@ -15,18 +15,18 @@ jobs:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
|
||||
- name: Get Git Hashes
|
||||
run: |
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:17.4
|
||||
image: postgres:18.2
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: --health-cmd="pg_isready -q -d postgres -U root" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
mock-oauth2-server:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
||||
image: ghcr.io/navikt/mock-oauth2-server:3.0.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
env:
|
||||
@@ -33,18 +33,18 @@ jobs:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
- name: Verify PostgreSQL connection
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
./gradlew --no-daemon --console=plain :oauth2-tests:test -PdbType=postgresql
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: test-results
|
||||
retention-days: 5
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
oauth2-tests/build/reports/
|
||||
- name: Archive server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: server-logs
|
||||
retention-days: 5
|
||||
|
||||
@@ -24,18 +24,18 @@ jobs:
|
||||
IMAGE_NAME: fineract
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
|
||||
- name: Build the image
|
||||
run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber
|
||||
|
||||
@@ -20,16 +20,16 @@ jobs:
|
||||
JAVA_BINARIES: .
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v4
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'zulu'
|
||||
- name: Setup Gradle and Validate Wrapper
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
validate-wrappers: true
|
||||
- name: Build
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v9
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# stale-issue-message: 'Stale issue message'
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
name: Verify API Backward Compatibility
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
api-compatibility-check:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
|
||||
env:
|
||||
TZ: Asia/Kolkata
|
||||
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
fetch-depth: 0
|
||||
path: baseline
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
|
||||
- name: Generate baseline spec
|
||||
working-directory: baseline
|
||||
run: ./gradlew :fineract-provider:resolve --no-daemon
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
path: current
|
||||
|
||||
- name: Generate PR spec
|
||||
working-directory: current
|
||||
run: ./gradlew :fineract-provider:resolve --no-daemon
|
||||
|
||||
- name: Sanitize specs
|
||||
run: |
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
def sanitize(path):
|
||||
with open(path) as f:
|
||||
spec = json.load(f)
|
||||
fixed = 0
|
||||
for path_item in spec.get('paths', {}).values():
|
||||
for op in path_item.values():
|
||||
if not isinstance(op, dict) or 'requestBody' not in op:
|
||||
continue
|
||||
for media in op['requestBody'].get('content', {}).values():
|
||||
if 'schema' not in media:
|
||||
media['schema'] = {'type': 'object'}
|
||||
fixed += 1
|
||||
if fixed:
|
||||
with open(path, 'w') as f:
|
||||
json.dump(spec, f)
|
||||
print(f'{path}: fixed {fixed} entries')
|
||||
|
||||
sanitize('${GITHUB_WORKSPACE}/baseline/fineract-provider/build/resources/main/static/fineract.json')
|
||||
sanitize('${GITHUB_WORKSPACE}/current/fineract-provider/build/resources/main/static/fineract.json')
|
||||
"
|
||||
|
||||
- name: Check breaking changes
|
||||
id: breaking-check
|
||||
continue-on-error: true
|
||||
working-directory: current
|
||||
run: |
|
||||
set -o pipefail
|
||||
./gradlew :fineract-provider:checkBreakingChanges \
|
||||
-PapiBaseline="${GITHUB_WORKSPACE}/baseline/fineract-provider/build/resources/main/static/fineract.json" \
|
||||
--no-daemon --quiet 2>&1 | tail -50
|
||||
|
||||
- name: Build report
|
||||
if: steps.breaking-check.outcome == 'failure'
|
||||
id: report
|
||||
run: |
|
||||
REPORT_DIR="current/fineract-provider/build/swagger-brake"
|
||||
|
||||
python3 -c "
|
||||
import json, glob, os
|
||||
from collections import defaultdict
|
||||
|
||||
RULE_DESC = {
|
||||
'R001': 'Standard API changed to beta',
|
||||
'R002': 'Path deleted',
|
||||
'R003': 'Request media type deleted',
|
||||
'R004': 'Request parameter deleted',
|
||||
'R005': 'Request parameter enum value deleted',
|
||||
'R006': 'Request parameter location changed',
|
||||
'R007': 'Request parameter made required',
|
||||
'R008': 'Request parameter type changed',
|
||||
'R009': 'Request attribute removed',
|
||||
'R010': 'Request type changed',
|
||||
'R011': 'Request enum value deleted',
|
||||
'R012': 'Response code deleted',
|
||||
'R013': 'Response media type deleted',
|
||||
'R014': 'Response attribute removed',
|
||||
'R015': 'Response type changed',
|
||||
'R016': 'Response enum value deleted',
|
||||
'R017': 'Request parameter constraint changed',
|
||||
}
|
||||
|
||||
report_dir = '${REPORT_DIR}'
|
||||
files = sorted(glob.glob(os.path.join(report_dir, '*.json')))
|
||||
if not files:
|
||||
body = 'Breaking change detected but no report file found.'
|
||||
else:
|
||||
with open(files[0]) as f:
|
||||
data = json.load(f)
|
||||
|
||||
all_changes = []
|
||||
for items in data.get('breakingChanges', {}).values():
|
||||
all_changes.extend(items)
|
||||
|
||||
if not all_changes:
|
||||
body = 'Breaking change detected but no details available in report.'
|
||||
else:
|
||||
def detail(c):
|
||||
for key in ('attributeName', 'attribute', 'name', 'mediaType', 'enumValue', 'code'):
|
||||
v = c.get(key)
|
||||
if v:
|
||||
val = v.rsplit('.', 1)[-1]
|
||||
if key in ('attributeName', 'attribute', 'name'):
|
||||
return val
|
||||
return f'{key}={val}'
|
||||
return '-'
|
||||
|
||||
groups = defaultdict(list)
|
||||
for c in all_changes:
|
||||
groups[(c.get('ruleCode', '?'), detail(c))].append(c)
|
||||
|
||||
lines = []
|
||||
lines.append('| Rule | Description | Detail | Affected endpoints | Count |')
|
||||
lines.append('|------|-------------|--------|--------------------|-------|')
|
||||
for (rule, det), items in sorted(groups.items()):
|
||||
desc = RULE_DESC.get(rule, '')
|
||||
eps = sorted(set(
|
||||
f'{c.get(\"method\",\"\")} {c.get(\"path\",\"\")}'
|
||||
for c in items if c.get('path')
|
||||
))
|
||||
ep_str = ', '.join(f'\`{e}\`' for e in eps[:5])
|
||||
if len(eps) > 5:
|
||||
ep_str += f' +{len(eps)-5} more'
|
||||
lines.append(f'| {rule} | {desc} | \`{det}\` | {ep_str} | {len(items)} |')
|
||||
|
||||
lines.append('')
|
||||
lines.append(f'**Total: {len(all_changes)} violations across {len(groups)} unique changes**')
|
||||
body = '\n'.join(lines)
|
||||
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write('has_report=true\n')
|
||||
|
||||
report_file = '${GITHUB_WORKSPACE}/breaking-changes-report.md'
|
||||
with open(report_file, 'w') as f:
|
||||
f.write('## Breaking API Changes Detected\n\n')
|
||||
f.write(body)
|
||||
f.write('\n\n> **Note:** This check is informational only and does not block the PR.\n')
|
||||
|
||||
# Also write to step summary
|
||||
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
|
||||
f.write('## Breaking API Changes Detected\n\n')
|
||||
f.write(body)
|
||||
f.write('\n\n> **Note:** This check is informational only and does not block the PR.\n')
|
||||
"
|
||||
|
||||
- name: Comment on PR
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
MARKER="<!-- swagger-brake-report -->"
|
||||
|
||||
# Find existing comment by marker
|
||||
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1)
|
||||
|
||||
if [ "${{ steps.breaking-check.outcome }}" == "failure" ] && [ -f "${GITHUB_WORKSPACE}/breaking-changes-report.md" ]; then
|
||||
# Prepend marker to the report
|
||||
BODY="${MARKER}
|
||||
$(cat ${GITHUB_WORKSPACE}/breaking-changes-report.md)"
|
||||
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
|
||||
-X PATCH -f body="${BODY}"
|
||||
else
|
||||
gh pr comment "${PR_NUMBER}" --repo ${{ github.repository }} --body "${BODY}"
|
||||
fi
|
||||
elif [ -n "$COMMENT_ID" ]; then
|
||||
# No breaking changes anymore, delete the old comment
|
||||
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" -X DELETE
|
||||
fi
|
||||
|
||||
- name: Report no breaking changes
|
||||
if: steps.breaking-check.outcome == 'success'
|
||||
run: |
|
||||
echo "## No Breaking API Changes Detected" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "The API contract is backward compatible." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Archive breaking change report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-compatibility-report
|
||||
path: current/fineract-provider/build/swagger-brake/
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail if breaking changes detected
|
||||
if: steps.breaking-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::Breaking API changes detected. See the report above for details."
|
||||
exit 1
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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: Fineract Signed Commits Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify-signatures:
|
||||
name: Verify Commit Signatures
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch base branch
|
||||
run: git fetch origin ${{ github.base_ref }}
|
||||
|
||||
- name: Verify signatures
|
||||
run: |
|
||||
chmod +x scripts/verify-signed-commits.sh
|
||||
./scripts/verify-signed-commits.sh \
|
||||
--base-ref origin/${{ github.base_ref }} \
|
||||
--head-ref ${{ github.event.pull_request.head.sha }} \
|
||||
--strict
|
||||
@@ -0,0 +1,163 @@
|
||||
name: Verify Liquibase Backward Compatibility
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
liquibase-backward-compatibility-check:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:18.2
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd="pg_isready -q -d postgres -U root"
|
||||
--health-interval=5s
|
||||
--health-timeout=2s
|
||||
--health-retries=5
|
||||
|
||||
env:
|
||||
DB_USER: root
|
||||
DB_PASSWORD: postgres
|
||||
DB_NAME: fineract_default
|
||||
TZ: Asia/Kolkata
|
||||
|
||||
steps:
|
||||
- name: Checkout the base branch (`develop`)
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: |
|
||||
until pg_isready -h localhost -U $DB_USER; do
|
||||
echo "Waiting for postgres..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Init base schema
|
||||
run: |
|
||||
./gradlew --no-daemon createPGDB -PdbName=fineract_tenants
|
||||
./gradlew --no-daemon createPGDB -PdbName=$DB_NAME
|
||||
|
||||
- name: Start backend on base branch
|
||||
run: |
|
||||
./gradlew :fineract-provider:devRun --args="\
|
||||
--spring.datasource.hikari.driverClassName=org.postgresql.Driver \
|
||||
--spring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants \
|
||||
--spring.datasource.hikari.username=root \
|
||||
--spring.datasource.hikari.password=postgres \
|
||||
--fineract.tenant.host=localhost \
|
||||
--fineract.tenant.port=5432 \
|
||||
--fineract.tenant.username=root \
|
||||
--fineract.tenant.password=postgres" &
|
||||
BACKEND_PID=$!
|
||||
echo $BACKEND_PID > backend.pid
|
||||
|
||||
# Wait for Actuator to come up (adjust host/port/path if needed)
|
||||
ACTUATOR_URL="https://localhost:8443/fineract-provider/actuator/health"
|
||||
TIMEOUT_SECONDS=600
|
||||
INTERVAL_SECONDS=2
|
||||
|
||||
echo "Waiting for backend Actuator: $ACTUATOR_URL (timeout ${TIMEOUT_SECONDS}s)..."
|
||||
|
||||
start_ts=$(date +%s)
|
||||
while true; do
|
||||
# If the process died, fail fast
|
||||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||||
echo "Backend process exited before Actuator became available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check endpoint
|
||||
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
|
||||
echo "Actuator is up."
|
||||
break
|
||||
fi
|
||||
|
||||
# Timeout
|
||||
now_ts=$(date +%s)
|
||||
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
|
||||
echo "Timed out waiting for Actuator."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$INTERVAL_SECONDS"
|
||||
done
|
||||
|
||||
- name: Stop backend
|
||||
run: |
|
||||
kill $(cat backend.pid)
|
||||
sleep 10
|
||||
|
||||
- name: Checkout the PR branch
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Start backend on PR branch
|
||||
run: |
|
||||
./gradlew :fineract-provider:devRun --args="\
|
||||
--spring.datasource.hikari.driverClassName=org.postgresql.Driver \
|
||||
--spring.datasource.hikari.jdbcUrl=jdbc:postgresql://localhost:5432/fineract_tenants \
|
||||
--spring.datasource.hikari.username=root \
|
||||
--spring.datasource.hikari.password=postgres \
|
||||
--fineract.tenant.host=localhost \
|
||||
--fineract.tenant.port=5432 \
|
||||
--fineract.tenant.username=root \
|
||||
--fineract.tenant.password=postgres" &
|
||||
BACKEND_PID=$!
|
||||
echo $BACKEND_PID > backend.pid
|
||||
|
||||
# Wait for Actuator to come up (adjust host/port/path if needed)
|
||||
ACTUATOR_URL="https://localhost:8443/fineract-provider/actuator/health"
|
||||
TIMEOUT_SECONDS=600
|
||||
INTERVAL_SECONDS=2
|
||||
|
||||
echo "Waiting for backend Actuator: $ACTUATOR_URL (timeout ${TIMEOUT_SECONDS}s)..."
|
||||
|
||||
start_ts=$(date +%s)
|
||||
while true; do
|
||||
# If the process died, fail fast
|
||||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||||
echo "Backend process exited before Actuator became available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check endpoint
|
||||
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
|
||||
echo "Actuator is up."
|
||||
break
|
||||
fi
|
||||
|
||||
# Timeout
|
||||
now_ts=$(date +%s)
|
||||
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
|
||||
echo "Timed out waiting for Actuator."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$INTERVAL_SECONDS"
|
||||
done
|
||||
- name: Stop backend
|
||||
run: |
|
||||
kill $(cat backend.pid)
|
||||
sleep 10
|
||||
@@ -1,3 +1,20 @@
|
||||
# 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.
|
||||
|
||||
# .mailmap unifies differences in commit names and emails, and shortens committers' names.
|
||||
# .mailmap format is a bit weird, e.g. https://github.com/sympy/sympy/wiki/Using-.mailmap explains it.
|
||||
# .mailmap official doc is on https://git-scm.com/docs/git-shortlog#_mapping_authors
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(type *)",
|
||||
"mcp__idea__get_symbol_info",
|
||||
"Bash(gradlew *)",
|
||||
"Bash(select-string *)",
|
||||
"Bash(select-object *)",
|
||||
"Bash(dir *)",
|
||||
"Bash(findstr *)",
|
||||
"Bash(./gradlew *)",
|
||||
"Bash(gradlew.bat *)",
|
||||
"Bash(taskkill *)",
|
||||
"Bash(timeout *)",
|
||||
"Bash(.\\gradlew.bat *)",
|
||||
"Bash(get-content *)",
|
||||
"Bash(tasklist)",
|
||||
"Bash(curl *)",
|
||||
"Bash(netstat *)",
|
||||
"Bash(curl.exe *)",
|
||||
"Bash(rmdir *)",
|
||||
"mcp__idea__build_project",
|
||||
"Bash(.\\gradlew *)",
|
||||
"Bash(.\\clean_build.bat)",
|
||||
"mcp__idea__find_files_by_name_keyword",
|
||||
"mcp__idea__search_in_files_by_text",
|
||||
"mcp__idea__get_file_text_by_path",
|
||||
"mcp__idea__search_in_files_by_regex",
|
||||
"Bash(powershell *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(type *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "CBS — CIN & DIN Verification (LincPay eKYC)",
|
||||
"description": "Postman collection for CIN (Company Identification Number) and DIN (Director Identification Number) verification via the CBS Fineract KYC module.\n\nFlow:\n1. Create/update a KYC record with cinNumber and/or dinNumber\n2. Trigger verify-cin or verify-din command\n3. The backend calls LincPay eKYC API (validate-company / validate-director)\n4. On success, cinVerified/dinVerified flags are set on the KYC record",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "https://localhost:8443/fineract-provider/api/v1",
|
||||
"description": "Fineract API base URL"
|
||||
},
|
||||
{
|
||||
"key": "tenant_id",
|
||||
"value": "default",
|
||||
"description": "Tenant identifier"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "mifos",
|
||||
"description": "API username"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "password",
|
||||
"description": "API password"
|
||||
},
|
||||
{
|
||||
"key": "client_id",
|
||||
"value": "1",
|
||||
"description": "Client ID for KYC operations"
|
||||
},
|
||||
{
|
||||
"key": "kyc_id",
|
||||
"value": "1",
|
||||
"description": "KYC record ID"
|
||||
},
|
||||
{
|
||||
"key": "cin_number",
|
||||
"value": "U12345MH2020PTC123456",
|
||||
"description": "21-character CIN for company verification"
|
||||
},
|
||||
{
|
||||
"key": "din_number",
|
||||
"value": "12345678",
|
||||
"description": "8-digit DIN for director verification"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{ "key": "username", "value": "{{username}}", "type": "string" },
|
||||
{ "key": "password", "value": "{{password}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "1. Initiate KYC with CIN & DIN",
|
||||
"item": [
|
||||
{
|
||||
"name": "Initiate KYC (Non-Individual with CIN & DIN)",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('Status code is 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('KYC record created', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('resourceId');",
|
||||
" pm.collectionVariables.set('kyc_id', json.resourceId);",
|
||||
"});"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"kycType\": 2,\n \"riskCategory\": 2,\n \"panNumber\": \"ABCDE1234F\",\n \"gstinNumber\": \"27ABCDE1234F1Z5\",\n \"cinNumber\": \"{{cin_number}}\",\n \"dinNumber\": \"{{din_number}}\",\n \"remarks\": \"Non-individual KYC with CIN and DIN\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/clients/{{client_id}}/kyc",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["clients", "{{client_id}}", "kyc"]
|
||||
},
|
||||
"description": "Initiate a KYC record for a non-individual client with CIN and DIN numbers.\n\n- kycType: 2 = NON_INDIVIDUAL\n- riskCategory: 1=LOW, 2=MEDIUM, 3=HIGH\n- cinNumber: 21-character Corporate Identification Number\n- dinNumber: 8-digit Director Identification Number"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update KYC — Add CIN & DIN",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cinNumber\": \"{{cin_number}}\",\n \"dinNumber\": \"{{din_number}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"]
|
||||
},
|
||||
"description": "Update an existing KYC record to add CIN and/or DIN numbers before triggering verification."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "Create or update a KYC record with CIN and DIN numbers before triggering verification."
|
||||
},
|
||||
{
|
||||
"name": "2. CIN Verification (Company)",
|
||||
"item": [
|
||||
{
|
||||
"name": "Verify CIN — Trigger LincPay validate-company",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('Status code is 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('CIN verified successfully', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json.changes).to.have.property('cinVerified');",
|
||||
" pm.expect(json.changes.cinVerified).to.be.true;",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('cinVerifiedOn date is set', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json.changes).to.have.property('cinVerifiedOn');",
|
||||
"});"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}?command=verify-cin",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"],
|
||||
"query": [
|
||||
{ "key": "command", "value": "verify-cin" }
|
||||
]
|
||||
},
|
||||
"description": "Trigger CIN verification via LincPay MCA API.\n\n**Backend flow:**\n1. Reads cinNumber from the KYC record\n2. Calls LincPay: POST https://ekyc.lincpay.in/EKYC/ekyc/validate-company\n Body: { \"apiKey\": \"...\", \"cin\": \"U12345MH2020PTC123456\", \"secretKey\": \"...\" }\n3. On success: marks cinVerified=true, cinVerifiedOn=today\n4. On failure: returns error with vendor message\n\n**Prerequisites:**\n- KYC record must exist with a cinNumber set\n- KYC must not be in REJECTED status"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "200 — CIN Verified Successfully",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": { "mode": "raw", "raw": "{}" },
|
||||
"url": { "raw": "{{base_url}}/clients/1/kyc/1?command=verify-cin" }
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"body": "{\n \"officeId\": 1,\n \"clientId\": 1,\n \"resourceId\": 1,\n \"changes\": {\n \"cinVerified\": true,\n \"cinVerifiedOn\": [2026, 4, 3],\n \"kycStatus\": 200\n }\n}"
|
||||
},
|
||||
{
|
||||
"name": "400 — CIN Not Set",
|
||||
"status": "Bad Request",
|
||||
"code": 400,
|
||||
"_postman_previewlanguage": "json",
|
||||
"body": "{\n \"developerMessage\": \"CIN number is not set on this KYC record. Update the KYC record first.\",\n \"httpStatusCode\": \"503\",\n \"defaultUserMessage\": \"CIN number is not set on this KYC record. Update the KYC record first.\",\n \"userMessageGlobalisationCode\": \"error.msg.client.kyc.cin.not.found\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Direct LincPay CIN API (for testing)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "accept", "value": "*/*" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"apiKey\": \"YOUR_API_KEY\",\n \"cin\": \"{{cin_number}}\",\n \"secretKey\": \"YOUR_SECRET_KEY\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://ekyc.lincpay.in/EKYC/ekyc/validate-company",
|
||||
"protocol": "https",
|
||||
"host": ["ekyc", "lincpay", "in"],
|
||||
"path": ["EKYC", "ekyc", "validate-company"]
|
||||
},
|
||||
"description": "Direct call to LincPay validate-company API for testing.\nReplace apiKey and secretKey with actual credentials."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "CIN (Corporate Identification Number) verification flow. CIN is a 21-character alphanumeric code allotted by MCA to companies registered under the Companies Act."
|
||||
},
|
||||
{
|
||||
"name": "3. DIN Verification (Director)",
|
||||
"item": [
|
||||
{
|
||||
"name": "Verify DIN — Trigger LincPay validate-director",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('Status code is 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('DIN verified successfully', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json.changes).to.have.property('dinVerified');",
|
||||
" pm.expect(json.changes.dinVerified).to.be.true;",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('dinVerifiedOn date is set', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json.changes).to.have.property('dinVerifiedOn');",
|
||||
"});"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}?command=verify-din",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"],
|
||||
"query": [
|
||||
{ "key": "command", "value": "verify-din" }
|
||||
]
|
||||
},
|
||||
"description": "Trigger DIN verification via LincPay MCA API.\n\n**Backend flow:**\n1. Reads dinNumber from the KYC record\n2. Calls LincPay: POST https://ekyc.lincpay.in/EKYC/ekyc/validate-director\n Body: { \"apiKey\": \"...\", \"din\": \"12345678\", \"secretKey\": \"...\" }\n3. On success: marks dinVerified=true, dinVerifiedOn=today\n4. On failure: returns error with vendor message\n\n**Prerequisites:**\n- KYC record must exist with a dinNumber set\n- KYC must not be in REJECTED status"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "200 — DIN Verified Successfully",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": { "mode": "raw", "raw": "{}" },
|
||||
"url": { "raw": "{{base_url}}/clients/1/kyc/1?command=verify-din" }
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"body": "{\n \"officeId\": 1,\n \"clientId\": 1,\n \"resourceId\": 1,\n \"changes\": {\n \"dinVerified\": true,\n \"dinVerifiedOn\": [2026, 4, 3],\n \"kycStatus\": 200\n }\n}"
|
||||
},
|
||||
{
|
||||
"name": "400 — DIN Not Set",
|
||||
"status": "Bad Request",
|
||||
"code": 400,
|
||||
"_postman_previewlanguage": "json",
|
||||
"body": "{\n \"developerMessage\": \"DIN number is not set on this KYC record. Update the KYC record first.\",\n \"httpStatusCode\": \"503\",\n \"defaultUserMessage\": \"DIN number is not set on this KYC record. Update the KYC record first.\",\n \"userMessageGlobalisationCode\": \"error.msg.client.kyc.din.not.found\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Direct LincPay DIN API (for testing)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "accept", "value": "*/*" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"apiKey\": \"YOUR_API_KEY\",\n \"din\": \"{{din_number}}\",\n \"secretKey\": \"YOUR_SECRET_KEY\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://ekyc.lincpay.in/EKYC/ekyc/validate-director",
|
||||
"protocol": "https",
|
||||
"host": ["ekyc", "lincpay", "in"],
|
||||
"path": ["EKYC", "ekyc", "validate-director"]
|
||||
},
|
||||
"description": "Direct call to LincPay validate-director API for testing.\nReplace apiKey and secretKey with actual credentials."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "DIN (Director Identification Number) verification flow. DIN is an 8-digit unique number allotted by MCA to company directors."
|
||||
},
|
||||
{
|
||||
"name": "4. Get KYC Record",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get KYC Record (check CIN/DIN verification status)",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('Status code is 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('CIN fields present', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('cinNumber');",
|
||||
" pm.expect(json).to.have.property('cinVerified');",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('DIN fields present', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('dinNumber');",
|
||||
" pm.expect(json).to.have.property('dinVerified');",
|
||||
"});"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"]
|
||||
},
|
||||
"description": "Retrieve the KYC record to verify CIN/DIN verification status after running the verify commands."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+289
-9
@@ -1,16 +1,296 @@
|
||||
# Contributing
|
||||
|
||||
Hello there!
|
||||
|
||||
First of all, **Thank You** for contributing to Apache Fineract, we're grateful for your interest in our project.
|
||||
First of all, **Thank You** for contributing to Apache Fineract! We are grateful for your interest in this project.
|
||||
|
||||
Please do [join our developer mailing list](https://fineract.apache.org/#contribute), as it's where discussions about this project take place - say _Hi_ there!
|
||||
Please join the [developer mailing list](https://fineract.apache.org/#contribute), if you have not already done so, as this is where discussions about this project take place - say _Hi_ there! Please also have a quick look at the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
Our [JIRA Dashboard](https://issues.apache.org/jira/secure/Dashboard.jspa?selectPageId=12335824) is another good way to see what's going on (do create a Login).
|
||||
The [JIRA Dashboard](https://issues.apache.org/jira/secure/Dashboard.jspa?selectPageId=12335824) shows what's going on. Create a login - it can take a day or two. If you face difficulties, ask on the mailing list.
|
||||
|
||||
Here are a few additional useful pointers:
|
||||
You don't need to be a committer to provide pull requests, but [Becoming a Committer](https://cwiki.apache.org/confluence/display/FINERACT/Becoming+a+Committer) explains the process of becoming one - just in case...
|
||||
|
||||
* https://github.com/apache/fineract/#pull-requests
|
||||
* https://github.com/apache/fineract/#checkstyle-and-spotless
|
||||
* https://github.com/apache/fineract/#logging-guidelines
|
||||
* https://github.com/apache/fineract/#error-handling-guidelines
|
||||
|
||||
Note also [our Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
## Developer How To's
|
||||
|
||||
### How to run tests
|
||||
|
||||
#### Unit tests
|
||||
|
||||
Here's how to run the set of relatively fast and independent Fineract tests:
|
||||
|
||||
```bash
|
||||
./gradlew test -x :twofactor-tests:test -x :oauth2-tests:test -x :integration-tests:test
|
||||
```
|
||||
|
||||
This runs nearly 1,000 tests and completes in a few minutes on decent hardware.
|
||||
They shouldn't need any special servers/services running.
|
||||
|
||||
#### Integration tests
|
||||
|
||||
Running tests with external dependencies is a multi-step process with many moving parts.
|
||||
Sometimes there are arbitrary failures and the prerequisite setup can be daunting.
|
||||
A full local integration test run (on a developer workstation) covering every possible test using every external service and every supported relational database engine could take an entire day - and that's assuming everything is properly configured and runs as expected.
|
||||
|
||||
Right now we depend on GitHub to know if "the build" is passing (it's actually multiple builds).
|
||||
The authoritative source of truth for what commands/services/tests to run, how, and when are the files in `.github/workflows/`.
|
||||
Output from runs based on those configuration files appears at <https://github.com/apache/fineract/actions>.
|
||||
|
||||
Incorrect default Java-related executables may cause test failures.
|
||||
To fix this on Debian and Ubuntu systems, run the following:
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/usr/lib/jvm/zulu21
|
||||
sudo update-alternatives --set java $JAVA_HOME/bin/java
|
||||
sudo update-alternatives --set javac $JAVA_HOME/bin/javac
|
||||
sudo update-alternatives --set javadoc $JAVA_HOME/bin/javadoc
|
||||
```
|
||||
|
||||
This would correct, for example, a [class file version error](https://en.wikipedia.org/wiki/Java_class_file#General_layout).
|
||||
You might see something like this if a Java 11 executable (class file format version 56) was the system default, but the integration tests were using Java 21 (class file format version 65):
|
||||
|
||||
> UnsupportedClassVersionError: com.example.package/ClassName has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 55.0
|
||||
|
||||
The GitHub builds are run in [short-lived virtual machines](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners), so locally reproducing the same may require additional effort, such as these extra clean-up procedures:
|
||||
|
||||
```bash
|
||||
# Might fix `error: cannot find symbol` or other intermittent failures.
|
||||
# `doc` here is a placeholder for any task(s) you are trying to run.
|
||||
# 💚 This is generally very safe to run between builds.
|
||||
./gradlew --refresh-dependencies doc
|
||||
|
||||
# Destroy anything untracked by git.
|
||||
# ⚠️ This may delete something important, e.g. a finely-tuned IDE configuration.
|
||||
git clean --force -dx
|
||||
|
||||
# Destroy various caches and configs.
|
||||
# ⚠️ This may delete gibibytes of cached data, making the next build very slow.
|
||||
rm -rf ~/.gradle ~/.m2 /tmp/cargo*
|
||||
|
||||
# Destroy any Java containers left running.
|
||||
# 💚 This is generally very safe to run between builds.
|
||||
ps auxwww | grep [c]argo | awk '{ print $2 }' | xargs -r kill
|
||||
```
|
||||
|
||||
Integration test runs such as
|
||||
```bash
|
||||
./gradlew --no-daemon --console=plain test -x :twofactor-tests:test \
|
||||
-x :oauth2-tests:test :fineract-e2e-tests-runner:test -PdbType=postgresql
|
||||
```
|
||||
in `.github/workflows/build-postgresql.yml` often take an hour or longer to complete.
|
||||
If you notice the `:integration-tests:test` task taking significantly less time, say, one minute, gradle may be skipping it.
|
||||
Look for something like this in the test output:
|
||||
|
||||
> Task :integration-tests:test UP-TO-DATE 👀
|
||||
Custom actions are attached to task ':integration-tests:test'.
|
||||
Build cache key for task ':integration-tests:test' is 6aeeec3f58bf9703d4c100fbaa657f5c
|
||||
Skipping task ':integration-tests:test' as it is up-to-date.
|
||||
Resolve mutations for :integration-tests:cargoStopLocal (Thread[Execution worker Thread 11,5,main]) started.
|
||||
:integration-tests:cargoStopLocal (Thread[Execution worker Thread 11,5,main]) started.
|
||||
|
||||
|
||||
(This is with the `--info` gradle argument with eyeballs added for emphasis.)
|
||||
The `--rerun-tasks` gradle argument may help, or you can try destroying `~/.gradle` and other clean-up procedures as indicated above, then re-running tests.
|
||||
This is useful for repeated test runs (say, for timing) when gradle would otherwise assume a task is "up-to-date" and not re-run it.
|
||||
|
||||
See the next section for testing in Eclipse and [here](https://fineract-academy.com) for testing in IntelliJ.
|
||||
|
||||
## Recommended IDEs
|
||||
|
||||
Apache Fineract can be developed using multiple IDEs. The most commonly used and supported IDEs are:
|
||||
|
||||
- **Eclipse IDE** – Fully supported and commonly used within the community.
|
||||
- **IntelliJ IDEA (Community or Ultimate)** – Recommended for most developers due to excellent Gradle support and debugging experience.
|
||||
|
||||
The sections below provide setup guidance for each IDE.
|
||||
|
||||
### How to run and debug in Eclipse IDE
|
||||
|
||||
It is possible to run Fineract in Eclipse IDE and also to debug Fineract using Eclipse's debugging facilities.
|
||||
To do this, you need to create the Eclipse project files and import the project into an Eclipse workspace:
|
||||
|
||||
1. Create Eclipse project files into the Fineract project by running `./gradlew cleanEclipse eclipse`
|
||||
2. Import the fineract-provider project into your Eclipse workspace (File->Import->General->Existing Projects into Workspace, choose root directory fineract/fineract-provider)
|
||||
3. Do a clean build of the project in Eclipse (Project->Clean...)
|
||||
3. Run / debug Fineract by right clicking on org.apache.fineract.ServerApplication class and choosing Run As / Debug As -> Java Application. All normal Eclipse debugging features (breakpoints, watchpoints etc) should work as expected.
|
||||
|
||||
If you change the project settings (dependencies etc) in Gradle, you should redo step 1 and refresh the project in Eclipse.
|
||||
|
||||
You can also use Eclipse JUnit support to run tests in Eclipse (Run As->JUnit Test)
|
||||
|
||||
Finally, modifying source code in Eclipse automatically triggers hot code replace to a running instance, allowing you to immediately test your changes
|
||||
|
||||
## How to run and debug in IntelliJ IDEA
|
||||
|
||||
IntelliJ IDEA provides strong Gradle integration and is recommended for most new contributors.
|
||||
|
||||
### Prerequisites
|
||||
- IntelliJ IDEA (Community Edition is sufficient)
|
||||
- Java 21 (as required by Fineract)
|
||||
- Gradle (wrapper included in the repository)
|
||||
|
||||
### Steps
|
||||
|
||||
1. Open IntelliJ IDEA and select **Open**
|
||||
2. Choose the root `fineract` directory
|
||||
3. When prompted, import the project as a **Gradle project**
|
||||
4. Use the Gradle wrapper (`gradlew`) when asked
|
||||
5. Ensure the correct JDK is selected (**Java 21**)
|
||||
6. After import completes, run:
|
||||
- `org.apache.fineract.ServerApplication` as a **Java Application**
|
||||
|
||||
### Notes
|
||||
- IntelliJ may take several minutes to index the project on first open
|
||||
- If you encounter build issues, try running `./gradlew clean build` from the terminal
|
||||
|
||||
How to download Gradle wrapper
|
||||
---
|
||||
The file gradle/wrapper/gradle-wrapper.jar binary is checked into this projects Git source repository,
|
||||
but won't exist in your copy of the Fineract codebase if you downloaded a released source archive from apache.org.
|
||||
In that case, you need to download it using the commands below:
|
||||
```bash
|
||||
wget -P gradle/wrapper https://github.com/apache/fineract/raw/develop/gradle/wrapper/gradle-wrapper.jar
|
||||
```
|
||||
or
|
||||
```bash
|
||||
curl -L https://github.com/apache/fineract/raw/develop/gradle/wrapper/gradle-wrapper.jar > \
|
||||
gradle/wrapper/gradle-wrapper.jar
|
||||
```
|
||||
|
||||
### How to run Apache RAT (Release Audit Tool)
|
||||
|
||||
1. Extract the archive file to your local directory.
|
||||
2. Run `./gradlew rat`. A report will be generated under build/reports/rat/rat-report.txt
|
||||
|
||||
|
||||
### How to build documentation
|
||||
|
||||
Run the following command:
|
||||
|
||||
```bash
|
||||
./gradlew doc
|
||||
```
|
||||
|
||||
Some dependencies are required (e.g. Ghostscript, Graphviz), see [.github/workflows/build-documentation.yml](https://github.com/apache/fineract/tree/develop/.github/workflows/build-documentation.yml) for hints.
|
||||
|
||||
IDEs such as IntelliJ are useful for editing the AsciiDoc source files while providing a live rendered preview.
|
||||
|
||||
HTML rendered from the AsciiDoc source files is also available online at [https://fineract.apache.org/docs/current/](https://fineract.apache.org/docs/current/).
|
||||
|
||||
|
||||
## How We Code
|
||||
|
||||
### Checkstyle and Spotless
|
||||
|
||||
This project enforces its code conventions using [checkstyle.xml](config/checkstyle/checkstyle.xml) through Checkstyle and [fineractdev-formatter.xml](config/fineractdev-formatter.xml) through Spotless. They are configured to run automatically during the normal Gradle build, and fail if there are any violations detected. You can run the following command to automatically fix spotless violations:
|
||||
```bash
|
||||
./gradlew spotlessApply
|
||||
```
|
||||
Since some checks are present in both Checkstyle and Spotless, the same command can help you fix some of the Checkstyle violations too.
|
||||
|
||||
You can also check solely for Spotless violations, but normally don't have to, because regular builds already include this:
|
||||
```bash
|
||||
./gradlew spotlessCheck
|
||||
```
|
||||
|
||||
We recommend that you configure your favourite Java IDE to match those conventions. For Eclipse, you can go to
|
||||
Window > Java > Code Style and import the aforementioned [config/fineractdev-formatter.xml](config/fineractdev-formatter.xml) under formatter section and [config/fineractdev-cleanup.xml](config/fineractdev-cleanup.xml) under cleanup section.
|
||||
|
||||
You could also use Checkstyle directly in your IDE, but you don't have to: it may just be more convenient for you. For Eclipse, use https://checkstyle.org/eclipse-cs/ and load our checkstyle.xml into it. For IntelliJ you can use [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea).
|
||||
|
||||
|
||||
### Code Coverage
|
||||
|
||||
Changed or added code should ideally have test coverage.
|
||||
|
||||
The project uses Jacoco to measure unit tests code coverage. To generate a report run the following command:
|
||||
```bash
|
||||
./gradlew clean build jacocoTestReport
|
||||
```
|
||||
Generated reports can be found in the build/code-coverage directory.
|
||||
|
||||
|
||||
### Lombok
|
||||
|
||||
The project uses [Lombok](https://projectlombok.org/) to reduce boilerplate code. Configuration is in [lombok.config](lombok.config).
|
||||
|
||||
* Use `@Getter` / `@Setter` / `@NoArgsConstructor` on JPA entities. Never use `@Data` on entities (causes JPA lazy loading issues).
|
||||
* Use `@RequiredArgsConstructor` on service classes for constructor-based dependency injection.
|
||||
* Use `@Slf4j` for logging instead of manually declaring loggers.
|
||||
* Use `@Data` for simple DTOs (includes getters, setters, toString, equals, hashCode).
|
||||
* Use `@Builder` with `@Builder.Default` for configuration classes and complex object construction.
|
||||
* Never use `@SneakyThrows` - handle exceptions explicitly.
|
||||
|
||||
|
||||
### Error Handling
|
||||
|
||||
* When catching exceptions, either rethrow them, or log them. Either way, include the root cause by using `catch (SomeException e)` and then either `throw AnotherException("..details..", e)` or `LOG.error("...context...", e)`.
|
||||
* Completely empty catch blocks are VERY suspicious. Are you sure that you want to just "swallow" an exception? Really, 100% totally absolutely sure?? ;-) Such "normal exceptions which just happen sometimes but are actually not really errors" are almost always a bad idea, can be a performance issue, and typically are an indication of another problem - e.g. the use of a wrong API which throws an Exception for an expected condition, when really you would want to use another API that instead returns something empty or optional.
|
||||
* In tests, you'll typically never catch exceptions, but just propagate them, with `@Test void testXYZ() throws SomeException, AnotherException`..., so that the test fails if the exception happens. Unless you actually really want to test for the occurrence of a problem - in that case, use [JUnit's Assert.assertThrows()](https://github.com/junit-team/junit4/wiki/Exception-testing) (but not `@Test(expected = SomeException.class)`).
|
||||
* Never catch `NullPointerException` & Co.
|
||||
|
||||
### Logging
|
||||
|
||||
* We use [SLF4J](http://www.slf4j.org) as our logging API.
|
||||
* Never, ever, use `System.out` and `System.err` or `printStackTrace()` anywhere, but always `LOG.info()` or `LOG.error()` instead.
|
||||
* Use placeholder (`LOG.error("Could not... details: {}", something, exception)`) and never String concatenation (`LOG.error("Could not... details: " + something, exception)`)
|
||||
* Which Log Level is appropriate?
|
||||
* `LOG.error()` should be used to inform an "operator" running Fineract who supervises error logs of an unexpected condition. This includes technical problems with an external "environment" (e.g. can't reach a database), and situations which are likely bugs which need to be fixed in the code. They do NOT include e.g. validation errors for incoming API requests - that is signaled through the API response - and does (should) not be logged as an error. (Note that there is no _FATAL_ level in SLF4J; a "FATAL" event should just be logged as an _ERROR_.)
|
||||
* `LOG.warn()` should be using sparingly. Make up your mind if it's an error (above) - or not!
|
||||
* `LOG.info()` can be used notably for one-time actions taken during start-up. It should typically NOT be used to print out "regular" application usage information. The default logging configuration always outputs the application INFO logs, and in production under load, there's really no point to constantly spew out lots of information from frequently traversed paths in the code about what's going on. (Metrics are a better way.) `LOG.info()` *can* be used freely in tests though.
|
||||
* `LOG.debug()` can be used anywhere in the code to log things that may be useful during investigations of specific problems. They are not shown in the default logging configuration, but can be enabled for troubleshooting. Developers should typically "turn down" most `LOG.info()` which they used while writing a new feature to "follow along what happens during local testing" to `LOG.debug()` for production before we merge their PRs.
|
||||
* `LOG.trace()` is not used in Fineract.
|
||||
|
||||
## Change Process
|
||||
|
||||
### Dependency Upgrades
|
||||
|
||||
This project uses a number of 3rd-party libraries. We have set up [Renovate's bot](https://github.com/renovatebot/github-action) to automatically raise Pull Requests for our review when new dependencies are available.
|
||||
|
||||
Our `ClasspathHellDuplicatesCheckRuleTest` detects classes that appear in more than 1 JAR. If a version bump in [build.gradle](https://github.com/apache/fineract/blob/develop/build.gradle) causes changes in transitives dependencies, then you may have to add related `exclude` to our [dependencies.gradle](https://github.com/apache/fineract/search?q=dependencies.gradle). Running `./gradlew dependencies` helps to understand what is required.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
We request that your commit message includes a FINERACT JIRA issue and a one-liner that describes the changes.
|
||||
Start with an upper case imperative verb (not past form), and a short but concise clear description. (E.g. "FINERACT-821: Add enforced HideUtilityClassConstructor checkstyle").
|
||||
|
||||
If your PR is failing to pass our CI build due to a test failure, then:
|
||||
|
||||
1. Understand if the failure is due to your PR or an unrelated unstable test.
|
||||
1. If you suspect it is because of a "flaky" test, and not due to a change in your PR, then please do not simply wait for an active maintainer to come and help you, but instead be a proactive contributor to the project - see next steps. Do understand that we may not review PRs that are not green - it is the contributor's (that's you!) responsibility to get a proposed PR to pass the build, not primarily the maintainers.
|
||||
1. Search for the name of the failed test on https://issues.apache.org/jira/, e.g. for `AccountingScenarioIntegrationTest` you would find [FINERACT-899](https://issues.apache.org/jira/browse/FINERACT-899).
|
||||
1. If you happen to read in such bug reports that tests were just recently fixed, or ignored, then rebase your PR to pick up that change.
|
||||
1. If you find previous comments "proving" that the same test has arbitrarily failed in at least 3 past PRs, then please do yourself raise a small separate new PR proposing to add an `@Disabled // TODO FINERACT-123` to the respective unstable test (e.g. [#774](https://github.com/apache/fineract/pull/774)) with the commit message mentioning said JIRA, as always. (Please do NOT just `@Disabled` any existing tests mixed in as part of your larger PR.)
|
||||
1. If there is no existing JIRA for the test, then first please evaluate whether the failure couldn't be a (perhaps strange) impact of the change you are proposing after all. If it's not, then please raise a new JIRA to document the suspected Flaky Test, and link it to [FINERACT-850](https://issues.apache.org/jira/browse/FINERACT-850). This will allow the next person coming along hitting the same test failure to easily find it, and eventually propose to ignore the unstable test.
|
||||
1. Then (only) Close and Reopen your PR, which will cause a new build, to see if it passes.
|
||||
1. Of course, we very much appreciate you then jumping onto any such bugs and helping us figure out how to fix all ignored tests!
|
||||
|
||||
[Pull Request Size Limit](https://cwiki.apache.org/confluence/display/FINERACT/Pull+Request+Size+Limit)
|
||||
documents that we cannot accept huge "code dump" Pull Requests, with some related suggestions.
|
||||
|
||||
Guideline for new Feature commits involving Refactoring: If you are submitting a PR for a new feature,
|
||||
and it involves refactoring, try to differentiate "new feature code" from "refactored" by placing
|
||||
them in different commits. This helps to review your code faster.
|
||||
|
||||
We have an automated bot which marks pull requests as "stale" after a while, and ultimately automatically closes them.
|
||||
|
||||
|
||||
### Merge Strategy
|
||||
|
||||
This project's committers typically prefer to bring your pull requests in through _Rebase and Merge_ instead of _Create a Merge Commit_. (If you are unfamiliar with GitHub's UI regarding this, note the somewhat hidden little triangle drop-down at the bottom of the PR, visible only to committers, not contributors.) This avoids the "merge commits" which we consider to be somewhat "polluting" the project's commit log history view. We understand this doesn't give an easy automatic reference to the original PR (which GitHub automatically adds to the merge commit message it generates), but we consider this an only very minor inconvenience; it's typically relatively easy to find the original PR even just from the commit message, and JIRA.
|
||||
|
||||
We expect most proposed PRs to typically consist of a single commit. Committers may use _Squash and merge_ to combine your commits at merge time, and if they do so, will rewrite your commit message as they see fit.
|
||||
|
||||
Neither of these two are hard absolute rules, but mere conventions. Multiple commits in single PRs make sense in certain cases (e.g. branch backports).
|
||||
|
||||
### Signing Your Commits
|
||||
|
||||
We encourage contributors to sign commits with GPG keys. Signed commits show a "Verified" badge on GitHub.
|
||||
|
||||
For GPG setup instructions, see the [Fineract GPG Guide](https://fineract.apache.org/docs/current/#_gpg_2).
|
||||
|
||||
To verify your commits locally before pushing:
|
||||
|
||||
```bash
|
||||
./scripts/verify-signed-commits.sh
|
||||
```
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# SavingsProduct Custom Fields - Implementation Summary
|
||||
|
||||
## ✅ Completed Changes
|
||||
|
||||
### 1. **Validator Updates**
|
||||
**File:** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java`
|
||||
|
||||
**Changes:**
|
||||
- Added `balanceFrequency` to `SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS` whitelist
|
||||
- Added `accountClosedZeroBalanceInDayClosing` to `SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS` whitelist
|
||||
|
||||
### 2. **API Constants**
|
||||
**File:** `fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java`
|
||||
|
||||
**Changes:**
|
||||
```java
|
||||
public static final String balanceFrequencyParamName = "balanceFrequency";
|
||||
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
||||
```
|
||||
|
||||
### 3. **Entity (Domain Model)**
|
||||
**File:** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java`
|
||||
|
||||
**Changes:**
|
||||
- Added field: `@Column(name = "balance_frequency", length = 50) private String balanceFrequency;`
|
||||
- Added field: `@Column(name = "account_closed_zero_balance_in_day_closing") private Boolean accountClosedZeroBalanceInDayClosing = false;`
|
||||
- Added getters: `getBalanceFrequency()`, `getAccountClosedZeroBalanceInDayClosing()`
|
||||
- Added setters: `setBalanceFrequency()`, `setAccountClosedZeroBalanceInDayClosing()`
|
||||
- Added update logic in `update()` method to handle changes to these fields
|
||||
|
||||
### 4. **Assembler**
|
||||
**File:** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java`
|
||||
|
||||
**Changes:**
|
||||
```java
|
||||
if (command.parameterExists("balanceFrequency")) {
|
||||
product.setBalanceFrequency(command.stringValueOfParameterNamed("balanceFrequency"));
|
||||
}
|
||||
if (command.parameterExists("accountClosedZeroBalanceInDayClosing")) {
|
||||
product.setAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("accountClosedZeroBalanceInDayClosing"));
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **DTO (Data Transfer Object)**
|
||||
**File:** `fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java`
|
||||
|
||||
**Changes:**
|
||||
```java
|
||||
@Getter @Setter private String balanceFrequency;
|
||||
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
||||
```
|
||||
|
||||
### 6. **Read Platform Service**
|
||||
**File:** `fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java`
|
||||
|
||||
**Changes:**
|
||||
- SQL Query: Added `sp.balance_frequency as balanceFrequency`
|
||||
- SQL Query: Added `sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing`
|
||||
- ResultSet mapping: Added extraction of both fields
|
||||
|
||||
### 7. **Database Migration**
|
||||
**File:** `database_migration_add_custom_fields.sql`
|
||||
|
||||
```sql
|
||||
ALTER TABLE m_savings_product
|
||||
ADD COLUMN balance_frequency VARCHAR(50) DEFAULT NULL,
|
||||
ADD COLUMN account_closed_zero_balance_in_day_closing BOOLEAN DEFAULT FALSE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 What These Fields Do
|
||||
|
||||
### **balanceFrequency** (String)
|
||||
- **Purpose:** Defines how often balance is calculated/checked
|
||||
- **Example Values:** "Daily", "Monthly", "Quarterly", "Yearly"
|
||||
- **Database Column:** `m_savings_product.balance_frequency`
|
||||
- **Type:** VARCHAR(50)
|
||||
|
||||
### **accountClosedZeroBalanceInDayClosing** (Boolean)
|
||||
- **Purpose:** Flag to automatically close accounts when balance reaches zero at day end
|
||||
- **Example Values:** `true` or `false`
|
||||
- **Default:** `false`
|
||||
- **Database Column:** `m_savings_product.account_closed_zero_balance_in_day_closing`
|
||||
- **Type:** BOOLEAN
|
||||
|
||||
---
|
||||
|
||||
## 🎯 API Usage
|
||||
|
||||
### **POST /v1/savingsproducts** (Create)
|
||||
```json
|
||||
{
|
||||
"name": "Saving Product",
|
||||
"shortName": "SD22",
|
||||
// ... other fields ...
|
||||
"balanceFrequency": "Monthly",
|
||||
"accountClosedZeroBalanceInDayClosing": true,
|
||||
// ... rest of fields ...
|
||||
}
|
||||
```
|
||||
|
||||
### **GET /v1/savingsproducts/{productId}** (Read)
|
||||
Response will include:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Saving Product",
|
||||
// ... other fields ...
|
||||
"balanceFrequency": "Monthly",
|
||||
"accountClosedZeroBalanceInDayClosing": true,
|
||||
// ... rest of fields ...
|
||||
}
|
||||
```
|
||||
|
||||
### **PUT /v1/savingsproducts/{productId}** (Update)
|
||||
```json
|
||||
{
|
||||
"balanceFrequency": "Quarterly",
|
||||
"accountClosedZeroBalanceInDayClosing": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Migration Steps
|
||||
|
||||
Run this SQL script to add the columns:
|
||||
|
||||
```bash
|
||||
mysql -u root -p your_database < database_migration_add_custom_fields.sql
|
||||
```
|
||||
|
||||
Or manually execute:
|
||||
```sql
|
||||
ALTER TABLE m_savings_product
|
||||
ADD COLUMN balance_frequency VARCHAR(50) DEFAULT NULL,
|
||||
ADD COLUMN account_closed_zero_balance_in_day_closing BOOLEAN DEFAULT FALSE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validation
|
||||
|
||||
The fields are now:
|
||||
- ✅ **Accepted** in CREATE request (POST)
|
||||
- ✅ **Saved** to database
|
||||
- ✅ **Returned** in GET response
|
||||
- ✅ **Updatable** via PUT request
|
||||
- ✅ **Validated** (no longer rejected as unsupported)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Run Database Migration:** Execute the SQL script
|
||||
2. **Rebuild Application:** `./gradlew clean build`
|
||||
3. **Restart Server:** Apply the changes
|
||||
4. **Test API:** Use your payload to create a savings product
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Both fields are **optional** (nullable)
|
||||
- `balanceFrequency` accepts any string value (you can add validation later if needed)
|
||||
- `accountClosedZeroBalanceInDayClosing` defaults to `false` if not provided
|
||||
- Both fields support **partial updates** (you can update just one of them)
|
||||
@@ -0,0 +1,277 @@
|
||||
i {
|
||||
"info": {
|
||||
"name": "Entity Classification - CBS Fineract",
|
||||
"description": "Entity Classification module for non-individual client onboarding with 7 regulatory categories:\n1. Startup India (DPIIT)\n2. Mudra Borrower (PMMY)\n3. Export Oriented Unit (EOU)\n4. Import/Export (IEC/DGFT)\n5. Priority Sector Lending (PSL)\n6. Non-Priority Sector (NPS)\n7. MSME (Udyam Registration)",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "bankadmin",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "LincPay@2026",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://localhost:8443/fineract-provider/api/v1"
|
||||
},
|
||||
{
|
||||
"key": "clientId",
|
||||
"value": "49"
|
||||
},
|
||||
{
|
||||
"key": "tenantId",
|
||||
"value": "default"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "GET - Retrieve All Entity Classifications",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification"]
|
||||
},
|
||||
"description": "Retrieves all 6 entity classification categories for the given client. Returns null for categories that have not been submitted yet."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Startup India (DPIIT)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/startup",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "startup"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"dpiitRecognitionNumber\": \"DIPP12345\",\n \"recognitionDate\": \"15 January 2024\",\n \"certificateExpiryDate\": \"14 January 2034\",\n \"entityStage\": \"GROWTH\",\n \"sectorAsDpiit\": \"Information Technology\",\n \"nicIndustryCode\": \"62011\",\n \"taxExemption80iac\": true,\n \"sidbiFund\": false,\n \"selfCertification\": true,\n \"fastTrackPatent\": true,\n \"govtTenderExemption\": true,\n \"creditGuaranteeCgss\": false,\n \"pslLoanLimit\": 5000000.00,\n \"pslSubCategory\": \"StartupIndia-IT\",\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Startup India (DPIIT) entity classification. Captures DPIIT recognition number, tax exemptions under Section 80IAC, SIDBI fund eligibility, and PSL limits."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Mudra Borrower (PMMY)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/mudra",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "mudra"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"mudraCategory\": \"KISHORE\",\n \"mudraLoanAccountNo\": \"MUDRA2024MH00567\",\n \"lendingInstitutionName\": \"State Bank of India\",\n \"sanctionedAmount\": 350000.00,\n \"disbursementDate\": \"01 March 2024\",\n \"loanPurpose\": \"Working Capital\",\n \"mudraCardNumber\": \"MC20240567\",\n \"currentOutstanding\": 280000.00,\n \"repaymentStatus\": \"REGULAR\",\n \"npaFlag\": false,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Mudra Borrower (PMMY) entity classification. Supports SHISHU (up to Rs 50K), KISHORE (50K-5L), and TARUN (5L-10L) categories under Pradhan Mantri Mudra Yojana."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Export Oriented Unit (EOU)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/eou",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "eou"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"eouRegistrationNumber\": \"EOU/MH/2023/00456\",\n \"registeredDcZone\": \"SEEPZ-SEZ Mumbai\",\n \"bondLutNumber\": \"LUT/2024/MH/0089\",\n \"validityFrom\": \"01 April 2023\",\n \"validityTo\": \"31 March 2028\",\n \"gstinEou\": \"27AABCU9603R1ZM\",\n \"annualExportObligation\": 25000000.00,\n \"nfeTarget\": 5000000.00,\n \"softexFilingStatus\": \"FILED\",\n \"brcSubmissionStatus\": \"SUBMITTED\",\n \"adCategoryBankCode\": \"AD-I\",\n \"femaDeclarationDate\": \"15 April 2023\",\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Export Oriented Unit (EOU) entity classification. Captures EOU registration, Bond/LUT details, GSTIN, export obligations, NFE targets, and FEMA compliance."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Import/Export (IEC/DGFT)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/importexport",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "importexport"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"iecCode\": \"0305012345\",\n \"iecIssueDate\": \"10 June 2020\",\n \"iecStatus\": \"ACTIVE\",\n \"tradeType\": \"BOTH\",\n \"primaryHsCode\": \"8471\",\n \"keyTradingCountries\": \"USA, UAE, Singapore, Germany\",\n \"adCategoryBankCode\": \"AD-I\",\n \"ecgcPolicyNumber\": \"ECGC/WTP/2024/00123\",\n \"ecgcPolicyExpiry\": \"31 March 2025\",\n \"eefcAccountRequired\": true,\n \"pcfcFacilityRequired\": false,\n \"femaDeclarationObtained\": true,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Import/Export (IEC/DGFT) entity classification. Captures Importer Exporter Code, HS codes, ECGC insurance, EEFC/PCFC requirements, and FEMA compliance."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Priority Sector Lending (PSL)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/psl",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "psl"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"pslCategory\": \"MICRO_ENTERPRISE\",\n \"pslSubCategory\": \"Manufacturing\",\n \"pslLoanPurposeCode\": \"ME01\",\n \"pslEligibilityBasis\": \"Udyam Registration - Micro Enterprise with investment up to Rs 1 Cr\",\n \"fiddReportingCode\": \"FIDD-ME-MFG\",\n \"pslSanctionedLimit\": 10000000.00,\n \"msmeClassification\": \"MICRO\",\n \"udyamRegistrationNumber\": \"UDYAM-MH-01-0012345\",\n \"udyamRegistrationDate\": \"20 July 2023\",\n \"annualTurnover\": 45000000.00,\n \"investmentPlantMachinery\": 8500000.00,\n \"weakerSectionFlag\": true,\n \"scStBorrowerFlag\": false,\n \"womenEntrepreneurFlag\": true,\n \"minorityCommunityFlag\": false,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Priority Sector Lending (PSL) entity classification as per RBI Master Direction. Captures PSL category, MSME classification, Udyam registration, FIDD reporting codes, and weaker section flags."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save Non-Priority Sector (NPS)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/nps",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "nps"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"industrySector\": \"Real Estate\",\n \"nicCode\": \"68100\",\n \"industryDescription\": \"Real estate activities with owned or leased property - Commercial\",\n \"annualRevenueBand\": \"50CR_TO_100CR\",\n \"riskCategory\": \"MEDIUM\",\n \"customerRiskScore\": 55,\n \"amlRiskClassification\": \"Medium Risk - Enhanced Due Diligence Required\",\n \"pepFlag\": false,\n \"amlWatchlistMatch\": false,\n \"adverseMediaFlag\": false\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates Non-Priority Sector (NPS) entity classification. Captures industry details, NIC code, risk categorization, AML risk classification, PEP status, and adverse media flags."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "POST - Save MSME (Udyam Registration)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/msme",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "entityclassification", "msme"]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"enabled\": true,\n \"udyamRegistrationNumber\": \"UDYAM-MP-23-0012345\",\n \"registrationDate\": \"15 March 2023\",\n \"registrationStatus\": \"ACTIVE\",\n \"msmeClassification\": \"SMALL\",\n \"enterpriseType\": \"MANUFACTURING\",\n \"nicActivityCode\": \"13111\",\n \"annualTurnover\": 12.50,\n \"investmentPlantMachinery\": 3.75,\n \"financialYearAssessment\": \"2024-25\",\n \"socialCategoryOwner\": \"GENERAL\",\n \"genderPrimaryOwner\": \"MALE\",\n \"noOfEmployees\": 45,\n \"womenLedEnterprise\": false,\n \"divyangPwdOwned\": false,\n \"dateFormat\": \"dd MMMM yyyy\",\n \"locale\": \"en\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Creates or updates MSME (Udyam Registration) entity classification. Captures Udyam number, MSME classification (Micro/Small/Medium), enterprise type, financial thresholds, owner/promoter details, and special flags (women-led, PWD owned)."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "user-mgmt-collection",
|
||||
"name": "Fineract - User Management",
|
||||
"description": "Fineract User Management APIs - Login, Create, Update, Change Password, Delete Users",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "{{username}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "{{password}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://localhost:8443/fineract-provider/api"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "mifos"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "password"
|
||||
},
|
||||
{
|
||||
"key": "tenantId",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
"value": "1"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Authentication",
|
||||
"item": [
|
||||
{
|
||||
"name": "Login (Self-Service)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"mifos\",\n \"password\": \"password\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/self/authentication?returnClientList=true",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "self", "authentication"],
|
||||
"query": [
|
||||
{
|
||||
"key": "returnClientList",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Authenticates a user and returns auth token. Use this for self-service users."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Login (Basic Auth Test)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "1"]
|
||||
},
|
||||
"description": "Test basic auth by fetching user details. If credentials are correct, you'll get a 200 response."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Users",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get User by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get User Template",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/template",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "template"]
|
||||
},
|
||||
"description": "Returns template data (available offices, roles) needed to create a new user."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create User",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"newuser\",\n \"firstname\": \"New\",\n \"lastname\": \"User\",\n \"email\": \"newuser@example.com\",\n \"officeId\": 1,\n \"roles\": [1],\n \"sendPasswordToEmail\": false,\n \"password\": \"Password@123\",\n \"repeatPassword\": \"Password@123\",\n \"isSelfServiceUser\": false,\n \"passwordNeverExpires\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update User (Change Username)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"updatedUsername\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update User (Change Password via PUT)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"password\": \"NewPassword@123\",\n \"repeatPassword\": \"NewPassword@123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Change Password (Dedicated Endpoint)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"password\": \"NewPassword@123\",\n \"repeatPassword\": \"NewPassword@123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}/pwd",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}", "pwd"]
|
||||
},
|
||||
"description": "Dedicated password change endpoint. Must provide both password and repeatPassword."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update User (Full Update)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"updatedUser\",\n \"firstname\": \"Updated\",\n \"lastname\": \"User\",\n \"email\": \"updated@example.com\",\n \"officeId\": 1,\n \"roles\": [1],\n \"password\": \"NewPassword@123\",\n \"repeatPassword\": \"NewPassword@123\",\n \"passwordNeverExpires\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Delete User",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/users/{{userId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "users", "{{userId}}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Roles",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Roles",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Role by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Role Permissions",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1/permissions",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1", "permissions"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Role",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"New Role\",\n \"description\": \"A new custom role\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Role",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Updated Role\",\n \"description\": \"Updated description\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Role Permissions",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"permissions\": {\n \"READ_USER\": true,\n \"CREATE_USER\": true,\n \"UPDATE_USER\": true\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1/permissions",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1", "permissions"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enable Role",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1?command=enable",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1"],
|
||||
"query": [
|
||||
{
|
||||
"key": "command",
|
||||
"value": "enable"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Disable Role",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1?command=disable",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1"],
|
||||
"query": [
|
||||
{
|
||||
"key": "command",
|
||||
"value": "disable"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Delete Role",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/roles/1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "roles", "1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Password Preferences",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Password Preferences",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/passwordpreferences",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "passwordpreferences"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Password Preferences Template",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/passwordpreferences/template",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "passwordpreferences", "template"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Password Preferences",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"validationPolicyId\": 2\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/passwordpreferences",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "passwordpreferences"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Permissions",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Permissions",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/permissions",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "permissions"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Permissions (Maker-Checker)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"permissions\": {\n \"CREATE_USER_MAKER\": true,\n \"CREATE_USER_CHECKER\": true\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/v1/permissions",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["v1", "permissions"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
# Savings Product Payload Implementation Guide
|
||||
|
||||
## Overview
|
||||
Your UI has been updated to send a comprehensive new payload with additional fields for the `/savingsproducts` POST endpoint. This document outlines **all changes required** in the backend to support these new fields.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 New Fields in Your Payload
|
||||
|
||||
### **Simple Fields (Direct Columns in m_savings_product table)**
|
||||
| Field | Type | Example | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| accountType | String | "Small Saving Account" | Type of savings account |
|
||||
| customerSegment | String | "Individual" | Customer classification |
|
||||
| productCode | String | "SB-IND-001" | Product identifier code |
|
||||
| effectiveDate | Date | "10 April 2026" | When the product becomes effective |
|
||||
| effectiveTo | Date | "10 April 2026" | Validity end date |
|
||||
| endDate | Date | "10 April 2026" | Product end date |
|
||||
| introducerRequired | Boolean | true | If introducer/referrer is mandatory |
|
||||
| shareLinkingEnabled | Boolean | true | If share account linking allowed |
|
||||
| nominationFacility | Boolean | true | If nomination facility available |
|
||||
| dormancyAlert | Boolean | true | If dormancy alerts enabled |
|
||||
| interestCalculateSelector | Integer | 2 | Interest calculation method selector |
|
||||
| averagePeriod | String | "Monthly" | Period for averaging balance |
|
||||
| autoPost | Boolean | true | Auto-post interest |
|
||||
| frequencyFixDay | Integer | 30 | Fixed day for frequency |
|
||||
| lastInterestDate | Date | "10 April 2026" | Last interest posting date |
|
||||
| enableLockinPeriod | Boolean | true | If lock-in period enforced |
|
||||
|
||||
### **Complex Objects (Require Separate Tables)**
|
||||
|
||||
#### 1️⃣ **accountFacilityConfig** (Single object with facility booleans)
|
||||
```json
|
||||
{
|
||||
"atmFacility": true,
|
||||
"atmWithdrawalFacility": true,
|
||||
"upiFacility": true,
|
||||
"posTransactionsFacility": true,
|
||||
"chequeBook": true,
|
||||
"impsFacility": true,
|
||||
"smsAlerts": true,
|
||||
"bbpsFacility": true,
|
||||
"insuranceCover": true,
|
||||
"bepgFacility": true,
|
||||
"neftFacility": true,
|
||||
"rtgsFacility": true,
|
||||
"interBankTotalCapFacility": true,
|
||||
"withdrawalFacility": true,
|
||||
"depositFacilityKyc": true,
|
||||
"virtualDebitCard": true
|
||||
}
|
||||
```
|
||||
→ **Table:** `m_savings_product_facility_config` (One-to-One relationship)
|
||||
|
||||
#### 2️⃣ **accountFacilityLimits** (Array of limit objects)
|
||||
```json
|
||||
[
|
||||
{"limitType": "ATM Withdrawal", "generalLimit": 40000, "minorAccountLimit": 5000, "smallSavingLimit": 10000},
|
||||
{"limitType": "POS Transactions", "generalLimit": 100000, "minorAccountLimit": 2000, "smallSavingLimit": 10000},
|
||||
...
|
||||
]
|
||||
```
|
||||
→ **Table:** `m_savings_product_facility_limits` (One-to-Many relationship)
|
||||
|
||||
#### 3️⃣ **depositLimitKyc** (Nested KYC limit rules)
|
||||
```json
|
||||
{
|
||||
"general": {"kycType": "Full KYC", "limit": "No Limit"},
|
||||
"minor": {"kycType": "Guardian KYC", "limit": 100000},
|
||||
"smallSaving": {"kycType": "Min KYC", "limit": 50000}
|
||||
}
|
||||
```
|
||||
→ **Table:** `m_savings_product_deposit_limit_kyc` (One-to-One relationship, or JSON column)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Database Changes Required
|
||||
|
||||
### **1. Alter m_savings_product table**
|
||||
```sql
|
||||
ALTER TABLE m_savings_product ADD COLUMN (
|
||||
account_type VARCHAR(50),
|
||||
customer_segment VARCHAR(50),
|
||||
product_code VARCHAR(20) UNIQUE,
|
||||
effective_date DATE,
|
||||
effective_to DATE,
|
||||
end_date DATE,
|
||||
introducer_required BOOLEAN DEFAULT FALSE,
|
||||
share_linking_enabled BOOLEAN DEFAULT FALSE,
|
||||
nomination_facility BOOLEAN DEFAULT FALSE,
|
||||
dormancy_alert BOOLEAN DEFAULT FALSE,
|
||||
interest_calculate_selector INT,
|
||||
average_period VARCHAR(20),
|
||||
auto_post BOOLEAN DEFAULT FALSE,
|
||||
frequency_fix_day INT,
|
||||
last_interest_date DATE,
|
||||
enable_lockin_period BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
```
|
||||
|
||||
### **2. Create m_savings_product_facility_config table** (One-to-One)
|
||||
```sql
|
||||
CREATE TABLE m_savings_product_facility_config (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
savings_product_id BIGINT NOT NULL UNIQUE,
|
||||
atm_facility BOOLEAN DEFAULT FALSE,
|
||||
atm_withdrawal_facility BOOLEAN DEFAULT FALSE,
|
||||
upi_facility BOOLEAN DEFAULT FALSE,
|
||||
pos_transactions_facility BOOLEAN DEFAULT FALSE,
|
||||
cheque_book BOOLEAN DEFAULT FALSE,
|
||||
imps_facility BOOLEAN DEFAULT FALSE,
|
||||
sms_alerts BOOLEAN DEFAULT FALSE,
|
||||
bbps_facility BOOLEAN DEFAULT FALSE,
|
||||
insurance_cover BOOLEAN DEFAULT FALSE,
|
||||
bepg_facility BOOLEAN DEFAULT FALSE,
|
||||
neft_facility BOOLEAN DEFAULT FALSE,
|
||||
rtgs_facility BOOLEAN DEFAULT FALSE,
|
||||
inter_bank_total_cap_facility BOOLEAN DEFAULT FALSE,
|
||||
withdrawal_facility BOOLEAN DEFAULT FALSE,
|
||||
deposit_facility_kyc BOOLEAN DEFAULT FALSE,
|
||||
virtual_debit_card BOOLEAN DEFAULT FALSE,
|
||||
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### **3. Create m_savings_product_facility_limits table** (One-to-Many)
|
||||
```sql
|
||||
CREATE TABLE m_savings_product_facility_limits (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
savings_product_id BIGINT NOT NULL,
|
||||
limit_type VARCHAR(100) NOT NULL,
|
||||
general_limit VARCHAR(50), -- Can be decimal or "N/A", "UNLIMITED"
|
||||
minor_account_limit VARCHAR(50),
|
||||
small_saving_limit VARCHAR(50),
|
||||
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_product_limit_type (savings_product_id, limit_type)
|
||||
);
|
||||
```
|
||||
|
||||
### **4. Create m_savings_product_deposit_limit_kyc table** (One-to-One)
|
||||
```sql
|
||||
CREATE TABLE m_savings_product_deposit_limit_kyc (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
savings_product_id BIGINT NOT NULL UNIQUE,
|
||||
general_kyc_type VARCHAR(50),
|
||||
general_limit VARCHAR(50), -- "No Limit" or numeric
|
||||
minor_kyc_type VARCHAR(50),
|
||||
minor_limit BIGINT,
|
||||
small_saving_kyc_type VARCHAR(50),
|
||||
small_saving_limit BIGINT,
|
||||
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Java Entity Classes to Create/Update
|
||||
|
||||
### **1. Update SavingsProduct Entity**
|
||||
Add these fields to `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java`:
|
||||
|
||||
```java
|
||||
@Column(name = "account_type", length = 50)
|
||||
private String accountType;
|
||||
|
||||
@Column(name = "customer_segment", length = 50)
|
||||
private String customerSegment;
|
||||
|
||||
@Column(name = "product_code", length = 20, unique = true)
|
||||
private String productCode;
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
@Column(name = "effective_to")
|
||||
private LocalDate effectiveTo;
|
||||
|
||||
@Column(name = "end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Column(name = "introducer_required")
|
||||
private boolean introducerRequired;
|
||||
|
||||
@Column(name = "share_linking_enabled")
|
||||
private boolean shareLinkingEnabled;
|
||||
|
||||
@Column(name = "nomination_facility")
|
||||
private boolean nominationFacility;
|
||||
|
||||
@Column(name = "dormancy_alert")
|
||||
private boolean dormancyAlert;
|
||||
|
||||
@Column(name = "interest_calculate_selector")
|
||||
private Integer interestCalculateSelector;
|
||||
|
||||
@Column(name = "average_period", length = 20)
|
||||
private String averagePeriod;
|
||||
|
||||
@Column(name = "auto_post")
|
||||
private boolean autoPost;
|
||||
|
||||
@Column(name = "frequency_fix_day")
|
||||
private Integer frequencyFixDay;
|
||||
|
||||
@Column(name = "last_interest_date")
|
||||
private LocalDate lastInterestDate;
|
||||
|
||||
@Column(name = "enable_lockin_period")
|
||||
private boolean enableLockinPeriod;
|
||||
|
||||
// One-to-One relationship
|
||||
@OneToOne(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true)
|
||||
private SavingsProductFacilityConfig facilityConfig;
|
||||
|
||||
// One-to-Many relationship
|
||||
@OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true)
|
||||
private Set<SavingsProductFacilityLimit> facilityLimits = new HashSet<>();
|
||||
|
||||
// One-to-One relationship
|
||||
@OneToOne(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true)
|
||||
private SavingsProductDepositLimitKyc depositLimitKyc;
|
||||
```
|
||||
|
||||
### **2. Create SavingsProductFacilityConfig Entity**
|
||||
`fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductFacilityConfig.java`:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "m_savings_product_facility_config")
|
||||
public class SavingsProductFacilityConfig extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "savings_product_id", nullable = false)
|
||||
private SavingsProduct savingsProduct;
|
||||
|
||||
@Column(name = "atm_facility")
|
||||
private Boolean atmFacility = false;
|
||||
|
||||
@Column(name = "atm_withdrawal_facility")
|
||||
private Boolean atmWithdrawalFacility = false;
|
||||
|
||||
@Column(name = "upi_facility")
|
||||
private Boolean upiFacility = false;
|
||||
|
||||
@Column(name = "pos_transactions_facility")
|
||||
private Boolean posTransactionsFacility = false;
|
||||
|
||||
@Column(name = "cheque_book")
|
||||
private Boolean chequeBook = false;
|
||||
|
||||
@Column(name = "imps_facility")
|
||||
private Boolean impsFacility = false;
|
||||
|
||||
@Column(name = "sms_alerts")
|
||||
private Boolean smsAlerts = false;
|
||||
|
||||
@Column(name = "bbps_facility")
|
||||
private Boolean bbpsFacility = false;
|
||||
|
||||
@Column(name = "insurance_cover")
|
||||
private Boolean insuranceCover = false;
|
||||
|
||||
@Column(name = "bepg_facility")
|
||||
private Boolean bepgFacility = false;
|
||||
|
||||
@Column(name = "neft_facility")
|
||||
private Boolean neftFacility = false;
|
||||
|
||||
@Column(name = "rtgs_facility")
|
||||
private Boolean rtgsFacility = false;
|
||||
|
||||
@Column(name = "inter_bank_total_cap_facility")
|
||||
private Boolean interBankTotalCapFacility = false;
|
||||
|
||||
@Column(name = "withdrawal_facility")
|
||||
private Boolean withdrawalFacility = false;
|
||||
|
||||
@Column(name = "deposit_facility_kyc")
|
||||
private Boolean depositFacilityKyc = false;
|
||||
|
||||
@Column(name = "virtual_debit_card")
|
||||
private Boolean virtualDebitCard = false;
|
||||
|
||||
// Getters and setters
|
||||
}
|
||||
```
|
||||
|
||||
### **3. Create SavingsProductFacilityLimit Entity**
|
||||
`fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductFacilityLimit.java`:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "m_savings_product_facility_limits")
|
||||
public class SavingsProductFacilityLimit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "savings_product_id", nullable = false)
|
||||
private SavingsProduct savingsProduct;
|
||||
|
||||
@Column(name = "limit_type", length = 100, nullable = false)
|
||||
private String limitType;
|
||||
|
||||
@Column(name = "general_limit", length = 50)
|
||||
private String generalLimit; // Can be "UNLIMITED", "N/A", or numeric value
|
||||
|
||||
@Column(name = "minor_account_limit", length = 50)
|
||||
private String minorAccountLimit;
|
||||
|
||||
@Column(name = "small_saving_limit", length = 50)
|
||||
private String smallSavingLimit;
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public static SavingsProductFacilityLimit create(
|
||||
SavingsProduct savingsProduct,
|
||||
String limitType,
|
||||
String generalLimit,
|
||||
String minorAccountLimit,
|
||||
String smallSavingLimit) {
|
||||
|
||||
SavingsProductFacilityLimit limit = new SavingsProductFacilityLimit();
|
||||
limit.savingsProduct = savingsProduct;
|
||||
limit.limitType = limitType;
|
||||
limit.generalLimit = generalLimit;
|
||||
limit.minorAccountLimit = minorAccountLimit;
|
||||
limit.smallSavingLimit = smallSavingLimit;
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Create SavingsProductDepositLimitKyc Entity**
|
||||
`fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductDepositLimitKyc.java`:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "m_savings_product_deposit_limit_kyc")
|
||||
public class SavingsProductDepositLimitKyc extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "savings_product_id", nullable = false)
|
||||
private SavingsProduct savingsProduct;
|
||||
|
||||
@Column(name = "general_kyc_type", length = 50)
|
||||
private String generalKycType;
|
||||
|
||||
@Column(name = "general_limit", length = 50)
|
||||
private String generalLimit; // "No Limit" or numeric
|
||||
|
||||
@Column(name = "minor_kyc_type", length = 50)
|
||||
private String minorKycType;
|
||||
|
||||
@Column(name = "minor_limit")
|
||||
private Long minorLimit;
|
||||
|
||||
@Column(name = "small_saving_kyc_type", length = 50)
|
||||
private String smallSavingKycType;
|
||||
|
||||
@Column(name = "small_saving_limit")
|
||||
private Long smallSavingLimit;
|
||||
|
||||
// Getters and setters
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 API Constants to Add
|
||||
|
||||
Update `fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java`:
|
||||
|
||||
```java
|
||||
// New Savings Product Fields
|
||||
public static final String accountTypeParamName = "accountType";
|
||||
public static final String customerSegmentParamName = "customerSegment";
|
||||
public static final String productCodeParamName = "productCode";
|
||||
public static final String effectiveDateParamName = "effectiveDate";
|
||||
public static final String effectiveToParamName = "effectiveTo";
|
||||
public static final String endDateParamName = "endDate";
|
||||
public static final String introducerRequiredParamName = "introducerRequired";
|
||||
public static final String shareLinkingEnabledParamName = "shareLinkingEnabled";
|
||||
public static final String nominationFacilityParamName = "nominationFacility";
|
||||
public static final String dormancyAlertParamName = "dormancyAlert";
|
||||
public static final String interestCalculateSelectorParamName = "interestCalculateSelector";
|
||||
public static final String averagePeriodParamName = "averagePeriod";
|
||||
public static final String autoPostParamName = "autoPost";
|
||||
public static final String frequencyFixDayParamName = "frequencyFixDay";
|
||||
public static final String lastInterestDateParamName = "lastInterestDate";
|
||||
public static final String enableLockinPeriodParamName = "enableLockinPeriod";
|
||||
|
||||
// Complex objects
|
||||
public static final String accountFacilityConfigParamName = "accountFacilityConfig";
|
||||
public static final String accountFacilityLimitsParamName = "accountFacilityLimits";
|
||||
public static final String depositLimitKycParamName = "depositLimitKyc";
|
||||
|
||||
// Facility config fields
|
||||
public static final String atmFacilityParamName = "atmFacility";
|
||||
public static final String atmWithdrawalFacilityParamName = "atmWithdrawalFacility";
|
||||
public static final String upiFacilityParamName = "upiFacility";
|
||||
// ... etc for all facility fields
|
||||
|
||||
// Limit fields
|
||||
public static final String limitTypeParamName = "limitType";
|
||||
public static final String generalLimitParamName = "generalLimit";
|
||||
public static final String minorAccountLimitParamName = "minorAccountLimit";
|
||||
public static final String smallSavingLimitParamName = "smallSavingLimit";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validator Updates
|
||||
|
||||
Update `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java`:
|
||||
|
||||
1. **Add to parameter whitelist:**
|
||||
```java
|
||||
private static final Set<String> SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS =
|
||||
new HashSet<>(Arrays.asList(
|
||||
// ... existing parameters ...
|
||||
"accountType",
|
||||
"customerSegment",
|
||||
"productCode",
|
||||
"effectiveDate",
|
||||
"effectiveTo",
|
||||
"endDate",
|
||||
"introducerRequired",
|
||||
"shareLinkingEnabled",
|
||||
"nominationFacility",
|
||||
"dormancyAlert",
|
||||
"interestCalculateSelector",
|
||||
"averagePeriod",
|
||||
"autoPost",
|
||||
"frequencyFixDay",
|
||||
"lastInterestDate",
|
||||
"enableLockinPeriod",
|
||||
"accountFacilityConfig",
|
||||
"accountFacilityLimits",
|
||||
"depositLimitKyc"
|
||||
));
|
||||
```
|
||||
|
||||
2. **Add validation logic in validateForCreate() and validateForUpdate():**
|
||||
```java
|
||||
if (command.stringValueOfParameterNamedAllowingNull("productCode") != null) {
|
||||
final String productCode = command.stringValueOfParameterNamed("productCode");
|
||||
if (StringUtils.isBlank(productCode)) {
|
||||
baseDataValidator.reset().parameter("productCode").value(productCode).notBlank();
|
||||
}
|
||||
}
|
||||
|
||||
if (command.hasParameter("effectiveDate")) {
|
||||
final LocalDate effectiveDate = command.localDateValueOfParameterNamed("effectiveDate");
|
||||
baseDataValidator.reset().parameter("effectiveDate").value(effectiveDate).notNull();
|
||||
}
|
||||
|
||||
// Similar for other date fields
|
||||
if (command.hasParameter("effectiveTo") && command.hasParameter("effectiveDate")) {
|
||||
final LocalDate effectiveDate = command.localDateValueOfParameterNamed("effectiveDate");
|
||||
final LocalDate effectiveTo = command.localDateValueOfParameterNamed("effectiveTo");
|
||||
if (effectiveTo.isBefore(effectiveDate)) {
|
||||
baseDataValidator.reset().parameter("effectiveTo").value(effectiveTo)
|
||||
.failWithCode("error.msg.effectiveto.cannot.be.before.effectivedate");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Assembler Updates
|
||||
|
||||
Update `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java`:
|
||||
|
||||
Add helper methods to extract and assemble nested objects:
|
||||
|
||||
```java
|
||||
public SavingsProductFacilityConfig assembleAccountFacilityConfig(final JsonCommand command, final SavingsProduct product) {
|
||||
final JsonObject jsonObject = command.parsedJson();
|
||||
|
||||
if (!jsonObject.has("accountFacilityConfig")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final JsonObject facilityConfigJson = jsonObject.getAsJsonObject("accountFacilityConfig");
|
||||
SavingsProductFacilityConfig config = new SavingsProductFacilityConfig();
|
||||
config.setSavingsProduct(product);
|
||||
config.setAtmFacility(facilityConfigJson.has("atmFacility") ?
|
||||
facilityConfigJson.getAsJsonPrimitive("atmFacility").getAsBoolean() : false);
|
||||
config.setAtmWithdrawalFacility(facilityConfigJson.has("atmWithdrawalFacility") ?
|
||||
facilityConfigJson.getAsJsonPrimitive("atmWithdrawalFacility").getAsBoolean() : false);
|
||||
// ... set other facilities ...
|
||||
return config;
|
||||
}
|
||||
|
||||
public Set<SavingsProductFacilityLimit> assembleAccountFacilityLimits(final JsonCommand command, final SavingsProduct product) {
|
||||
final JsonObject jsonObject = command.parsedJson();
|
||||
final Set<SavingsProductFacilityLimit> limits = new HashSet<>();
|
||||
|
||||
if (!jsonObject.has("accountFacilityLimits")) {
|
||||
return limits;
|
||||
}
|
||||
|
||||
final JsonArray limitsArray = jsonObject.getAsJsonArray("accountFacilityLimits");
|
||||
for (JsonElement element : limitsArray) {
|
||||
final JsonObject limitJson = element.getAsJsonObject();
|
||||
SavingsProductFacilityLimit limit = SavingsProductFacilityLimit.create(
|
||||
product,
|
||||
limitJson.has("limitType") ? limitJson.getAsJsonPrimitive("limitType").getAsString() : null,
|
||||
limitJson.has("generalLimit") ? limitJson.getAsJsonPrimitive("generalLimit").getAsString() : null,
|
||||
limitJson.has("minorAccountLimit") ? limitJson.getAsJsonPrimitive("minorAccountLimit").getAsString() : null,
|
||||
limitJson.has("smallSavingLimit") ? limitJson.getAsJsonPrimitive("smallSavingLimit").getAsString() : null
|
||||
);
|
||||
limits.add(limit);
|
||||
}
|
||||
return limits;
|
||||
}
|
||||
|
||||
public SavingsProductDepositLimitKyc assembleDepositLimitKyc(final JsonCommand command, final SavingsProduct product) {
|
||||
final JsonObject jsonObject = command.parsedJson();
|
||||
|
||||
if (!jsonObject.has("depositLimitKyc")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final JsonObject kycJson = jsonObject.getAsJsonObject("depositLimitKyc");
|
||||
SavingsProductDepositLimitKyc kyc = new SavingsProductDepositLimitKyc();
|
||||
kyc.setSavingsProduct(product);
|
||||
|
||||
if (kycJson.has("general")) {
|
||||
final JsonObject generalJson = kycJson.getAsJsonObject("general");
|
||||
kyc.setGeneralKycType(generalJson.has("kycType") ?
|
||||
generalJson.getAsJsonPrimitive("kycType").getAsString() : null);
|
||||
kyc.setGeneralLimit(generalJson.has("limit") ?
|
||||
generalJson.getAsJsonPrimitive("limit").getAsString() : null);
|
||||
}
|
||||
|
||||
// ... handle minor and smallSaving similarly ...
|
||||
|
||||
return kyc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 Repository Classes to Create
|
||||
|
||||
### **SavingsProductFacilityConfigRepository.java**
|
||||
```java
|
||||
@Repository
|
||||
public interface SavingsProductFacilityConfigRepository extends
|
||||
JpaRepository<SavingsProductFacilityConfig, Long> {
|
||||
SavingsProductFacilityConfig findBySavingsProductId(Long savingsProductId);
|
||||
}
|
||||
```
|
||||
|
||||
### **SavingsProductFacilityLimitRepository.java**
|
||||
```java
|
||||
@Repository
|
||||
public interface SavingsProductFacilityLimitRepository extends
|
||||
JpaRepository<SavingsProductFacilityLimit, Long> {
|
||||
List<SavingsProductFacilityLimit> findBySavingsProductId(Long savingsProductId);
|
||||
}
|
||||
```
|
||||
|
||||
### **SavingsProductDepositLimitKycRepository.java**
|
||||
```java
|
||||
@Repository
|
||||
public interface SavingsProductDepositLimitKycRepository extends
|
||||
JpaRepository<SavingsProductDepositLimitKyc, Long> {
|
||||
SavingsProductDepositLimitKyc findBySavingsProductId(Long savingsProductId);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Service Updates
|
||||
|
||||
Update `SavingsProductWritePlatformServiceImpl.create()` method:
|
||||
|
||||
```java
|
||||
public CommandProcessingResult create(final JsonCommand command) {
|
||||
try {
|
||||
this.savingsProductDataValidator.validateForCreate(command.json());
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
// Assemble new objects
|
||||
final SavingsProductFacilityConfig facilityConfig =
|
||||
this.savingsProductAssembler.assembleAccountFacilityConfig(command, product);
|
||||
final Set<SavingsProductFacilityLimit> facilityLimits =
|
||||
this.savingsProductAssembler.assembleAccountFacilityLimits(command, product);
|
||||
final SavingsProductDepositLimitKyc depositLimitKyc =
|
||||
this.savingsProductAssembler.assembleDepositLimitKyc(command, product);
|
||||
|
||||
if (facilityConfig != null) {
|
||||
product.setFacilityConfig(facilityConfig);
|
||||
facilityConfigRepository.saveAndFlush(facilityConfig);
|
||||
}
|
||||
|
||||
if (!facilityLimits.isEmpty()) {
|
||||
product.setFacilityLimits(facilityLimits);
|
||||
facilityLimitRepository.saveAll(facilityLimits);
|
||||
}
|
||||
|
||||
if (depositLimitKyc != null) {
|
||||
product.setDepositLimitKyc(depositLimitKyc);
|
||||
depositLimitKycRepository.saveAndFlush(depositLimitKyc);
|
||||
}
|
||||
|
||||
// Save main product
|
||||
final SavingsProduct productSaved = this.savingProductRepository.saveAndFlush(product);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withEntityId(productSaved.getId())
|
||||
.build();
|
||||
|
||||
} catch (final DataAccessException e) {
|
||||
// error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Read Service Updates
|
||||
|
||||
Update `SavingsProductReadPlatformService` to include new fields in queries and DTO.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary of Changes
|
||||
|
||||
| Component | Type | Files to Modify/Create |
|
||||
|-----------|------|----------------------|
|
||||
| Database | Migration | SQL scripts |
|
||||
| Entities | Create/Modify | 4 entity classes |
|
||||
| Constants | Modify | SavingsApiConstants.java |
|
||||
| Validator | Modify | SavingsProductDataValidator.java |
|
||||
| Assembler | Modify | SavingsProductAssembler.java |
|
||||
| Repositories | Create | 3 repository interfaces |
|
||||
| Services | Modify | SavingsProductWrite/ReadPlatformService |
|
||||
| DAO | Modify | SQL queries in read service |
|
||||
|
||||
---
|
||||
|
||||
## 📌 Implementation Priority
|
||||
|
||||
1. **Phase 1:** Create database tables
|
||||
2. **Phase 2:** Create entity classes
|
||||
3. **Phase 3:** Add API constants and validator rules
|
||||
4. **Phase 4:** Create repositories and update service layer
|
||||
5. **Phase 5:** Update read service and DTOs
|
||||
6. **Phase 6:** Test with your POST endpoint
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
- ✅ Fields like `otherGlMappings`, `isDormancyTrackingActive`, etc. **already exist** in the system
|
||||
- ✅ Ensure backward compatibility - make all new fields optional in the validator
|
||||
- ✅ Use cascade = CascadeType.ALL and orphanRemoval = true for nested entities
|
||||
- ✅ Add unique constraints where needed (e.g., productCode, limit combinations)
|
||||
- ✅ Consider adding indexes on frequently queried fields
|
||||
- ✅ Update DTO classes to include these new fields in API responses
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
Apache Fineract
|
||||
Copyright 2008-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software
|
||||
Foundation (http://www.apache.org/).
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
This product includes third party libraries and resource files from
|
||||
different providers. The details of the different packages and their
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
Apache Fineract
|
||||
Copyright 2008-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software
|
||||
Foundation (http://www.apache.org/).
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "op-fineract-collection",
|
||||
"name": "Fineract - OpenProject Integration",
|
||||
"description": "Postman collection for Fineract OpenProject integration API endpoints.\n\nBase URL: {{baseUrl}}/fineract-provider/api/v1/openproject\n\nAuthentication: Basic Auth (Fineract user credentials)",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "{{fineractUser}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "{{fineractPassword}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://localhost:8443",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "fineractUser",
|
||||
"value": "mifos",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "fineractPassword",
|
||||
"value": "password",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "tenantId",
|
||||
"value": "default",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Project Details",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/project",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "project"]
|
||||
},
|
||||
"description": "Returns the configured OpenProject project details (CBSNexora)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Work Packages (All)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?pageSize=25&offset=1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||
"query": [
|
||||
{
|
||||
"key": "pageSize",
|
||||
"value": "25"
|
||||
},
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Returns all work packages from the OpenProject project."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Work Packages (Open)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?status=open&pageSize=25&offset=1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||
"query": [
|
||||
{
|
||||
"key": "status",
|
||||
"value": "open"
|
||||
},
|
||||
{
|
||||
"key": "pageSize",
|
||||
"value": "25"
|
||||
},
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Returns only open work packages."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Work Packages (Closed)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?status=closed&pageSize=25&offset=1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||
"query": [
|
||||
{
|
||||
"key": "status",
|
||||
"value": "closed"
|
||||
},
|
||||
{
|
||||
"key": "pageSize",
|
||||
"value": "25"
|
||||
},
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Returns only closed work packages."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Status Summary",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/summary",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "summary"]
|
||||
},
|
||||
"description": "Returns a summary of all work packages grouped by status, type, and priority."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Work Package - Feature",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"subject\": \"Integrate OpenProject with CBS Dashboard\",\n \"description\": \"Add OpenProject work package tracking to the CBS admin dashboard so project managers can view task progress directly from the banking system.\",\n \"type\": \"Feature\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||
},
|
||||
"description": "Creates a new Feature work package in OpenProject.\n\nValid types: Task, Milestone, Summary task, Feature, Epic, User story, Bug, Incident, Support Ticket, Problem, Change Request, Enhancement"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Work Package - Task",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"subject\": \"Setup CI/CD pipeline for CBS deployment\",\n \"description\": \"Configure automated build and deployment pipeline for the Core Banking System.\",\n \"type\": \"Task\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||
},
|
||||
"description": "Creates a new Task work package in OpenProject."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Work Package - Bug",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "fineract-platform-tenantid",
|
||||
"value": "{{tenantId}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"subject\": \"Fix loan repayment calculation rounding error\",\n \"description\": \"Loan repayment amounts show minor rounding differences when calculated with different interest methods.\",\n \"type\": \"Bug\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||
},
|
||||
"description": "Creates a new Bug work package in OpenProject."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "rbi-feeds-collection",
|
||||
"name": "RBI Feeds API - Fineract CBS",
|
||||
"description": "RBI RSS Feed endpoints for fetching and retrieving RBI circulars, notifications, press releases etc.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "mifos",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "password",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://localhost:8443/fineract-provider/api/v1",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "tenantId",
|
||||
"value": "default",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. Trigger Fetch (Run First)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds/fetch",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds", "fetch"]
|
||||
},
|
||||
"description": "Fetches latest RSS feeds from rbi.org.in (5 categories: PRESS_RELEASE, NOTIFICATION, SPEECH, TENDER, PUBLICATION) and stores new items in the database. Run this FIRST to populate data."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "2. Get All Feeds",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"]
|
||||
},
|
||||
"description": "Retrieve all RBI feed items sorted by publication date (newest first)."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "3. Get Feeds by Category - PRESS_RELEASE",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds?category=PRESS_RELEASE",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "PRESS_RELEASE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Filter feeds by PRESS_RELEASE category."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "4. Get Feeds by Category - NOTIFICATION",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds?category=NOTIFICATION",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "NOTIFICATION"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Filter feeds by NOTIFICATION category."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "5. Get Feeds by Category - SPEECH",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds?category=SPEECH",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "SPEECH"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Filter feeds by SPEECH category."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "6. Get Feeds by Category - TENDER",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds?category=TENDER",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "TENDER"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Filter feeds by TENDER category."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "7. Get Feeds by Category - PUBLICATION",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds?category=PUBLICATION",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "PUBLICATION"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Filter feeds by PUBLICATION category."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "8. Get Single Feed by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/rbi-feeds/1",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["rbi-feeds", "1"]
|
||||
},
|
||||
"description": "Retrieve a single RBI feed item by its ID. Change the ID in the URL as needed."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,80 +1,71 @@
|
||||
Apache Fineract: A Platform for Microfinance
|
||||
============
|
||||
# Apache Fineract
|
||||
|
||||
<!-- TODO Reactivate when there is a working CI-CD instance: [](https://validator.swagger.io/validator/debug?url=https://sandbox.mifos.community/fineract-provider/swagger-ui/fineract.yaml) -->
|
||||
[](https://github.com/apache/fineract/actions/workflows/build-mariadb.yml)
|
||||
[](https://hub.docker.com/r/apache/fineract)
|
||||
[](https://github.com/apache/fineract/actions/workflows/publish-dockerhub.yml)
|
||||
[](https://sonarcloud.io/summary/new_code?id=apache_fineract)
|
||||
[](https://www.digitalpublicgoods.net/r/apache-fineract "Apache Fineract is a verified Digital Public Good under the Digital Public Goods Alliance.")
|
||||
|
||||
</b>
|
||||
Apache Fineract is an open-source core banking platform providing a
|
||||
flexible, extensible foundation for a wide range of financial services. By
|
||||
making robust banking technology openly available, it lowers barriers for
|
||||
institutions and innovators to reach underserved and unbanked populations.
|
||||
|
||||
Fineract is a mature platform with open APIs that provides a reliable, robust, and affordable core banking solution for financial institutions offering services to the world’s 3 billion underbanked and unbanked.
|
||||
Have a look at the [documentation](https://fineract.apache.org/docs/current), the [wiki](https://cwiki.apache.org/confluence/display/FINERACT) or at the [FAQ](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=91554327), if this README does not answer what you are looking for.
|
||||
|
||||
[Have a look at the FAQ on our Wiki at apache.org](https://cwiki.apache.org/confluence/display/FINERACT/FAQ) if this README does not answer what you are looking for. [Visit our JIRA Dashboard](https://issues.apache.org/jira/secure/Dashboard.jspa?selectPageId=12335824) to find issues to work on, see what others are working on, or open new issues.
|
||||
|
||||
COMMUNITY
|
||||
=========
|
||||
|
||||
If you are interested in contributing to this project, but perhaps don't quite know how and where to get started, please [join our developer mailing list](http://fineract.apache.org/#contribute), listen into our conversations, chime into threads, and just send us a "Hello!" introduction email; we're a friendly bunch, and look forward to hearing from you.
|
||||
If you are interested in contributing to this project, but perhaps don't quite know how and where to get started, please [join our developer mailing list](http://fineract.apache.org/#contribute), listen into our conversations, chime into threads, or just send us a "Hello!" introduction email; we're a friendly bunch, and look forward to hearing from you. A more informal alternative is the [Fineract Slack channel](https://app.slack.com/client/T0F5GHE8Y/C028634A61L) (thank you, Mifos, for supporting the Slack channel!).
|
||||
|
||||
For the developer wiki, see [Contributor's Zone](https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone). Maybe [these how-to articles](https://cwiki.apache.org/confluence/display/FINERACT/How-to+articles) help you to get started.
|
||||
|
||||
In any case visit [our JIRA Dashboard](https://issues.apache.org/jira/secure/Dashboard.jspa?selectPageId=12335824) to find issues to work on, see what others are doing, or open new issues.
|
||||
|
||||
In the moment you get started writing code, please consult our [CONTRIBUTING](CONTRIBUTING.md) guidelines, where you will find more information on subjects like coding style, testing and pull requests.
|
||||
|
||||
|
||||
REQUIREMENTS
|
||||
============
|
||||
* min. 16GB RAM and 8 core CPU
|
||||
* `MariaDB >= 11.5.2` or `PostgreSQL >= 17.0`
|
||||
* `Java >= 21` (Azul Zulu JVM is tested by our CI on GitHub Actions)
|
||||
* MariaDB `11.5.2`
|
||||
|
||||
You can run the required version of the database server in a container, instead of having to install it, like this:
|
||||
Tomcat (min. v10) is only required, if you wish to deploy the Fineract WAR to a separate external servlet container. You do not need to install Tomcat to run Fineract. We recommend the use of the self-contained JAR, which transparently embeds a servlet container using Spring Boot.
|
||||
|
||||
docker run --name mariadb-11.5 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:11.5.2
|
||||
|
||||
and stop and destroy it like this:
|
||||
SECURITY
|
||||
============
|
||||
For a list of known vulnerabilities, see [Apache Fineract Security Reports](https://fineract.apache.org/security.html).
|
||||
|
||||
docker rm -f mariadb-11.5
|
||||
If you believe you have found a new vulnerability, [let us know privately](https://fineract.apache.org/#contribute).
|
||||
|
||||
<br>Beware that this database container database keeps its state inside the container and not on the host filesystem. It is lost when you destroy (rm) this container. This is typically fine for development. See [Caveats: Where to Store Data on the database container documentation](https://hub.docker.com/_/mariadb) re. how to make it persistent instead of ephemeral.<br>
|
||||
For details about security during development and deployment, see the documentation [here](https://fineract.apache.org/docs/current/#_security).
|
||||
|
||||
Tomcat v10 is only required if you wish to deploy the Fineract WAR to a separate external servlet container. Note that you do not require to install Tomcat to develop Fineract, or to run it in production if you use the self-contained JAR, which transparently embeds a servlet container using Spring Boot. (Until FINERACT-730, Tomcat 7/8 were also supported, but now Tomcat 10 is required.)
|
||||
|
||||
<br>IMPORTANT: If you use MySQL or MariaDB
|
||||
INSTRUCTIONS
|
||||
============
|
||||
|
||||
Recently (after release `1.7.0`), we introduced improved date time handling in Fineract. Date time is from now on stored in UTC and we are enforcing UTC timezone even on the JDBC driver, e. g. for MySQL:
|
||||
The following how-to's assume you have Java installed, you cloned the repository (or downloaded and extracted a [specific version](https://github.com/apache/fineract/releases)) and you have a [database server](#database-and-tables) (MariaDB or PostgreSQL) running.
|
||||
|
||||
```
|
||||
serverTimezone=UTC&useLegacyDatetimeCode=false&sessionVariables=time_zone=‘-00:00’
|
||||
How to run for local development
|
||||
---
|
||||
|
||||
Run the following commands in this order:
|
||||
```bash
|
||||
./gradlew createDB -PdbName=fineract_tenants
|
||||
./gradlew createDB -PdbName=fineract_default
|
||||
./gradlew devRun
|
||||
```
|
||||
|
||||
__DO__: If you do use MySQL as your Fineract database then the following configuration is highly recommended:
|
||||
|
||||
* Run the application in UTC (the default command line in our Docker image has the necessary parameters already set)
|
||||
* Run the MySQL database server in UTC (if you use managed services like AWS RDS then this should be the default anyway, but it would be good to double-check)
|
||||
|
||||
__DON'T__: In case the Fineract instance and the MySQL server are __not__ running in UTC then the following could happen:
|
||||
|
||||
* MySQL is saving date time values differently from PostgreSQL
|
||||
* Example scenario: if the Fineract instance runs in timezone: GMT+2, and the local date time is 2022-08-11 17:15 ...
|
||||
* ... then __PostgreSQL saves__ the LocalDateTime as is: __2022-08-11 17:15__
|
||||
* ... and __MySQL saves__ the LocalDateTime in UTC: __2022-08-11 15:15__
|
||||
* ... but when we __read__ the date time from PostgreSQL __or__ from MySQL, then both systems give us the same values: __2022-08-11 17:15 GMT+2__
|
||||
|
||||
If a previously used Fineract instance didn't run in UTC (backward compatibility), then all prior dates will be read wrongly by MySQL/MariaDB. This can cause issues when you run the database migration scripts.
|
||||
|
||||
__RECOMMENDATION__: you need to shift all dates in your database by the timezone offset that your Fineract instance used.
|
||||
|
||||
<br>INSTRUCTIONS: How to run for local development
|
||||
============
|
||||
|
||||
Run the following commands:
|
||||
1. `./gradlew createDB -PdbName=fineract_tenants`
|
||||
1. `./gradlew createDB -PdbName=fineract_default`
|
||||
1. `./gradlew devRun`
|
||||
|
||||
Fineract is now running, and will be listening for API requests on port 8443 by default.
|
||||
This creates two databases and builds and runs Fineract, which will be listening for API requests on port 8443 (by default) now.
|
||||
|
||||
Confirm Fineract is ready with, for example:
|
||||
|
||||
```bash
|
||||
curl --insecure https://localhost:8443/fineract-provider/actuator/health`
|
||||
curl --insecure https://localhost:8443/fineract-provider/actuator/health
|
||||
```
|
||||
|
||||
To test authenticated endpoints, include credentials in your request:
|
||||
@@ -87,233 +78,102 @@ curl --location \
|
||||
--header 'Authorization: Basic bWlmb3M6cGFzc3dvcmQ='
|
||||
```
|
||||
|
||||
<br>INSTRUCTIONS: How to build the JAR file
|
||||
============
|
||||
1. Clone the repository or download and extract the archive file to your local directory.
|
||||
2. Run `./gradlew clean bootJar` to build a modern cloud native fully self contained JAR file which will be created at `fineract-provider/build/libs` directory.
|
||||
3. As we are not allowed to include a JDBC driver in the built JAR, download a JDBC driver of your choice. For example: `wget https://dlm.mariadb.com/4174416/Connectors/java/connector-java-3.5.2/mariadb-java-client-3.5.2.jar`
|
||||
4. Start the jar and pass the directory where you have downloaded the JDBC driver as loader.path, for example: `java -Dloader.path=. -jar fineract-provider/build/libs/fineract-provider.jar` (does not require external Tomcat)
|
||||
|
||||
The tenants database connection details are configured [via environment variables (as with Docker container)](#instructions-to-run-using-docker-and-docker-compose), e.g. like this:
|
||||
|
||||
export FINERACT_HIKARI_PASSWORD=verysecret
|
||||
...
|
||||
java -jar fineract-provider.jar
|
||||
How to run for production
|
||||
---
|
||||
Running Fineract to try it out is relatively easy. If you intend to use it in a production environment, be aware that a proper deployment can be complex, costly, and time-consuming. Considerations include: Security, privacy, compliance, performance, service availability, backups, and more. The Fineract project does not provide a comprehensive guide for deploying Fineract in production. You might need skills in enterprise Java applications and more. Alternatively, you could pay a vendor for Fineract deployment and maintenance. You will find tips and tricks for deploying and securing Fineract in our official documentation and in the community-maintained wiki.
|
||||
|
||||
|
||||
<br>SECURITY
|
||||
============
|
||||
If you believe you have found a security vulnerability, [let us know privately](https://fineract.apache.org/#contribute).
|
||||
How to build the JAR file
|
||||
---
|
||||
Build a modern, cloud native, fully self contained JAR file:
|
||||
```bash
|
||||
./gradlew clean bootJar
|
||||
```
|
||||
The JAR will be created in the `fineract-provider/build/libs` directory.
|
||||
As we are not allowed to include a JDBC driver in the built JAR, download a JDBC driver of your choice. For example:
|
||||
```bash
|
||||
wget https://dlm.mariadb.com/4174416/Connectors/java/connector-java-3.5.2/mariadb-java-client-3.5.2.jar
|
||||
```
|
||||
Start the JAR and specify the directory containing the JDBC driver using the loader.path option, for example:
|
||||
```bash
|
||||
java -Dloader.path=. -jar fineract-provider/build/libs/fineract-provider.jar
|
||||
```
|
||||
This does not require an external Tomcat.
|
||||
|
||||
For details about security during development and deployment, see <https://fineract.apache.org/docs/current/#_security>.
|
||||
The tenants database connection details are configured [via environment variables (as with Docker container)](#instructions-to-run-using-docker-or-podman), e.g. like this:
|
||||
```bash
|
||||
export FINERACT_HIKARI_PASSWORD=verysecret
|
||||
...
|
||||
java -jar fineract-provider.jar
|
||||
```
|
||||
|
||||
|
||||
<br>INSTRUCTIONS: How to build a WAR file
|
||||
============
|
||||
1. Clone the repository or download and extract the archive file to your local directory.
|
||||
2. Run `./gradlew :fineract-war:clean :fineract-war:war` to build a traditional WAR file which will be created at `fineract-war/build/libs` directory.
|
||||
3. Deploy this WAR to your Tomcat v10 Servlet Container.
|
||||
How to build the WAR file
|
||||
---
|
||||
Build a traditional WAR file:
|
||||
```bash
|
||||
./gradlew :fineract-war:clean :fineract-war:war
|
||||
```
|
||||
The WAR will be created in the `fineract-war/build/libs` directory. Afterwards deploy the WAR to your Tomcat Servlet Container.
|
||||
|
||||
We recommend using the JAR instead of the WAR file deployment, because it's much easier.
|
||||
|
||||
Note that with the 1.4 release the tenants database pool configuration changed from Tomcat DBCP in XML to an embedded Hikari, configured by environment variables, see above.
|
||||
|
||||
|
||||
INSTRUCTIONS: How to run tests
|
||||
============
|
||||
|
||||
Unit tests
|
||||
----------
|
||||
|
||||
Here's how to run the set of relatviely fast and indepedent Fineract tests:
|
||||
|
||||
```bash
|
||||
./gradlew test -x :twofactor-tests:test -x :oauth2-tests:test -x :integration-tests:test
|
||||
```
|
||||
|
||||
This runs nearly 1,000 tests and completes in a few minutes on decent hardware.
|
||||
They shouldn't need any special servers/services running.
|
||||
|
||||
Integration tests
|
||||
-----------------
|
||||
|
||||
Running tests with external dependencies yourself is a multi-step process with many moving parts.
|
||||
Sometimes there are arbitrary failures and the prerequisite setup can be daunting.
|
||||
A full local integration test run (on a developer workstation) covering every possible test using every external service and every supported relational database engine could take an entire day, and that's assuming everything is properly configured and runs as expected.
|
||||
|
||||
Right now we depend on GitHub to know if "the build" is passing (it's actually multiple builds).
|
||||
The authoritative source of truth for what commands/services/tests to run, how, and when are the files in `.github/workflows/`.
|
||||
Output from runs based on those configuration files appears at <https://github.com/apache/fineract/actions>.
|
||||
|
||||
Incorrect default Java-related executables may cause test failures.
|
||||
To fix this on Debian and Ubuntu systems, run the following:
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/usr/lib/jvm/zulu21
|
||||
sudo update-alternatives --set java $JAVA_HOME/bin/java
|
||||
sudo update-alternatives --set javac $JAVA_HOME/bin/javac
|
||||
sudo update-alternatives --set javadoc $JAVA_HOME/bin/javadoc
|
||||
```
|
||||
|
||||
This would correct, for example, a [class file verson error](https://en.wikipedia.org/wiki/Java_class_file#General_layout).
|
||||
You might see something like this if a Java 11 executable (class file format version 56) was the system default, but the integration tests were using Java 21 (class file format version 65):
|
||||
|
||||
```
|
||||
UnsupportedClassVersionError: com.example.package/ClassName has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 55.0
|
||||
```
|
||||
|
||||
The GitHub builds are run in [short-lived virtual machines](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners), so locally reproducing the same may require additional effort, such as these extra clean-up procedures:
|
||||
|
||||
```bash
|
||||
# Might fix `error: cannot find symbol` or other intermittent failures.
|
||||
# `doc` here is a placeholder for any task(s) you are trying to run.
|
||||
# 💚 This is generally very safe to run between builds.
|
||||
./gradlew --refresh-dependencies doc
|
||||
|
||||
# Destroy anything untracked by git.
|
||||
# ⚠️ This may delete something important, e.g. a finely-tuned IDE configuration.
|
||||
git clean --force -dx
|
||||
|
||||
# Destroy various caches and configs.
|
||||
# ⚠️ This may delete gibibytes of cached data, making the next build very slow.
|
||||
rm -rf ~/.gradle ~/.m2 /tmp/cargo*
|
||||
|
||||
# Destroy any Java containers left running.
|
||||
# 💚 This is generally very safe to run between builds.
|
||||
ps auxwww | grep [c]argo | awk '{ print $2 }' | xargs -r kill
|
||||
```
|
||||
|
||||
Integration test runs such as `./gradlew --no-daemon --console=plain test -x :twofactor-tests:test -x :oauth2-test:test :fineract-e2e-tests-runner:test -PdbType=postgresql` in `.github/workflows/build-postgresql.yml` often take an hour or longer to complete.
|
||||
If you notice the `:integration-tests:test` task taking significantly less time, say, one minute, gradle may be skipping it.
|
||||
Look for something like this in the test output:
|
||||
|
||||
```
|
||||
> Task :integration-tests:test UP-TO-DATE 👀
|
||||
Custom actions are attached to task ':integration-tests:test'.
|
||||
Build cache key for task ':integration-tests:test' is 6aeeec3f58bf9703d4c100fbaa657f5c
|
||||
Skipping task ':integration-tests:test' as it is up-to-date.
|
||||
Resolve mutations for :integration-tests:cargoStopLocal (Thread[Execution worker Thread 11,5,main]) started.
|
||||
:integration-tests:cargoStopLocal (Thread[Execution worker Thread 11,5,main]) started.
|
||||
```
|
||||
|
||||
(This is with the `--info` gradle argument with eyeballs added for emphasis)
|
||||
The `--rerun-tasks` gradle argument may help, or you can try destroying `~/.gradle` and other clean-up procedures as indicated above then re-running tests.
|
||||
This is useful for repeated test runs (say, for timing) when gradle would otherwise assume a task is "up-to-date" and not re-run it.
|
||||
|
||||
Testing within IDEs
|
||||
-----------------
|
||||
|
||||
See the next section for testing in Eclipse.
|
||||
|
||||
See <https://fineract-academy.com> for testing in IntelliJ.
|
||||
|
||||
INSTRUCTIONS: How to run and debug in Eclipse IDE
|
||||
============
|
||||
|
||||
It is possible to run Fineract in Eclipse IDE and also to debug Fineract using Eclipse's debugging facilities.
|
||||
To do this, you need to create the Eclipse project files and import the project into an Eclipse workspace:
|
||||
|
||||
1. Create Eclipse project files into the Fineract project by running `./gradlew cleanEclipse eclipse`
|
||||
2. Import the fineract-provider project into your Eclipse workspace (File->Import->General->Existing Projects into Workspace, choose root directory fineract/fineract-provider)
|
||||
3. Do a clean build of the project in Eclipse (Project->Clean...)
|
||||
3. Run / debug Fineract by right clicking on org.apache.fineract.ServerApplication class and choosing Run As / Debug As -> Java Application. All normal Eclipse debugging features (breakpoints, watchpoints etc) should work as expected.
|
||||
|
||||
If you change the project settings (dependencies etc) in Gradle, you should redo step 1 and refresh the project in Eclipse.
|
||||
|
||||
You can also use Eclipse Junit support to run tests in Eclipse (Run As->Junit Test)
|
||||
|
||||
Finally, modifying source code in Eclipse automatically triggers hot code replace to a running instance, allowing you to immediately test your changes
|
||||
|
||||
|
||||
INSTRUCTIONS: How to run using Docker and docker-compose
|
||||
===================================================
|
||||
How to run using Docker or Podman
|
||||
---
|
||||
|
||||
It is possible to do a 'one-touch' installation of Fineract using containers (AKA "Docker").
|
||||
Fineract now packs the mifos community-app web UI in it's docker deploy.
|
||||
You can now run and test fineract with a GUI directly from the combined docker builds.
|
||||
This includes the database running in a container.
|
||||
This includes the database running in the container.
|
||||
|
||||
As Prerequisites, you must have `docker` and `docker-compose` installed on your machine; see
|
||||
[Docker Install](https://docs.docker.com/install/) and
|
||||
[Docker Compose Install](https://docs.docker.com/compose/install/).
|
||||
As prerequisites, you must have `docker` and `docker-compose` installed on your machine; see
|
||||
[Docker Install](https://docs.docker.com/install/) and [Docker Compose Install](https://docs.docker.com/compose/install/).
|
||||
|
||||
Alternatively, you can also use [Podman](https://github.com/containers/libpod)
|
||||
(e.g. via `dnf install podman-docker`), and [Podman Compose](https://github.com/containers/podman-compose/)
|
||||
(e.g. via `pip3 install podman-compose`) instead of Docker.
|
||||
|
||||
Now to run a new Fineract instance you can simply:
|
||||
To run a new Fineract instance on Linux you can simply:
|
||||
```bash
|
||||
git clone https://github.com/apache/fineract.git
|
||||
cd fineract
|
||||
./gradlew :fineract-provider:jibDockerBuild -x test
|
||||
```
|
||||
On Windows, do this instead:
|
||||
```cmd
|
||||
git clone https://github.com/apache/fineract.git --config core.autocrlf=input
|
||||
cd fineract
|
||||
gradlew :fineract-provider:jibDockerBuild -x test
|
||||
```
|
||||
Install the Loki log driver and start:
|
||||
```bash
|
||||
docker plugin install grafana/loki-docker-driver:latest \
|
||||
--alias loki --grant-all-permissions
|
||||
docker compose -f docker-compose-development.yml up -d
|
||||
```
|
||||
The Fineract (back-end) should be running at https://localhost:8443/fineract-provider/ now.
|
||||
Wait for https://localhost:8443/fineract-provider/actuator/health to return `{"status":"UP"}`.
|
||||
You must go to https://localhost:8443 and remember to accept the self-signed SSL certificate of the API once in your browser.
|
||||
|
||||
1. `git clone https://github.com/apache/fineract.git ; cd fineract`
|
||||
1. for windows, use `git clone https://github.com/apache/fineract.git --config core.autocrlf=input ; cd fineract`
|
||||
1. `./gradlew :fineract-provider:jibDockerBuild -x test`
|
||||
1. install the Loki log driver with `docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions`
|
||||
1. `docker compose -f docker-compose-development.yml up -d`
|
||||
1. fineract (back-end) is running at https://localhost:8443/fineract-provider/
|
||||
1. wait for https://localhost:8443/fineract-provider/actuator/health to return `{"status":"UP"}`
|
||||
1. you must go to https://localhost:8443 and remember to accept the self-signed SSL certificate of the API once in your browser, otherwise you get a message that is rather misleading from the UI.
|
||||
1. community-app (UI) is running at http://localhost:9090/?baseApiUrl=https://localhost:8443/fineract-provider&tenantIdentifier=default
|
||||
1. login using default _username_ `mifos` and _password_ `password`
|
||||
|
||||
https://hub.docker.com/r/apache/fineract has a pre-built container image of this project, built continuously.
|
||||
[Docker Hub](https://hub.docker.com/r/apache/fineract) has a pre-built container image of this project, built continuously.
|
||||
|
||||
You must specify the MySQL tenants database JDBC URL by passing it to the `fineract` container via environment
|
||||
variables; please consult the [`docker-compose.yml`](docker-compose.yml) for exact details how to specify those.
|
||||
_(Note that in previous versions, the `mysqlserver` environment variable used at `docker build` time instead of at
|
||||
`docker run` time did something similar; this has changed in [FINERACT-773](https://issues.apache.org/jira/browse/FINERACT-773)),
|
||||
and the `mysqlserver` environment variable is now no longer supported.)_
|
||||
|
||||
The logfiles and the Java Flight Recorder output are available in `PROJECT_ROOT/build/fineract/logs`. If you use IntelliJ then you can double-click on the `.jfr` file and open it with the IDE. You can also download Azul Mission Control from here https://www.azul.com/products/components/azul-mission-control/ to analyze the Java Flight Recorder file.
|
||||
The logfiles and the Java Flight Recorder output are available in `PROJECT_ROOT/build/fineract/logs`. If you use IntelliJ then you can double-click on the `.jfr` file and open it with the IDE. You can also download [Azul Mission Control](https://www.azul.com/products/components/azul-mission-control/) to analyze the Java Flight Recorder file.
|
||||
|
||||
NOTE: If you have issues with the file permissions and Docker Compose then you might need to change the variable values for `FINERACT_USER` and `FINERACT_GROUP` in `PROJECT_ROOT/config/docker/env/fineract-common.env`. You can find out what values you need to put there with the following commands:
|
||||
|
||||
```
|
||||
id -u ${USER}
|
||||
id -u ${GROUP}
|
||||
```
|
||||
|
||||
Please make sure that you are not checking in your changed values. The defaults should normally work for most people.
|
||||
|
||||
INSTRUCTIONS: How to build documentation
|
||||
===================================================
|
||||
|
||||
Run the following command:
|
||||
|
||||
```bash
|
||||
./gradlew doc
|
||||
id -u ${USER}
|
||||
id -g ${GROUP}
|
||||
```
|
||||
|
||||
Some dependencies are required (e.g. Ghostscript, Graphviz), see `.github/workflows/build-documentation.yml` for hints.
|
||||
|
||||
Additionally, IDEs such as IntelliJ are useful for editing the AsciiDoc source files while providing a live rendered preview.
|
||||
|
||||
HTML rendered from the AsciiDoc source files is also available online at <https://fineract.apache.org/docs/current/>.
|
||||
|
||||
A release version is derived from source control. The version will include `-SNAPSHOT` unless the current branch looks like a release or release maintenance branch. See `gitVersioning` settings in `build.gradle` for details.
|
||||
|
||||
Connection pool configuration
|
||||
=============================
|
||||
|
||||
Please check `application.properties` to see which connection pool settings can be tweaked. The associated environment variables are prefixed with `FINERACT_HIKARI_*`. You can find more information about specific connection pool settings (Hikari) at https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby
|
||||
|
||||
NOTE: we'll keep backwards compatibility until one of the next releases to ensure that things are working as expected. Environment variables prefixed `fineract_tenants_*` can still be used to configure the database connection, but we strongly encourage using `FINERACT_HIKARI_*` with more options.
|
||||
|
||||
<br>SSL CONFIGURATION
|
||||
=================
|
||||
|
||||
See [the HTTPS related doc](https://fineract.apache.org/docs/current/#_https).
|
||||
Please make sure that you are not checking in your changed values. The defaults should work for most people.
|
||||
|
||||
|
||||
<br>TOMCAT CONFIGURATION
|
||||
====================
|
||||
How to run on Kubernetes
|
||||
---
|
||||
|
||||
Please refer to the `application.properties` and the official Spring Boot documentation (https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html) on how to do performance tuning for Tomcat. Note: you can set now the acceptable form POST size (default is 2MB) via environment variable `FINERACT_SERVER_TOMCAT_MAX_HTTP_FORM_POST_SIZE`.
|
||||
|
||||
|
||||
<br>INSTRUCTIONS: How to run on Kubernetes
|
||||
=================================
|
||||
|
||||
<br>General Clusters
|
||||
----------------
|
||||
### General Clusters
|
||||
|
||||
You can also run Fineract using containers on a Kubernetes cluster.
|
||||
Make sure you set up and connect to your Kubernetes cluster.
|
||||
@@ -321,72 +181,81 @@ You can follow [this](https://cwiki.apache.org/confluence/display/FINERACT/Insta
|
||||
|
||||
Now e.g. from your Google Cloud shell, run the following commands:
|
||||
|
||||
1. `git clone https://github.com/apache/fineract.git ; cd fineract/kubernetes`
|
||||
1. `./kubectl-startup.sh`
|
||||
```bash
|
||||
git clone https://github.com/apache/fineract.git
|
||||
cd fineract/kubernetes
|
||||
./kubectl-startup.sh
|
||||
```
|
||||
|
||||
To shutdown and reset your Cluster, run:
|
||||
```bash
|
||||
./kubectl-shutdown.sh
|
||||
```
|
||||
|
||||
./kubectl-shutdown.sh
|
||||
|
||||
Using Minikube
|
||||
--------------
|
||||
### Using Minikube
|
||||
|
||||
Alternatively, you can run fineract on a local kubernetes cluster using [minikube](https://minikube.sigs.k8s.io/docs/).
|
||||
As Prerequisites, you must have `minikube` and `kubectl` installed on your machine; see
|
||||
As prerequisite you must have `minikube` and `kubectl` installed on your machine; see
|
||||
[Minikube & Kubectl install](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
||||
|
||||
Now to run a new Fineract instance on Minikube you can simply:
|
||||
To run a new Fineract instance on Minikube you can simply:
|
||||
|
||||
1. `git clone https://github.com/apache/fineract.git ; cd fineract/kubernetes`
|
||||
1. `minikube start`
|
||||
1. `./kubectl-startup.sh`
|
||||
1. `minikube service fineract-server --url --https`
|
||||
1. Fineract is now running at the printed URL (note HTTP), which you can check e.g. using:
|
||||
```bash
|
||||
git clone https://github.com/apache/fineract.git
|
||||
cd fineract/kubernetes
|
||||
minikube start
|
||||
./kubectl-startup.sh
|
||||
```
|
||||
|
||||
http --verify=no --timeout 240 --check-status get $(minikube service fineract-server --url --https)/fineract-provider/actuator/health
|
||||
Wait for all pods to be ready:
|
||||
```bash
|
||||
kubectl get pods -w
|
||||
```
|
||||
|
||||
Once all pods are running, access the Mifos web application:
|
||||
```bash
|
||||
minikube service mifos-community
|
||||
```
|
||||
|
||||
This opens the Mifos X web application in your browser. The nginx reverse proxy in the mifos-community pod forwards API requests to the fineract-server backend.
|
||||
|
||||
**Default credentials:**
|
||||
- Username: `mifos`
|
||||
- Password: `password`
|
||||
|
||||
You can also access the Fineract API directly:
|
||||
```bash
|
||||
minikube service fineract-server --url --https
|
||||
```
|
||||
|
||||
Fineract is now running at the printed URL, which you can check e.g. using:
|
||||
```bash
|
||||
http --verify=no --timeout 240 --check-status get $(minikube service fineract-server --url --https)/fineract-provider/actuator/health
|
||||
```
|
||||
|
||||
To check the status of your containers on your local minikube Kubernetes cluster, run:
|
||||
|
||||
minikube dashboard
|
||||
```bash
|
||||
minikube dashboard
|
||||
```
|
||||
|
||||
You can check Fineract logs using:
|
||||
```bash
|
||||
kubectl logs deployment/fineract-server
|
||||
```
|
||||
|
||||
kubectl logs deployment/fineract-server
|
||||
You can check Mifos web app logs using:
|
||||
```bash
|
||||
kubectl logs deployment/mifos-community
|
||||
```
|
||||
|
||||
To shutdown and reset your cluster, run:
|
||||
|
||||
./kubectl-shutdown.sh
|
||||
|
||||
To shutdown and reset your cluster, run:
|
||||
|
||||
minikube ssh
|
||||
|
||||
sudo rm -rf /mnt/data/
|
||||
|
||||
We have [some open issues in JIRA with Kubernetes related enhancement ideas](https://jira.apache.org/jira/browse/FINERACT-783?jql=labels%20%3D%20kubernetes%20AND%20project%20%3D%20%22Apache%20Fineract%22%20) which you are welcome to contribute to.
|
||||
```bash
|
||||
./kubectl-shutdown.sh
|
||||
```
|
||||
|
||||
|
||||
INSTRUCTIONS: How to download Gradle wrapper
|
||||
============
|
||||
The file gradle/wrapper/gradle-wrapper.jar binary is checked into this projects Git source repository,
|
||||
but won't exist in your copy of the Fineract codebase if you downloaded a released source archive from apache.org.
|
||||
In that case, you need to download it using the commands below:
|
||||
|
||||
wget --no-check-certificate -P gradle/wrapper https://github.com/apache/fineract/raw/develop/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
(or)
|
||||
|
||||
curl --insecure -L https://github.com/apache/fineract/raw/develop/gradle/wrapper/gradle-wrapper.jar > gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
INSTRUCTIONS: How to run Apache RAT (Release Audit Tool)
|
||||
============
|
||||
1. Extract the archive file to your local directory.
|
||||
2. Run `./gradlew rat`. A report will be generated under build/reports/rat/rat-report.txt
|
||||
|
||||
|
||||
INSTRUCTIONS: How to enable External Message Broker (ActiveMQ or Apache Kafka)
|
||||
============
|
||||
How to enable External Message Broker (ActiveMQ or Apache Kafka)
|
||||
---
|
||||
|
||||
There are two use-cases where external message broker is needed:
|
||||
- External Business Events / Reliable Event Framework
|
||||
@@ -394,10 +263,11 @@ There are two use-cases where external message broker is needed:
|
||||
|
||||
External Events are business events, e.g.: `ClientCreated`, which might be important for third party systems. Apache Fineract supports ActiveMQ (or other JMS compliant brokers) and Apache Kafka endpoints for sending out Business Events. By default, they are not emitted.
|
||||
|
||||
In case of a large deployment with millions of accounts, the Close of Business Day Spring Batch job may run several hours. In order to speed up this task, remote partitioning of the job is supported. The Manager node partitions (breaks up) the COB job into smaller pieces (sub tasks) which then can be executed on multiple Worker nodes in parallel. The worker nodes are notified either by ActiveMQ or Kafka regarding their new sub tasks.
|
||||
### Active MQ
|
||||
In case of a large deployment with millions of accounts, the Close of Business Day Spring Batch job may run several hours. In order to speed up this task, remote partitioning of the job is supported. The Manager node partitions breaks up the COB job into smaller pieces (sub tasks), which then can be executed on multiple Worker nodes in parallel. The worker nodes are notified either by ActiveMQ or Kafka regarding their new sub tasks.
|
||||
|
||||
JMS based messaging is disabled by default. In `docker-compose-postgresql-activemq.yml` an example is shown where ActiveMQ is enabled. In that configuration one Spring Batch Manager instance and two Spring Batch Worker instances are created.
|
||||
### ActiveMQ
|
||||
|
||||
JMS based messaging is disabled by default. In `docker-compose-postgresql-activemq.yml` an example is shown, where ActiveMQ is enabled. In that configuration one Spring Batch Manager instance and two Spring Batch Worker instances are created.
|
||||
Spring based events should be disabled and jms based event handling should be enabled. Furthermore, proper broker JMS URL should be configured.
|
||||
|
||||
```
|
||||
@@ -410,186 +280,92 @@ For additional ActiveMQ related configuration please take a look to the `applica
|
||||
|
||||
### Kafka
|
||||
|
||||
Kafka support also disabled by default. In `docker-compose-postgresql-kafka.yml` an example is shown where self-hosted Kafka is enabled for both External Events and Spring Batch Remote Job execution.
|
||||
Kafka support is also disabled by default. In `docker-compose-postgresql-kafka.yml` an example is shown, where self-hosted Kafka is enabled for both External Events and Spring Batch Remote Job execution.
|
||||
|
||||
During the development Fineract was tested with PLAINTEXT Kafka brokers without authentication and with AWS MSK using IAM authentication. The extra [jar file](https://github.com/aws/aws-msk-iam-auth/releases) required for IAM authentication is already added to the classpath.
|
||||
During the development Fineract was tested with PLAINTEXT Kafka brokers without authentication and with AWS MSK using IAM authentication. The extra [JAR file](https://github.com/aws/aws-msk-iam-auth/releases) required for IAM authentication is already added to the classpath.
|
||||
An example MSK setup can be found in `docker-compose-postgresql-kafka-msk.yml`.
|
||||
|
||||
The full list of supported Kafka related properties are documented here: https://fineract.apache.org/docs/current/
|
||||
The full list of supported Kafka related properties is documented in the [Fineract Platform documentation](https://fineract.apache.org/docs/current/).
|
||||
|
||||
Checkstyle and Spotless
|
||||
|
||||
DATABASE AND TABLES
|
||||
===================
|
||||
|
||||
You can run the required version of the database server in a container, instead of having to install it, like this:
|
||||
|
||||
docker run --name mariadb-11.5 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:11.5.2
|
||||
|
||||
and stop and destroy it like this:
|
||||
|
||||
docker rm -f mariadb-11.5
|
||||
|
||||
Beware that this container database keeps its state inside the container and not on the host filesystem. It is lost when you destroy (rm) this container. This is typically fine for development. See [Caveats: Where to Store Data on the database container documentation](https://hub.docker.com/_/mariadb) regarding how to make it persistent instead of ephemeral.
|
||||
|
||||
|
||||
MySQL/MariaDB and UTC timezone
|
||||
---
|
||||
With release `1.8.0` we introduced improved date time handling in Fineract. Date time is stored in UTC, and UTC timezone enforced even on the JDBC driver, e. g. for MySQL:
|
||||
|
||||
```
|
||||
serverTimezone=UTC&useLegacyDatetimeCode=false&sessionVariables=time_zone='-00:00'
|
||||
```
|
||||
|
||||
If you use MySQL as Fineract database, the following configuration is highly recommended:
|
||||
|
||||
* Run the application in UTC (the default command line in our Docker image has the necessary parameters already set)
|
||||
* Run the MySQL database server in UTC (if you use managed services like AWS RDS, then this should be the default anyway, but it would be good to double-check)
|
||||
|
||||
In case Fineract and MySQL do not run in UTC, MySQL might save date time values differently from PostgreSQL
|
||||
|
||||
Example scenario: If the Fineract instance runs in timezone: GMT+2, and the local date time is 2022-08-11 17:15 ...
|
||||
* ... then PostgreSQL saves the LocalDateTime as is: 2022-08-11 17:15
|
||||
* ... and MySQL saves the LocalDateTime in UTC: 2022-08-11 15:15
|
||||
* ... but when we read the date time from PostgreSQL or from MySQL, both systems give us the same value: 2022-08-11 17:15 GMT+2
|
||||
|
||||
If a previously used Fineract instance didn't run in UTC (backward compatibility), all prior dates will be read wrongly by MySQL. This can cause issues, when you run the database migration scripts.
|
||||
|
||||
Recommendation: Shift all dates in your database by the timezone offset that your Fineract instance used.
|
||||
|
||||
|
||||
CONNECTION POOL CONFIGURATION
|
||||
=======
|
||||
|
||||
Please check `application.properties` to see which connection pool settings can be tweaked. The associated environment variables are prefixed with `FINERACT_HIKARI_*`. You can find more information about specific connection pool settings at the [HikariCP Github repository](https://github.com/brettwooldridge/HikariCP?tab=readme-ov-file#gear-configuration-knobs-baby).
|
||||
|
||||
NOTE: We keep backwards compatibility until one of the next releases to ensure that things are working as expected. Environment variables prefixed `fineract_tenants_*` can still be used to configure the database connection, but we strongly encourage using `FINERACT_HIKARI_*` with more options.
|
||||
|
||||
|
||||
RELEASES
|
||||
============
|
||||
|
||||
This project enforces its code conventions using [checkstyle.xml](config/checkstyle/checkstyle.xml) through Checkstyle and [fineract-formatting-preferences.xml](config/fineract-formatting-preferences.xml) through Spotless. They are configured to run automatically during the normal Gradle build, and fail if there are any violations detected. You can run the following command to automatically fix spotless violations:
|
||||
[Official releases](https://fineract.apache.org/#downloads) are created quarterly, at the end of each quarter. Our [documented release procedure](https://fineract.apache.org/docs/current/#_releases) follows the [ASF release policy](https://www.apache.org/legal/release-policy.html).
|
||||
|
||||
`./gradlew spotlessApply`
|
||||
See <https://cwiki.apache.org/confluence/display/FINERACT/Fineract+Releases> for an archive of historical release notes along with JIRA issues relevant to each release.
|
||||
|
||||
Since some checks are present in both Checkstyle and Spotless, the same command can help you fix some of the Checkstyle violations (but not all, other Checkstyle violations need to fixed manually).
|
||||
See <https://github.com/apache/fineract/releases> for a list of PRs and contributors for each release.
|
||||
|
||||
You can also check for Spotless violations (only; but normally don't have to, because the regular build full already includes this anyway):
|
||||
EOL/unsupported releases are [archived](https://www.apache.org/legal/release-policy.html#archived).
|
||||
|
||||
`./gradlew spotlessCheck`
|
||||
|
||||
We recommend that you configure your favourite Java IDE to match those conventions. For Eclipse, you can go to
|
||||
Window > Java > Code Style and import our [config/fineractdev-formatter.xml](config/fineractdev-formatter.xml) under formatter section and [config/fineractdev-cleanup.xml](config/fineractdev-cleanup.xml) under Clean up section. The same fineractdev-formatter.xml configuration file (that can be used in Eclipse IDE) is also used by Spotless to both check for violations and autoformat code on the CLI.
|
||||
You could also use Checkstyle directly in your IDE (but you don't neccesarily have to, it may just be more convenient for you). For Eclipse, use https://checkstyle.org/eclipse-cs/ and load our checkstyle.xml into it, for IntelliJ you can use [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea).
|
||||
Versioned build products created from the `develop` branch will include `-SNAPSHOT`. See [related settings](https://github.com/qoomon/gradle-git-versioning-plugin) near `version` and `gitVersioning` in `build.gradle` for details.
|
||||
|
||||
|
||||
Code Coverage Reports
|
||||
LICENSE
|
||||
============
|
||||
|
||||
The project uses Jacoco to measure unit tests code coverage, to generate a report run the following command:
|
||||
This project is licensed under [Apache License Version 2.0](https://github.com/apache/fineract/blob/develop/APACHE_LICENSETEXT.md).
|
||||
|
||||
`./gradlew clean build jacocoTestReport`
|
||||
|
||||
Generated reports can be found in build/code-coverage directory.
|
||||
|
||||
|
||||
Versions
|
||||
============
|
||||
|
||||
The latest stable release can be viewed on the develop branch: [Latest Release on Develop](https://github.com/apache/fineract/tree/develop "Latest Release").
|
||||
|
||||
The progress of this project can be viewed here: [View change log](https://github.com/apache/fineract/blob/develop/CHANGELOG.md "Latest release change log")
|
||||
|
||||
|
||||
License
|
||||
============
|
||||
|
||||
This project is licensed under Apache License Version 2.0. See <https://github.com/apache/fineract/blob/develop/APACHE_LICENSETEXT.md> for reference.
|
||||
|
||||
The Connector/J JDBC Driver client library from MariaDB.org, which is licensed under the LGPL,
|
||||
is used in development when running integration tests that use the Liquibase library. That JDBC
|
||||
driver is however not included in and distributed with the Fineract product and is not
|
||||
required to use the product.
|
||||
If you are developer and object to using the LGPL licensed Connector/J JDBC driver,
|
||||
simply do not run the integration tests that use the Liquibase library and/or use another JDBC driver.
|
||||
The Connector/J JDBC Driver client library from [MariaDB](https://www.mariadb.org) is licensed under the LGPL.
|
||||
The library is often used in development when running integration tests that use the Liquibase library. That JDBC
|
||||
driver is however not distributed with the Fineract product and is not required to use the product.
|
||||
If you are a developer and object to using the LGPL licensed Connector/J JDBC driver,
|
||||
simply do not run the integration tests that use the Liquibase library and use another JDBC driver.
|
||||
As discussed in [LEGAL-462](https://issues.apache.org/jira/browse/LEGAL-462), this project therefore
|
||||
complies with the [Apache Software Foundation third-party license policy](https://www.apache.org/legal/resolved.html).
|
||||
|
||||
|
||||
<br><br>APACHE FINERACT PLATFORM API
|
||||
PLATFORM API
|
||||
============
|
||||
|
||||
The API for Fineract is documented in [apiLive.htm](fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm), and the [apiLive.htm can be viewed on fineract.apache.org](https://fineract.apache.org/docs/legacy/ "API Documentation"). If you have your own Fineract instance running, you can find this documentation under [/fineract-provider/legacy-docs/apiLive.htm](https://localhost:8443/fineract-provider/legacy-docs/apiLive.htm).
|
||||
Fineract does not provide a UI, but provides an API. Running Fineract locally, the Swagger documentation can be accessed under `https://localhost:8443/fineract-provider/swagger-ui/index.html`. A live version can be accessed via [this Sandbox](https://sandbox.mifos.community/fineract-provider/swagger-ui/index.html) (not hosted by us).
|
||||
|
||||
The Swagger documentation (work in progress; see [FINERACT-733](https://issues.apache.org/jira/browse/FINERACT-733)) can be accessed under [/fineract-provider/swagger-ui/index.html](https://localhost:8443/fineract-provider/swagger-ui/index.html) and [live Swagger UI here on Fineract.dev](https://sandbox.mifos.community/fineract-provider/swagger-ui/index.html).
|
||||
|
||||
Apache Fineract supports client code generation using [Swagger Codegen](https://github.com/swagger-api/swagger-codegen) based on the [OpenAPI Specification](https://swagger.io/specification/). For more instructions on how to generate the client code, check [fineract-doc/src/docs/en/chapters/sdk/client.adoc](fineract-doc/src/docs/en/chapters/sdk/client.adoc).
|
||||
|
||||
|
||||
<br>API CLIENTS (Web UIs, Mobile, etc.)
|
||||
============
|
||||
|
||||
* https://github.com/openMF/community-app/ is the "traditional" Reference Client App Web UI for the API offered by this project
|
||||
* https://github.com/openMF/web-app is the next generation UI rewrite also using this project's API
|
||||
* https://github.com/openMF/android-client is an Android Mobile App client for this project's API
|
||||
* https://github.com/openMF has more related proejcts
|
||||
|
||||
|
||||
<br>ONLINE DEMOS
|
||||
============
|
||||
|
||||
* [sandbox.mifos.community](https://sandbox.mifos.community) always runs the latest version of this code
|
||||
* [demo.mifos.io](https://demo.mifos.io) A demo account is provided for users to experience the functionality of the Community App. Users can use "mifos" for USERNAME and "password" for PASSWORD (without quotation marks).
|
||||
* [Swagger-UI Demo video](https://www.youtube.com/watch?v=FlVd-0YAo6c) This is a demo video for Swagger-UI documentation, more information [here](https://github.com/apache/fineract#swagger-ui-documentation).
|
||||
|
||||
|
||||
<br>DEVELOPERS
|
||||
============
|
||||
Please see <https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone> for the developers wiki page.
|
||||
|
||||
Please refer to <https://cwiki.apache.org/confluence/display/FINERACT/Fineract+101> for the first-time contribution to this project.
|
||||
|
||||
Please see <https://cwiki.apache.org/confluence/display/FINERACT/How-to+articles> for technical details to get started.
|
||||
|
||||
Please visit [our JIRA Dashboard](https://issues.apache.org/jira/secure/Dashboard.jspa?selectPageId=12335824) to find issues to work on, see what others are working on, or open new issues.
|
||||
|
||||
|
||||
<br>VIDEO DEMONSTRATION
|
||||
============
|
||||
|
||||
Apache Fineract / Mifos X Demo (November 2016) - <https://www.youtube.com/watch?v=h61g9TptMBo>
|
||||
|
||||
<br>SWAGGER UI DEMONSTRATION
|
||||
============
|
||||
|
||||
We use Swagger-UI to generate and maintain our API documentation, you can see the demo video [here](https://www.youtube.com/watch?v=FlVd-0YAo6c) or a live version
|
||||
[here](https://sandbox.mifos.community/fineract-provider/swagger-ui/index.html). If you interested to know more about Swagger-UI you can check their [website](https://swagger.io/).
|
||||
|
||||
<br>GOVERNANCE AND POLICIES
|
||||
=======================
|
||||
|
||||
[Becoming a Committer](https://cwiki.apache.org/confluence/display/FINERACT/Becoming+a+Committer)
|
||||
documents the process through which you can become a committer in this project.
|
||||
|
||||
|
||||
<br>ERROR HANDLING GUIDELINES
|
||||
------------------
|
||||
* When catching exceptions, either rethrow them, or log them. Either way, include the root cause by using `catch (SomeException e)` and then either `throw AnotherException("..details..", e)` or `LOG.error("...context...", e)`.
|
||||
* Completely empty catch blocks are VERY suspicous! Are you sure that you want to just "swallow" an exception? Really, 100% totally absolutely sure?? ;-) Such "normal exceptions which just happen sometimes but are actually not really errors" are almost always a bad idea, can be a performance issue, and typically are an indication of another problem - e.g. the use of a wrong API which throws an Exception for an expected condition, when really you would want to use another API that instead returns something empty or optional.
|
||||
* In tests, you'll typically never catch exceptions, but just propagate them, with `@Test void testXYZ() throws SomeException, AnotherException`..., so that the test fails if the exception happens. Unless you actually really want to test for the occurence of a problem - in that case, use [JUnit's Assert.assertThrows()](https://github.com/junit-team/junit4/wiki/Exception-testing) (but not `@Test(expected = SomeException.class)`).
|
||||
* Never catch `NullPointerException` & Co.
|
||||
|
||||
<br>LOGGING GUIDELINES
|
||||
------------------
|
||||
* We use [SLF4J](http://www.slf4j.org) as our logging API.
|
||||
* Never, ever, use `System.out` and `System.err` or `printStackTrace()` anywhere, but always `LOG.info()` or `LOG.error()` instead.
|
||||
* Use placeholder (`LOG.error("Could not... details: {}", something, exception)`) and never String concatenation (`LOG.error("Could not... details: " + something, exception)`)
|
||||
* Which Log Level is appropriate?
|
||||
* `LOG.error()` should be used to inform an "operator" running Fineract who supervises error logs of an unexpected condition. This includes technical problems with an external "environment" (e.g. can't reach a database), and situations which are likely bugs which need to be fixed in the code. They do NOT include e.g. validation errors for incoming API requests - that is signaled through the API response - and does (should) not be logged as an error. (Note that there is no _FATAL_ level in SLF4J; a "FATAL" event should just be logged as an _ERROR_.)
|
||||
* `LOG.warn()` should be using sparingly. Make up your mind if it's an error (above) - or not!
|
||||
* `LOG.info()` can be used notably for one-time actions taken during start-up. It should typically NOT be used to print out "regular" application usage information. The default logging configuration always outputs the application INFO logs, and in production under load, there's really no point to constantly spew out lots of information from frequently traversed paths in the code about what's going on. (Metrics are a better way.) `LOG.info()` *can* be used freely in tests though.
|
||||
* `LOG.debug()` can be used anywhere in the code to log things that may be useful during investigations of specific problems. They are not shown in the default logging configuration, but can be enabled for troubleshooting. Developers should typically "turn down" most `LOG.info()` which they used while writing a new feature to "follow along what happens during local testing" to `LOG.debug()` for production before we merge their PRs.
|
||||
* `LOG.trace()` is not used in Fineract.
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
|
||||
We request that your commit message include a FINERACT JIRA issue, and a one-liner that describe the changes.
|
||||
Start with an upper case imperative verb (not past form), and a short but concise clear description. (E.g. "FINERACT-821: Add enforced HideUtilityClassConstructor checkstyle").
|
||||
|
||||
If your PR is failing to pass our CI build due to a test failure, then:
|
||||
|
||||
1. Understand if the failure is due to your PR or an unrelated unstable test.
|
||||
1. If you suspect it is because of a "flaky" test, and not due to a change in your PR, then please do not simply wait for an active maintainer to come and help you, but instead be a proactive contributor to the project - see next steps. Do understand that we may not review PRs that are not green - it is the contributor's (that's you!) responsability to get a proposed PR to pass the build, not primarily the maintainers.
|
||||
1. Search for the name of the failed test on https://issues.apache.org/jira/, e.g. for `AccountingScenarioIntegrationTest` you would find [FINERACT-899](https://issues.apache.org/jira/browse/FINERACT-899).
|
||||
1. If you happen to read in such bugs that tests were just recently fixed, or ignored, then rebase your PR to pick up that change.
|
||||
1. If you find previous comments "proving" that the same test has arbitrarily failed in at least 3 past PRs, then please do yourself raise a small separate new PR proposing to add an `@Disabled // TODO FINERACT-123` to the respective unstable test (e.g. [#774](https://github.com/apache/fineract/pull/774)) with the commit message mentioning said JIRA, as always. (Please do NOT just `@Disabled` any existing tests mixed in as part of your larger PR.)
|
||||
1. If there is no existing JIRA for the test, then first please evaluate whether the failure couldn't be a (perhaps strange) impact of the change you are proposing after all. If it's not, then please raise a new JIRA to document the suspected Flaky Test, and link it to [FINERACT-850](https://issues.apache.org/jira/browse/FINERACT-850). This will allow the next person coming along hitting the same test failure to easily find it, and eventually propose to ignore the unstable test.
|
||||
1. Then (only) Close and Reopen your PR, which will cause a new build, to see if it passes.
|
||||
1. Of course, we very much appreciate you then jumping onto any such bugs and helping us figure out how to fix all ignored tests!
|
||||
|
||||
[Pull Request Size Limit](https://cwiki.apache.org/confluence/display/FINERACT/Pull+Request+Size+Limit)
|
||||
documents that we cannot accept huge "code dump" Pull Requests, with some related suggestions.
|
||||
|
||||
Guideline for new Feature commits involving Refactoring: If you are submitting PR for a new Feature,
|
||||
and it involves refactoring, try to differentiate "new Feature code" with "Refactored" by placing
|
||||
them in different commits. This helps review to review your code faster.
|
||||
|
||||
We have an automated Bot which marks pull requests as "stale" after a while, and ultimately automatically closes them.
|
||||
|
||||
|
||||
Merge Strategy
|
||||
--------------
|
||||
|
||||
This project's committers typically prefer to bring your Pull Requests in through _Rebase and Merge_ instead of _Create a Merge Commit_. (If you are unfamiliar with GitHub's UI re. this, note the somewhat hidden little triangle drop-down at the bottom of PR, visible only to committers, not contributors.) This avoids the "merge commits" which we consider to be somewhat "polluting" the projects commits log history view. We understand this doesn't give an easy automatic reference to the original PR (which GitHub automatically adds to the Merge Commit message it generates), but we consider this an only very minor inconvenience; it's typically relatively easy to find the original PR even just from the commit message, and JIRA.
|
||||
|
||||
We expect most proposed PRs to typically consist of a single commit. Committers may use _Squash and merge_ to combine your commits at merge time, and if they do so will rewrite your commit message as they see fit.
|
||||
|
||||
Neither of these two are hard absolute rules, but mere conventions. Multiple commits in single PR make sense in certain cases (e.g. branch backports).
|
||||
|
||||
|
||||
Dependency Upgrades
|
||||
-------------------
|
||||
|
||||
This project uses a number of 3rd-party libraries, and this section provides some guidance for their updates. We have set-up [Renovate's bot](https://renovate.whitesourcesoftware.com) to automatically raise Pull Requests for our review when new dependencies are available [FINERACT-962](https://issues.apache.org/jira/browse/FINERACT-962).
|
||||
|
||||
Upgrades sometimes require package name changes. Changed code should ideally have test coverage.
|
||||
|
||||
Our `ClasspathHellDuplicatesCheckRuleTest` detects classes that appear in more than 1 JAR. If a version bump in [`build.gradle`](https://github.com/search?q=repo%3Aapache%2Ffineract+filename%3Abuild.gradle&type=Code&ref=advsearch&l=&l=) causes changes in transitives dependencies, then you may have to add related `exclude` to our [`dependencies.gradle`](https://github.com/apache/fineract/search?q=dependencies.gradle). Running `./gradlew dependencies` helps to understand what is required.
|
||||
|
||||
|
||||
More Information
|
||||
============
|
||||
More details of the project can be found at <https://cwiki.apache.org/confluence/display/FINERACT>.
|
||||
Apache Fineract supports client code generation using [Swagger Codegen](https://github.com/swagger-api/swagger-codegen) based on the [OpenAPI Specification](https://swagger.io/specification/). For more instructions on how to generate client code, check [this section](https://fineract.apache.org/docs/current/#_generate_api_client) of the Fineract documentation. [This video](https://www.youtube.com/watch?v=FlVd-0YAo6c) documents the use of the Swagger-UI.
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "sanctions-screening-001",
|
||||
"name": "Sanctions Screening API",
|
||||
"description": "API collection for UN Security Council consolidated sanctions list screening.\n\nEndpoints:\n- Screen client names against sanctions list\n- Load sanctions XML data\n- Check sanctions database status\n\nBase URL: {{baseUrl}}/fineract-provider/api/v1",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "{{username}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "{{password}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://localhost:8443"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "mifos"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "password"
|
||||
},
|
||||
{
|
||||
"key": "tenantId",
|
||||
"value": "default"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Sanctions Screening",
|
||||
"description": "Screen client names against the UN Security Council consolidated sanctions list",
|
||||
"item": [
|
||||
{
|
||||
"name": "Screen Name (GET)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen?name=Mohammad Ali",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "Mohammad Ali",
|
||||
"description": "Name to screen against sanctions list"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Screen a client name against the UN Security Council consolidated sanctions list using GET method.\n\n**Response when match found (FLAGGED):**\n```json\n{\n \"screenedName\": \"Mohammad Ali\",\n \"matchFound\": true,\n \"status\": \"FLAGGED\",\n \"message\": \"Potential match found in UN Security Council consolidated sanctions list. Manual review required before proceeding.\",\n \"matchCount\": 2,\n \"matches\": [...]\n}\n```\n\n**Response when no match (CLEAR):**\n```json\n{\n \"screenedName\": \"John Smith\",\n \"matchFound\": false,\n \"status\": \"CLEAR\",\n \"message\": \"No match found in UN Security Council consolidated sanctions list.\",\n \"matchCount\": 0,\n \"matches\": []\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Screen Name (POST)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"AL-QAIDA\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||
},
|
||||
"description": "Screen a client name against sanctions list using POST method with JSON body."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Screen Name - Individual (FLAGGED example)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"ERIC BADEGE\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||
},
|
||||
"description": "Example: Screen a known sanctioned individual name. Should return FLAGGED status."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Screen Name - Clear (no match)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Aditya Kumar\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||
},
|
||||
"description": "Example: Screen a name that does not exist in sanctions list. Should return CLEAR status."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Screen Name - Entity (FLAGGED example)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"TALIBAN\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||
},
|
||||
"description": "Example: Screen a known sanctioned entity name. Should return FLAGGED status."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sanctions Management",
|
||||
"description": "Load and manage sanctions data",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Sanctions Status",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/status",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "status"]
|
||||
},
|
||||
"description": "Get the count of active sanctions entries in the database.\n\n**Response:**\n```json\n{\n \"totalActiveEntries\": 1002,\n \"status\": \"ACTIVE\"\n}\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Load Sanctions XML",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/xml",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "file",
|
||||
"file": {
|
||||
"src": ""
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/load",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "load"]
|
||||
},
|
||||
"description": "Upload and load the UN consolidated sanctions XML file into the database.\n\nSelect the `consolidatedLegacyByPRN.xml` file in the Body > Binary section.\n\n**Response:**\n```json\n{\n \"message\": \"Sanctions data loaded successfully\",\n \"entriesLoaded\": 1002,\n \"totalActive\": 1002\n}\n```"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screening Activity Logs",
|
||||
"description": "View sanctions screening activity logs — every search is recorded",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Screening Logs",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"]
|
||||
},
|
||||
"description": "Get all sanctions screening activity logs (most recent first).\n\n**Response:**\n```json\n[\n {\n \"id\": 1,\n \"screenedName\": \"ERIC BADEGE\",\n \"screeningResult\": \"FLAGGED\",\n \"matchCount\": 1,\n \"matchedNames\": \"ERIC BADEGE\",\n \"screeningSource\": \"CLIENT_CREATION\",\n \"screenedOn\": \"2026-03-16T10:30:00Z\",\n \"screenedBy\": 1\n }\n]\n```"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get FLAGGED Logs Only",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?result=FLAGGED",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||
"query": [
|
||||
{
|
||||
"key": "result",
|
||||
"value": "FLAGGED",
|
||||
"description": "Filter by result: FLAGGED or CLEAR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Get only FLAGGED screening logs — shows all searches that matched a sanctioned entry."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get CLEAR Logs Only",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?result=CLEAR",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||
"query": [
|
||||
{
|
||||
"key": "result",
|
||||
"value": "CLEAR",
|
||||
"description": "Filter by result: FLAGGED or CLEAR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Get only CLEAR screening logs — shows all searches that did not match any sanctioned entry."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search Logs by Name",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenantId}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?name=ERIC",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "ERIC",
|
||||
"description": "Search logs by screened name (partial match)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Search screening logs by the name that was screened (partial, case-insensitive match)."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "savings-products-collection",
|
||||
"name": "Fineract - Savings Products",
|
||||
"description": "Postman collection for Savings Product APIs.\n\nThis resource exposes 6 endpoints:\n1. POST /v1/savingsproducts\n2. PUT /v1/savingsproducts/{productId}\n3. GET /v1/savingsproducts\n4. GET /v1/savingsproducts/{productId}\n5. GET /v1/savingsproducts/template\n6. DELETE /v1/savingsproducts/{productId}\n\nThe create request below uses the mandatory fields required by the backend validator. If you switch accountingRule to cash-based or accrual-periodic, you must also supply the corresponding accounting mappings.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{
|
||||
"key": "username",
|
||||
"value": "{{username}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "{{password}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "https://localhost:8443/fineract-provider/api/v1",
|
||||
"description": "Fineract API base URL"
|
||||
},
|
||||
{
|
||||
"key": "tenant_id",
|
||||
"value": "default",
|
||||
"description": "Tenant identifier"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "mifos",
|
||||
"description": "API username"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "password",
|
||||
"description": "API password"
|
||||
},
|
||||
{
|
||||
"key": "product_id",
|
||||
"value": "1",
|
||||
"description": "Savings product id returned by the create request"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Savings Products",
|
||||
"item": [
|
||||
{
|
||||
"name": "Create Savings Product",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status code is 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"",
|
||||
"pm.test('Resource id returned', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('resourceId');",
|
||||
" pm.collectionVariables.set('product_id', json.resourceId);",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Passbook Savings\",\n \"shortName\": \"PBSV\",\n \"description\": \"Daily compounding savings product\",\n \"currencyCode\": \"USD\",\n \"digitsAfterDecimal\": 2,\n \"inMultiplesOf\": 0,\n \"locale\": \"en\",\n \"nominalAnnualInterestRate\": 5.0,\n \"interestCompoundingPeriodType\": 1,\n \"interestPostingPeriodType\": 4,\n \"interestCalculationType\": 1,\n \"interestCalculationDaysInYearType\": 365,\n \"accountingRule\": 1,\n \"withdrawalFeeForTransfers\": false,\n \"enforceMinRequiredBalance\": false,\n \"allowOverdraft\": false,\n \"withHoldTax\": false,\n \"isDormancyTrackingActive\": false,\n \"charges\": [\n {\n \"id\": 1\n }\n ]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts"
|
||||
]
|
||||
},
|
||||
"description": "Create a savings product.\n\nRequired by backend validator:\n- name\n- shortName\n- currencyCode\n- digitsAfterDecimal\n- nominalAnnualInterestRate\n- interestCompoundingPeriodType\n- interestPostingPeriodType\n- interestCalculationType\n- interestCalculationDaysInYearType\n- accountingRule\n\nIf accountingRule = 2 or 3, the backend also expects accounting mappings."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update Savings Product",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"description\": \"Updated savings product description\",\n \"locale\": \"en\",\n \"nominalAnnualInterestRate\": 5.75\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts/{{product_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts",
|
||||
"{{product_id}}"
|
||||
]
|
||||
},
|
||||
"description": "Update an existing savings product by product id."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "List Savings Products",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts"
|
||||
]
|
||||
},
|
||||
"description": "Lists all savings products."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Savings Product By ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts/{{product_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts",
|
||||
"{{product_id}}"
|
||||
]
|
||||
},
|
||||
"description": "Retrieves a savings product by id. You can also call the same endpoint with ?template=true."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Savings Product Template",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts/template",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts",
|
||||
"template"
|
||||
]
|
||||
},
|
||||
"description": "Returns template data with dropdown options and default values for savings product creation."
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete Savings Product",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Fineract-Platform-TenantId",
|
||||
"value": "{{tenant_id}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/savingsproducts/{{product_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"savingsproducts",
|
||||
"{{product_id}}"
|
||||
]
|
||||
},
|
||||
"description": "Deletes a savings product by id."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "Savings product master APIs."
|
||||
}
|
||||
]
|
||||
}
|
||||
+91
-48
@@ -26,6 +26,7 @@ buildscript {
|
||||
fineractJavaProjects = subprojects.findAll{
|
||||
[
|
||||
'fineract-core',
|
||||
'fineract-security',
|
||||
'fineract-cob',
|
||||
'fineract-validation',
|
||||
'fineract-command',
|
||||
@@ -37,25 +38,32 @@ buildscript {
|
||||
'fineract-charge',
|
||||
'fineract-rates',
|
||||
'fineract-tax',
|
||||
'fineract-loan-origination',
|
||||
'fineract-loan',
|
||||
'fineract-savings',
|
||||
'fineract-current-product',
|
||||
'fineract-report',
|
||||
'fineract-mix',
|
||||
'integration-tests',
|
||||
'twofactor-tests',
|
||||
'oauth2-tests',
|
||||
'fineract-client',
|
||||
'fineract-client-feign',
|
||||
'fineract-avro-schemas',
|
||||
'fineract-e2e-tests-core',
|
||||
'fineract-e2e-tests-runner',
|
||||
'fineract-progressive-loan',
|
||||
'fineract-progressive-loan-embeddable-schedule-generator'
|
||||
'fineract-progressive-loan-embeddable-schedule-generator',
|
||||
'fineract-working-capital-loan'
|
||||
].contains(it.name)
|
||||
}
|
||||
fineractPublishProjects = subprojects.findAll{
|
||||
[
|
||||
'fineract-avro-schemas',
|
||||
'fineract-client',
|
||||
'fineract-client-feign',
|
||||
'fineract-core',
|
||||
'fineract-security',
|
||||
'fineract-cob',
|
||||
'fineract-validation',
|
||||
'fineract-command',
|
||||
@@ -65,13 +73,15 @@ buildscript {
|
||||
'fineract-charge',
|
||||
'fineract-rates',
|
||||
'fineract-tax',
|
||||
'fineract-loan-origination',
|
||||
'fineract-loan',
|
||||
'fineract-savings',
|
||||
'fineract-report',
|
||||
'fineract-branch',
|
||||
'fineract-document',
|
||||
'fineract-progressive-loan',
|
||||
'fineract-progressive-loan-embeddable-schedule-generator'
|
||||
'fineract-progressive-loan-embeddable-schedule-generator',
|
||||
'fineract-working-capital-loan'
|
||||
].contains(it.name)
|
||||
}
|
||||
npmRepository = 'https://npm.pkg.github.com'
|
||||
@@ -100,7 +110,7 @@ plugins {
|
||||
id 'com.github.hierynomus.license' version '0.16.1' apply false
|
||||
id 'com.github.jk1.dependency-license-report' version '2.9' apply false
|
||||
id 'org.zeroturnaround.gradle.jrebel' version '1.2.0' apply false
|
||||
id 'org.springframework.boot' version '3.5.5' apply false
|
||||
id 'org.springframework.boot' version '3.5.6' apply false
|
||||
id 'net.ltgt.errorprone' version '4.1.0' apply false
|
||||
id 'io.swagger.core.v3.swagger-gradle-plugin' version '2.2.23' apply false
|
||||
id 'com.gorylenko.gradle-git-properties' version '2.4.2' apply false
|
||||
@@ -115,6 +125,8 @@ plugins {
|
||||
id 'org.openapi.generator' version '7.8.0' apply false
|
||||
id 'com.gradleup.shadow' version '8.3.5' apply false
|
||||
id 'me.champeau.jmh' version '0.7.1' apply false
|
||||
id 'org.cyclonedx.bom' version '3.1.0' apply false
|
||||
id 'com.docktape.swagger-brake' version '2.7.0' apply false
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.release.gradle"
|
||||
@@ -161,6 +173,15 @@ allprojects {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
resolutionStrategy {
|
||||
dependencySubstitution {
|
||||
// Substitution is to resolve CVE-2025-12183
|
||||
substitute module('org.lz4:lz4-java') using module('at.yawk.lz4:lz4-java:1.10.1')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
implementation {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
@@ -342,8 +363,7 @@ allprojects {
|
||||
"**/generated/**/*MapperImpl.java",
|
||||
'**/META-INF/fineract-test.config',
|
||||
// Apache-specific GitHub metadata settings file
|
||||
'/.asf.yaml',
|
||||
'**/*.sh'
|
||||
'/.asf.yaml'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -379,7 +399,7 @@ configure(project.fineractJavaProjects) {
|
||||
}
|
||||
|
||||
// Add performance optimizations
|
||||
configurations.all {
|
||||
configurations.configureEach {
|
||||
resolutionStrategy {
|
||||
cacheChangingModulesFor 0, 'seconds'
|
||||
cacheDynamicVersionsFor 0, 'seconds'
|
||||
@@ -427,40 +447,40 @@ configure(project.fineractJavaProjects) {
|
||||
configurations {
|
||||
api.setCanBeResolved(true)
|
||||
}
|
||||
tasks.withType(Copy) {
|
||||
tasks.withType(Copy).configureEach {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
tasks.withType(JavaCompile) {
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += [
|
||||
"-Xlint:cast",
|
||||
"-Xlint:auxiliaryclass",
|
||||
"-Xlint:dep-ann",
|
||||
"-Xlint:divzero",
|
||||
"-Xlint:empty",
|
||||
"-Xlint:exports",
|
||||
"-Xlint:fallthrough",
|
||||
"-Xlint:finally",
|
||||
"-Xlint:module",
|
||||
"-Xlint:opens",
|
||||
"-Xlint:options",
|
||||
"-Xlint:overloads",
|
||||
"-Xlint:overrides",
|
||||
"-Xlint:path",
|
||||
"-Xlint:processing",
|
||||
"-Xlint:removal",
|
||||
"-Xlint:requires-automatic",
|
||||
"-Xlint:requires-transitive-automatic",
|
||||
"-Xlint:try",
|
||||
"-Xlint:varargs",
|
||||
"-Xlint:preview",
|
||||
"-Xlint:static",
|
||||
// -Werror needs to be disabled because EclipseLink's static weaving doesn't generate warning-free code
|
||||
// and during an IntelliJ recompilation, it fails
|
||||
//"-Werror",
|
||||
"-Xmaxwarns",
|
||||
"1500",
|
||||
"-Xmaxerrs",
|
||||
"1500"
|
||||
"-Xlint:cast",
|
||||
"-Xlint:auxiliaryclass",
|
||||
"-Xlint:dep-ann",
|
||||
"-Xlint:divzero",
|
||||
"-Xlint:empty",
|
||||
"-Xlint:exports",
|
||||
"-Xlint:fallthrough",
|
||||
"-Xlint:finally",
|
||||
"-Xlint:module",
|
||||
"-Xlint:opens",
|
||||
"-Xlint:options",
|
||||
"-Xlint:overloads",
|
||||
"-Xlint:overrides",
|
||||
"-Xlint:path",
|
||||
"-Xlint:processing",
|
||||
"-Xlint:removal",
|
||||
"-Xlint:requires-automatic",
|
||||
"-Xlint:requires-transitive-automatic",
|
||||
"-Xlint:try",
|
||||
"-Xlint:varargs",
|
||||
"-Xlint:preview",
|
||||
"-Xlint:static",
|
||||
// -Werror needs to be disabled because EclipseLink's static weaving doesn't generate warning-free code
|
||||
// and during an IntelliJ recompilation, it fails
|
||||
//"-Werror",
|
||||
"-Xmaxwarns",
|
||||
"1500",
|
||||
"-Xmaxerrs",
|
||||
"1500"
|
||||
]
|
||||
// TODO FINERACT-959 (gradually) enable -Xlint:all (see "javac -help -X")
|
||||
|
||||
@@ -558,6 +578,9 @@ configure(project.fineractJavaProjects) {
|
||||
if (project.path == ':fineract-client') {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
if (project.path == ':fineract-client-feign') {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
disable(
|
||||
// TODO Remove disabled checks from this list, by fixing remaining usages
|
||||
"UnusedVariable",
|
||||
@@ -743,11 +766,13 @@ configure(project.fineractJavaProjects) {
|
||||
]
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc) {
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
options.encoding = 'UTF-8'
|
||||
// Ignore any errors during javadoc generation
|
||||
failOnError = false
|
||||
tasks.withType(Javadoc).tap {
|
||||
configureEach {
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
options.encoding = 'UTF-8'
|
||||
// Ignore any errors during javadoc generation
|
||||
failOnError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,17 +898,35 @@ configure(project.fineractPublishProjects) {
|
||||
}
|
||||
}
|
||||
|
||||
task printSourceSetInformation() {
|
||||
doLast{
|
||||
// Configuration for CycloneDX SBOM generator
|
||||
// https://github.com/CycloneDX/cyclonedx-gradle-plugin
|
||||
apply plugin: 'org.cyclonedx.bom'
|
||||
|
||||
cyclonedxBom {
|
||||
projectType = "application"
|
||||
includeBomSerialNumber = true
|
||||
includeLicenseText = true
|
||||
}
|
||||
|
||||
tasks.withType(org.cyclonedx.gradle.CyclonedxDirectTask).configureEach {
|
||||
includeMetadataResolution.set(false)
|
||||
}
|
||||
|
||||
tasks.register('printSourceSetInformation') {
|
||||
doLast {
|
||||
sourceSets.each { srcSet ->
|
||||
println "["+srcSet.name+"]"
|
||||
print "-->Source directories: "+srcSet.allJava.srcDirs+"\n"
|
||||
print "-->Output directories: "+srcSet.output.classesDirs.files+"\n"
|
||||
println "[" + srcSet.name + "]"
|
||||
print "-->Source directories: " + srcSet.allJava.srcDirs + "\n"
|
||||
print "-->Output directories: " + srcSet.output.classesDirs.files + "\n"
|
||||
print "-->Compile classpath:\n"
|
||||
srcSet.compileClasspath.files.each {
|
||||
print " "+it.path+"\n"
|
||||
print " " + it.path + "\n"
|
||||
}
|
||||
println ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Checkstyle).configureEach {
|
||||
exclude("**/*.avsc")
|
||||
}
|
||||
|
||||
@@ -41,6 +41,15 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
dependencySubstitution {
|
||||
// Substitution is to resolve CVE-2025-12183
|
||||
substitute module('org.lz4:lz4-java') using module('at.yawk.lz4:lz4-java:1.10.1')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.sun.activation:jakarta.activation'
|
||||
implementation 'com.sun.mail:jakarta.mail'
|
||||
|
||||
@@ -25,12 +25,12 @@ dependencyManagement {
|
||||
mavenBom 'com.squareup.okhttp3:okhttp-bom:4.12.0'
|
||||
mavenBom 'org.slf4j:slf4j-bom:2.0.17'
|
||||
mavenBom 'io.micrometer:micrometer-bom:1.13.6'
|
||||
mavenBom 'org.springframework.boot:spring-boot-dependencies:3.5.5'
|
||||
mavenBom 'org.springframework.boot:spring-boot-dependencies:3.5.6'
|
||||
mavenBom 'io.awspring.cloud:spring-cloud-aws-dependencies:3.2.1'
|
||||
mavenBom 'io.opentelemetry:opentelemetry-bom:1.44.1'
|
||||
mavenBom 'org.jetbrains.kotlin:kotlin-bom:2.0.21'
|
||||
mavenBom 'org.junit:junit-bom:5.11.3'
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.18.3'
|
||||
mavenBom 'com.fasterxml.jackson:jackson-bom:2.19.2'
|
||||
mavenBom 'io.cucumber:cucumber-bom:7.20.1'
|
||||
mavenBom 'org.mockito:mockito-bom:5.14.2'
|
||||
mavenBom 'software.amazon.awssdk:bom:2.29.9'
|
||||
@@ -44,8 +44,8 @@ dependencyManagement {
|
||||
// We do not use :+ to get the latest available version available on Maven Central, as that could suddenly break things.
|
||||
// We use the Renovate Bot to automatically propose Pull Requests (PRs) when upgrades for all of these versions are available.
|
||||
|
||||
dependency 'ch.qos.logback:logback-core:1.5.17'
|
||||
dependency 'ch.qos.logback:logback-classic:1.5.17'
|
||||
dependency 'ch.qos.logback:logback-core:1.5.19'
|
||||
dependency 'ch.qos.logback:logback-classic:1.5.19'
|
||||
dependency 'ch.qos.logback.contrib:logback-json-classic:0.1.5'
|
||||
dependency 'ch.qos.logback.contrib:logback-jackson:0.1.5'
|
||||
dependency 'org.codehaus.janino:janino:3.1.12'
|
||||
@@ -55,7 +55,7 @@ dependencyManagement {
|
||||
dependency 'com.google.code.gson:gson:2.11.0'
|
||||
dependency 'com.google.googlejavaformat:google-java-format:1.24.0'
|
||||
dependency 'org.apache.commons:commons-collections4:4.4'
|
||||
dependency 'org.apache.commons:commons-compress:1.26.0'
|
||||
dependency 'org.apache.commons:commons-compress:1.28.0'
|
||||
dependency ('software.amazon.msk:aws-msk-iam-auth:2.2.0') {
|
||||
exclude 'commons-logging:commons-logging:'
|
||||
}
|
||||
@@ -64,7 +64,7 @@ dependencyManagement {
|
||||
exclude 'javax.activation:activation'
|
||||
}
|
||||
dependency 'commons-io:commons-io:2.18.0'
|
||||
dependency 'com.github.librepdf:openpdf:2.0.3'
|
||||
dependency 'com.github.librepdf:openpdf:3.0.0'
|
||||
dependency ('org.mnode.ical4j:ical4j:3.2.19') {
|
||||
exclude 'com.sun.mail:javax.mail'
|
||||
exclude 'org.codehaus.groovy:groovy'
|
||||
@@ -74,17 +74,17 @@ dependencyManagement {
|
||||
dependency 'org.ehcache:ehcache:3.10.8'
|
||||
dependency 'com.github.spullara.mustache.java:compiler:0.9.14'
|
||||
dependency 'com.jayway.jsonpath:json-path:2.9.0'
|
||||
dependency ('org.apache.tika:tika-core:2.9.3') {
|
||||
dependency ('org.apache.tika:tika-core:3.2.3') {
|
||||
exclude 'commons-logging:commons-logging'
|
||||
}
|
||||
dependency ('org.apache.tika:tika-core:2.9.3') {
|
||||
dependency ('org.apache.tika:tika-core:3.2.3') {
|
||||
exclude 'commons-logging:commons-logging'
|
||||
}
|
||||
dependency ('org.apache.tika:tika-parser-miscoffice-module:2.9.3') {
|
||||
dependency ('org.apache.tika:tika-parser-miscoffice-module:3.2.3') {
|
||||
exclude 'org.bouncycastle:bcprov-jdk15on'
|
||||
exclude 'org.bouncycastle:bcmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcprov-jdk18on'
|
||||
exclude 'org.bouncycastle:bcmail-jdk18on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk18on'
|
||||
exclude 'commons-logging:commons-logging'
|
||||
exclude 'org.apache.logging.log4j:log4j-api'
|
||||
exclude 'org.slf4j:slf4j-api'
|
||||
@@ -97,11 +97,28 @@ dependencyManagement {
|
||||
exclude 'org.apache.commons:commons-compress'
|
||||
exclude 'xml-apis:xml-apis'
|
||||
}
|
||||
dependency ('org.apache.tika:tika-parser-microsoft-module:2.9.3') {
|
||||
dependency ('org.apache.tika:tika-parser-microsoft-module:3.2.3') {
|
||||
exclude 'org.bouncycastle:bcprov-jdk15on'
|
||||
exclude 'org.bouncycastle:bcmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcprov-jdk18on'
|
||||
exclude 'org.bouncycastle:bcmail-jdk18on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk18on'
|
||||
exclude 'commons-logging:commons-logging'
|
||||
exclude 'org.apache.logging.log4j:log4j-api'
|
||||
exclude 'org.slf4j:slf4j-api'
|
||||
exclude 'commons-io:commons-io'
|
||||
exclude 'commons-codec:commons-codec'
|
||||
exclude 'org.apache.commons:commons-lang3'
|
||||
exclude 'org.apache.poi:poi'
|
||||
exclude 'org.apache.poi:poi-scratchpad'
|
||||
exclude 'org.glassfish.jaxb:jaxb-runtime'
|
||||
exclude 'org.apache.commons:commons-compress'
|
||||
exclude 'xml-apis:xml-apis'
|
||||
}
|
||||
dependency ('org.apache.tika:tika-parser-image-module:3.2.3') {
|
||||
exclude 'org.bouncycastle:bcprov-jdk15on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk15on'
|
||||
exclude 'org.bouncycastle:bcprov-jdk18on'
|
||||
exclude 'org.bouncycastle:bcjmail-jdk18on'
|
||||
exclude 'commons-logging:commons-logging'
|
||||
exclude 'org.apache.logging.log4j:log4j-api'
|
||||
exclude 'org.slf4j:slf4j-api'
|
||||
@@ -152,14 +169,14 @@ dependencyManagement {
|
||||
dependency "commons-codec:commons-codec:1.17.1"
|
||||
dependency "org.projectlombok:lombok:1.18.36"
|
||||
|
||||
dependency 'org.bouncycastle:bcpkix-jdk18on:1.80'
|
||||
dependency 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
dependency 'org.bouncycastle:bcutil-jdk18on:1.80'
|
||||
dependency 'org.bouncycastle:bcpg-jdk18on:1.80'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk18on:1.81'
|
||||
dependency 'org.bouncycastle:bcprov-jdk18on:1.81'
|
||||
dependency 'org.bouncycastle:bcutil-jdk18on:1.81'
|
||||
dependency 'org.bouncycastle:bcpg-jdk18on:1.81'
|
||||
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit:7.2.0.202503040940-r'
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit.gpg.bc:7.2.0.202503040940-r'
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.2.0.202503040940-r'
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit:7.2.1.202505142326-r'
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit.gpg.bc:7.2.1.202505142326-r'
|
||||
dependency 'org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.2.1.202505142326-r'
|
||||
|
||||
dependency ('com.tmatesoft.svnkit:svnkit:1.10.12')
|
||||
dependency 'com.vdurmont:semver4j:3.1.0'
|
||||
@@ -185,7 +202,7 @@ dependencyManagement {
|
||||
|
||||
dependency 'jakarta.annotation:jakarta.annotation-api:3.0.0'
|
||||
dependency 'jakarta.activation:jakarta.activation-api:2.1.3'
|
||||
dependency ('com.sun.mail:jakarta.mail:2.0.1') {
|
||||
dependency ('com.sun.mail:jakarta.mail:2.0.2') {
|
||||
// Spring needs this version
|
||||
exclude 'com.sun.activation:jakarta.activation'
|
||||
}
|
||||
@@ -195,10 +212,10 @@ dependencyManagement {
|
||||
dependency 'jakarta.validation:jakarta.validation-api:3.1.1'
|
||||
dependency 'org.hibernate.validator:hibernate-validator:9.0.1.Final'
|
||||
|
||||
dependency ('org.liquibase:liquibase-core:4.31.1') {
|
||||
dependency ('org.liquibase:liquibase-core:4.33.0') {
|
||||
exclude 'javax.xml.bind:jaxb-api'
|
||||
}
|
||||
dependency 'org.liquibase.ext:liquibase-postgresql:4.31.1'
|
||||
dependency 'org.liquibase.ext:liquibase-postgresql:4.33.0'
|
||||
|
||||
dependency ('org.dom4j:dom4j:2.1.4') {
|
||||
exclude 'relaxngDatatype:relaxngDatatype' // already in com.sun.xml.bind:jaxb-osgi:2.3.0.1
|
||||
@@ -221,8 +238,8 @@ dependencyManagement {
|
||||
entry 'json-path'
|
||||
entry 'xml-path'
|
||||
}
|
||||
dependency 'org.apache.groovy:groovy-xml:4.0.26'
|
||||
dependency 'org.apache.groovy:groovy-json:4.0.26'
|
||||
dependency 'org.apache.groovy:groovy-xml:5.0.2'
|
||||
dependency 'org.apache.groovy:groovy-json:5.0.2'
|
||||
|
||||
dependency 'org.mapstruct:mapstruct:1.6.3'
|
||||
dependency 'org.mapstruct:mapstruct-processor:1.6.3'
|
||||
@@ -235,9 +252,9 @@ dependencyManagement {
|
||||
}
|
||||
dependency 'org.postgresql:postgresql:42.7.8'
|
||||
|
||||
dependency 'com.mysql:mysql-connector-j:9.2.0'
|
||||
dependency 'com.mysql:mysql-connector-j:9.3.0'
|
||||
|
||||
dependency 'org.assertj:assertj-core:3.26.3'
|
||||
dependency 'org.assertj:assertj-core:3.27.7'
|
||||
|
||||
dependency 'org.apache.commons:commons-math3:3.6.1'
|
||||
dependency 'commons-beanutils:commons-beanutils:1.11.0'
|
||||
@@ -265,6 +282,12 @@ dependencyManagement {
|
||||
|
||||
dependency 'com.lmax:disruptor:3.4.4'
|
||||
|
||||
dependency ('com.github.romankh3:image-comparison:4.4.0') {
|
||||
exclude 'org.junit.jupiter:junit-jupiter-api'
|
||||
exclude 'org.junit.jupiter:junit-jupiter-engine'
|
||||
exclude 'org.mockito:mockito-core'
|
||||
}
|
||||
|
||||
dependency 'com.ibm.icu:icu4j:76.1'
|
||||
dependency 'org.yakworks:spring-icu4j:0.4.2'
|
||||
dependency 'org.apache.commons:commons-lang3:3.18.0'
|
||||
@@ -273,9 +296,15 @@ dependencyManagement {
|
||||
dependency 'org.springframework:spring-core:6.2.11'
|
||||
// Force Spring Framework version: CVE-2025-41248
|
||||
dependency 'org.springframework.security:spring-security-core:6.5.4'
|
||||
// Force netty-codec version: CVE-2025-58057
|
||||
dependency 'io.netty:netty-codec:4.1.125.Final'
|
||||
// Force netty-codec version: CVE-2025-67735
|
||||
dependency 'io.netty:netty-codec:4.1.129.Final'
|
||||
// Force netty-codec version: CVE-2025-58056
|
||||
dependency 'io.netty:netty-codec-http:4.1.125.Final'
|
||||
dependency 'io.netty:netty-codec-http:4.1.129.Final'
|
||||
// Force lz4-java version: CVE-2025-12183
|
||||
dependency 'at.yawk.lz4:lz4-java:1.10.1'
|
||||
// Force tomcat-embed-core version: CVE-2025-24813
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-core:10.1.49'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-el:10.1.49'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:10.1.49'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,36 +196,10 @@ fineract {
|
||||
step10: [
|
||||
order: 10,
|
||||
description: 'Start voting on the dev mailing list',
|
||||
email: [
|
||||
from: "${findProperty('fineract.config.email')}",
|
||||
name: "${findProperty('fineract.config.name')}",
|
||||
to: "${findProperty('fineract.config.email')}",
|
||||
// to: 'dev@fineract.apache.org',
|
||||
mime: 'text/plain',
|
||||
subjectTemplate: [
|
||||
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step10.vote.subject.ftl"
|
||||
],
|
||||
messageTemplate: [
|
||||
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step10.vote.message.ftl"
|
||||
]
|
||||
]
|
||||
],
|
||||
step11: [
|
||||
order: 11,
|
||||
description: 'Conclude the vote and announce results',
|
||||
email: [
|
||||
from: "${findProperty('fineract.config.email')}",
|
||||
name: "${findProperty('fineract.config.name')}",
|
||||
to: "${findProperty('fineract.config.email')}",
|
||||
// to: 'dev@fineract.apache.org',
|
||||
mime: 'text/plain',
|
||||
subjectTemplate: [
|
||||
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step11.vote.subject.ftl"
|
||||
],
|
||||
messageTemplate: [
|
||||
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step11.vote.message.ftl"
|
||||
]
|
||||
]
|
||||
],
|
||||
step12: [
|
||||
order: 12,
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.gradle
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.filefilter.NotFileFilter
|
||||
import org.apache.commons.io.filefilter.PrefixFileFilter
|
||||
@@ -384,23 +383,7 @@ class FineractPlugin implements Plugin<Project> {
|
||||
doFirst {
|
||||
FineractPluginExtension.FineractPluginStep step = step(extension, "step10")
|
||||
|
||||
String version = project.properties?['fineract.release.version']
|
||||
|
||||
if(!version) {
|
||||
TextIO textIO = TextIoFactory.getTextIO();
|
||||
|
||||
version = textIO.newStringInputReader()
|
||||
.withPattern("\\d+.\\d+.\\d+")
|
||||
.read("Release Version");
|
||||
}
|
||||
|
||||
// TODO: input validation, see FINERACT-1610
|
||||
|
||||
this.context?.project?['fineract.release.version'] = version
|
||||
|
||||
if(step.email) {
|
||||
emailService.send( processEmailParams(step.email, this.context) )
|
||||
}
|
||||
printInstructions(project, "step10")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,28 +392,7 @@ class FineractPlugin implements Plugin<Project> {
|
||||
doFirst {
|
||||
FineractPluginExtension.FineractPluginStep step = step(extension, "step11")
|
||||
|
||||
String version = project.properties?['fineract.release.version']
|
||||
|
||||
if(!version) {
|
||||
TextIO textIO = TextIoFactory.getTextIO()
|
||||
|
||||
version = textIO.newStringInputReader()
|
||||
.withPattern("\\d+.\\d+.\\d+")
|
||||
.read("Release Version");
|
||||
}
|
||||
|
||||
// TODO: input validation, see FINERACT-1610
|
||||
|
||||
this.context?.project?['fineract.release.version'] = version
|
||||
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
def data = jsonSlurper.parse(new File("buildSrc/src/main/resources/vote/result.${version}.json"))
|
||||
|
||||
this.context?.project?['fineract.vote'] = data
|
||||
|
||||
if(step.email) {
|
||||
emailService.send( processEmailParams(step.email, this.context) )
|
||||
}
|
||||
printInstructions(project, "step11")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,36 +432,7 @@ class FineractPlugin implements Plugin<Project> {
|
||||
doFirst {
|
||||
log.warn("Release step 15: send email to announcement mailing list")
|
||||
|
||||
FineractPluginExtension.FineractPluginStep step = step(extension, "step15")
|
||||
|
||||
String version = project.properties?['fineract.release.version']
|
||||
|
||||
if(!version) {
|
||||
TextIO textIO = TextIoFactory.getTextIO()
|
||||
|
||||
version = textIO.newStringInputReader()
|
||||
.withPattern("\\d+.\\d+.\\d+")
|
||||
.read("Release Version");
|
||||
}
|
||||
|
||||
// TODO: input validation, see FINERACT-1610
|
||||
|
||||
FineractPluginExtension.FineractPluginJiraParams issues = jiraService.search(step.jira)
|
||||
|
||||
def versions = jiraService.getProjectVersions(step.jira.projectId)
|
||||
def filteredVersions = versions.findAll {
|
||||
log.warn(">>>> VERSION: ${it.id} - ${it.name} - ${it.description}")
|
||||
it.name == version
|
||||
}
|
||||
|
||||
this.context?.project?['fineract.release.version'] = version
|
||||
this.context?.project?['fineract.release.issues'] = issues.result
|
||||
this.context?.project?['fineract.release.projectId'] = step.jira.projectId
|
||||
this.context?.project?['fineract.release.versionId'] = filteredVersions[0]?.id
|
||||
|
||||
if(step.email) {
|
||||
emailService.send( processEmailParams(step.email, this.context) )
|
||||
}
|
||||
printInstructions(project, "step15")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<#--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
Hello everyone,
|
||||
|
||||
We are proud to present Apache Fineract ${project['fineract.release.version']}, with the artifacts below up for a vote. Releases are important for a number of reasons: They put a stamp of approval on a set of code changes and they build momentum for future improvements.
|
||||
|
||||
Release notes and ChangeLog: https://cwiki.apache.org/confluence/display/FINERACT/${project['fineract.release.version']}+-+Apache+Fineract
|
||||
|
||||
Source and binary artifacts: https://dist.apache.org/repos/dist/dev/fineract/${project['fineract.release.version']}/
|
||||
|
||||
Tagged as ${project['fineract.release.version']}
|
||||
|
||||
Committer PGP keys, including the release signing key: https://dist.apache.org/repos/dist/dev/fineract/KEYS
|
||||
|
||||
Note that this release candidate contains source and binary artifacts.
|
||||
|
||||
This vote will be open for 72 hours:
|
||||
|
||||
[ ] +1 approve
|
||||
[ ] +0 no opinion
|
||||
[ ] -1 disapprove (and reason why)
|
||||
|
||||
Please indicate if you are a binding vote (member of the PMC). Note: PMC members are required to download, compile, and test the artifacts before submitting their +1 vote.
|
||||
|
||||
Please also indicate with "Tested: YES/NO/PARTIAL" if you have locally built and/or tested these artifacts and/or a clone of the code checked out to the release commit, following the form:
|
||||
|
||||
Tested: YES ... Verified integrity and signatures of release artifacts locally, built from source, ran jar/war: Did everything mentioned in the current release candidate verification guidance ( https://fineract.apache.org/docs/rc/#_artifact_verification ). If you did more than that, please specify.
|
||||
|
||||
Tested: NO ... No testing performed on release candidate, e.g. relying on testing performed by other contributors and/or output of GitHub Actions, while exercising my right to vote.
|
||||
|
||||
Tested: PARTIAL ... Please specify.
|
||||
|
||||
Cheers,
|
||||
|
||||
${project['fineract.config.name']}
|
||||
@@ -1,89 +0,0 @@
|
||||
<#--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<#if (project['fineract.vote'].approve.binding?size + project['fineract.vote'].approve.nonBinding?size > project['fineract.vote'].disapprove.binding?size + project['fineract.vote'].disapprove.nonBinding?size)>
|
||||
Voting is now closed and has passed with the following tally,
|
||||
|
||||
Binding +1s: ${project['fineract.vote'].approve.binding?size}
|
||||
Non binding +1s: ${project['fineract.vote'].approve.nonBinding?size}
|
||||
<#else>
|
||||
Voting is now closed and has not passed with the following tally,
|
||||
|
||||
Binding +1s: ${project['fineract.vote'].approve.binding?size}
|
||||
Non binding +1s: ${project['fineract.vote'].approve.nonBinding?size}
|
||||
|
||||
Binding -1s: ${project['fineract.vote'].disapprove.binding?size}
|
||||
Non binding -1s: ${project['fineract.vote'].disapprove.nonBinding?size}
|
||||
</#if>
|
||||
|
||||
Here are the detailed results:
|
||||
|
||||
<#list project['fineract.vote'].approve.binding>
|
||||
Binding +1s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
|
||||
<#list project['fineract.vote'].approve.nonBinding>
|
||||
Non binding +1s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
|
||||
<#list project['fineract.vote'].disapprove.binding>
|
||||
Binding -1s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
<#list project['fineract.vote'].disapprove.nonBinding>
|
||||
Non binding -1s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
|
||||
<#list project['fineract.vote'].noOpinion.binding>
|
||||
Binding +0s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
<#list project['fineract.vote'].noOpinion.nonBinding>
|
||||
Non binding +0s:
|
||||
<#items as item>
|
||||
- ${item.name} (${item.email})
|
||||
</#items>
|
||||
</#list>
|
||||
|
||||
<#if (project['fineract.vote'].approve.binding?size + project['fineract.vote'].approve.nonBinding?size > project['fineract.vote'].disapprove.binding?size + project['fineract.vote'].disapprove.nonBinding?size)>
|
||||
Thanks to everyone who voted! I'll now continue with the rest of the release process.
|
||||
<#else>
|
||||
Thanks to everyone who voted! Looks like we have to repeat the vote.
|
||||
</#if>
|
||||
|
||||
${project['fineract.config.name']}
|
||||
@@ -1,43 +0,0 @@
|
||||
<#--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
The Apache Fineract project is pleased to announce
|
||||
the release of Apache Fineract ${project['fineract.release.version']}.
|
||||
The release is available for download from
|
||||
https://fineract.apache.org/#downloads
|
||||
|
||||
Fineract provides a reliable, robust, and affordable solution for entrepreneurs,
|
||||
financial institutions, and service providers to offer financial services to the
|
||||
world’s 2 billion underbanked and unbanked. Fineract is aimed at innovative mobile
|
||||
and cloud-based solutions, and enables digital transaction accounts for all.
|
||||
|
||||
This release addressed ${project['fineract.release.issues']?size} issues.
|
||||
|
||||
Readme: https://github.com/apache/fineract/blob/${project['fineract.release.version']}/README.md
|
||||
|
||||
Release page: https://cwiki.apache.org/confluence/display/FINERACT/${project['fineract.release.version']}+-+Apache+Fineract
|
||||
|
||||
List of fixed issues:
|
||||
https://issues.apache.org/jira/secure/ReleaseNote.jspa?version=${project['fineract.release.versionId']}&styleName=Html&projectId=${project['fineract.release.projectId']}
|
||||
|
||||
For more information on Apache Fineract please visit
|
||||
project home page: https://fineract.apache.org
|
||||
|
||||
The Apache Fineract Team
|
||||
+9
-1
@@ -18,4 +18,12 @@
|
||||
under the License.
|
||||
|
||||
-->
|
||||
[ANNOUNCE] Apache Fineract ${project['fineract.release.version']} Release
|
||||
|
||||
|
||||
[INSTRUCTIONS:START]
|
||||
|
||||
Initiate voting with the ATR (ASF Trusted Releases) tool.
|
||||
|
||||
https://release-test.apache.org
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
+9
-1
@@ -18,4 +18,12 @@
|
||||
under the License.
|
||||
|
||||
-->
|
||||
[FINERACT] [VOTE] 🗳️ ${project['fineract.release.version']} for release
|
||||
|
||||
|
||||
[INSTRUCTIONS:START]
|
||||
|
||||
Conclude voting with the ATR (ASF Trusted Releases) tool.
|
||||
|
||||
https://release-test.apache.org
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
Move the release candidate from the dev area to the release area using a Subversion server-side copy:
|
||||
|
||||
svn mv https://dist.apache.org/repos/dist/dev/fineract/${project['fineract.release.version']} https://dist.apache.org/repos/dist/release /fineract/
|
||||
# this is a remote operation
|
||||
svn mv -m "Release Fineract 1.14.0 🚢" https://dist.apache.org/repos/dist/dev/fineract/${project['fineract.release.version']} https://dist.apache.org/repos/dist/release/fineract/
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
|
||||
@@ -30,5 +30,6 @@ and make sure that everything on the release tag is merged to develop and that e
|
||||
>> git push origin develop
|
||||
>> git branch -D release/${project['fineract.release.version']}
|
||||
>> git push origin :release/${project['fineract.release.version']}
|
||||
>> git describe
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
|
||||
+9
-1
@@ -18,4 +18,12 @@
|
||||
under the License.
|
||||
|
||||
-->
|
||||
[FINERACT] [VOTE] [RESULT] 🧾️ ${project['fineract.release.version']} for release
|
||||
|
||||
|
||||
[INSTRUCTIONS:START]
|
||||
|
||||
Manually draft an email using your Apache ID.
|
||||
|
||||
See https://fineract.apache.org/docs/current/#_step_15_announcement_email
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
@@ -22,17 +22,6 @@
|
||||
|
||||
[INSTRUCTIONS:START]
|
||||
|
||||
Following are the typical things we need to verify before voting on a release candidate. And the release manager should verify them too before calling out a vote.
|
||||
|
||||
Make sure release artifacts are hosted at https://dist.apache.org/repos/dist/dev/fineract
|
||||
|
||||
* Release candidates should be in format apache-fineract-bin-${project['fineract.release.version']}.tar.gz
|
||||
* Verify signatures and hashes. You may have to import the public key of the release manager to verify the signatures. (gpg --import KEYS or gpg --recv-key <key id>)
|
||||
* Git tag matches the released bits (diff -rf)
|
||||
* Can compile docs and code successfully from source
|
||||
* Verify DISCLAIMER, NOTICE and LICENSE (year etc)
|
||||
* All files have correct headers (Rat check should be clean - gradlew rat)
|
||||
* No jar files in the source artifacts
|
||||
* Integration tests should work
|
||||
See https://fineract.apache.org/docs/current/#_step_9_verify_distribution_staging
|
||||
|
||||
[INSTRUCTIONS:END]
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"approve": {
|
||||
"binding": [
|
||||
{
|
||||
"name": "James Dailey",
|
||||
"email": "jdailey@apache.org"
|
||||
},
|
||||
{
|
||||
"name": "Victor Manuel Romero Rodriguez",
|
||||
"email": "victor.romero@fintecheando.mx"
|
||||
},
|
||||
{
|
||||
"name": "Terence Monteiro",
|
||||
"email": "terence.placker@gmail.com"
|
||||
}
|
||||
],
|
||||
"nonBinding": [
|
||||
{
|
||||
"name": "Adam Monsen",
|
||||
"email": "amonsen@mifos.org"
|
||||
},
|
||||
{
|
||||
"name": "Bharath Gowda",
|
||||
"email": "bgowda@mifos.org"
|
||||
},
|
||||
{
|
||||
"name": "Aleksandar Vidakovic",
|
||||
"email": "cheetah@monkeysintown.com"
|
||||
},
|
||||
{
|
||||
"name": "Ed Cable",
|
||||
"email": "edcable@mifos.org"
|
||||
},
|
||||
{
|
||||
"name": "Felix van Hove",
|
||||
"email": "fvanhove@gmx.de.invalid"
|
||||
},
|
||||
{
|
||||
"name": "Ahmed adel",
|
||||
"email": "a7med3del1973@gmail.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"disapprove": {
|
||||
"binding": [
|
||||
],
|
||||
"nonBinding": [
|
||||
]
|
||||
},
|
||||
"noOpinion": {
|
||||
"binding": [
|
||||
],
|
||||
"nonBinding": [
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@echo off
|
||||
echo Stopping any running Gradle daemons...
|
||||
call gradlew.bat --stop
|
||||
echo.
|
||||
echo Deleting corrupted build folders...
|
||||
if exist fineract-provider\build (
|
||||
echo Deleting fineract-provider\build...
|
||||
rmdir /s /q fineract-provider\build
|
||||
)
|
||||
if exist fineract-command\build (
|
||||
echo Deleting fineract-command\build...
|
||||
rmdir /s /q fineract-command\build
|
||||
)
|
||||
if exist fineract-core\build (
|
||||
echo Deleting fineract-core\build...
|
||||
rmdir /s /q fineract-core\build
|
||||
)
|
||||
if exist fineract-accounting\build (
|
||||
echo Deleting fineract-accounting\build...
|
||||
rmdir /s /q fineract-accounting\build
|
||||
)
|
||||
if exist fineract-charge\build (
|
||||
echo Deleting fineract-charge\build...
|
||||
rmdir /s /q fineract-charge\build
|
||||
)
|
||||
if exist fineract-cob\build (
|
||||
echo Deleting fineract-cob\build...
|
||||
rmdir /s /q fineract-cob\build
|
||||
)
|
||||
if exist fineract-document\build (
|
||||
echo Deleting fineract-document\build...
|
||||
rmdir /s /q fineract-document\build
|
||||
)
|
||||
if exist fineract-branch\build (
|
||||
echo Deleting fineract-branch\build...
|
||||
rmdir /s /q fineract-branch\build
|
||||
)
|
||||
if exist fineract-avro-schemas\build (
|
||||
echo Deleting fineract-avro-schemas\build...
|
||||
rmdir /s /q fineract-avro-schemas\build
|
||||
)
|
||||
echo.
|
||||
echo Build folders deleted successfully.
|
||||
echo Now you can run: .\gradlew devRun
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,116 @@
|
||||
> Task :buildSrc:extractPluginRequests UP-TO-DATE
|
||||
> Task :buildSrc:generatePluginAdapters UP-TO-DATE
|
||||
> Task :buildSrc:compileJava UP-TO-DATE
|
||||
> Task :buildSrc:compileGroovy UP-TO-DATE
|
||||
> Task :buildSrc:compileGroovyPlugins UP-TO-DATE
|
||||
> Task :buildSrc:pluginDescriptors UP-TO-DATE
|
||||
> Task :buildSrc:processResources UP-TO-DATE
|
||||
> Task :buildSrc:classes UP-TO-DATE
|
||||
> Task :buildSrc:jar UP-TO-DATE
|
||||
|
||||
> Configure project :
|
||||
matching ref: COMMIT - 011239f3995f9ea94a893ed4a60251a3b154564c
|
||||
ref configuration: COMMIT - pattern: null
|
||||
version: ${describe.tag.version.major}.${describe.tag.version.minor.next}.0-SNAPSHOT
|
||||
describeTagPattern: .*(\d+\.\d+\.\d+).*
|
||||
describeTagFirstParent: false
|
||||
|
||||
project version: 0.1.0-SNAPSHOT
|
||||
|
||||
> Task :fineract-core:processResources UP-TO-DATE
|
||||
> Task :fineract-tax:processResources UP-TO-DATE
|
||||
> Task :fineract-accounting:processResources UP-TO-DATE
|
||||
> Task :spotlessInternalRegisterDependencies UP-TO-DATE
|
||||
> Task :fineract-document:processResources NO-SOURCE
|
||||
> Task :fineract-avro-schemas:preprocessAvroSchemas UP-TO-DATE
|
||||
> Task :fineract-charge:processResources UP-TO-DATE
|
||||
> Task :fineract-branch:processResources UP-TO-DATE
|
||||
> Task :fineract-cob:processResources UP-TO-DATE
|
||||
> Task :fineract-rates:processResources UP-TO-DATE
|
||||
> Task :fineract-investor:processResources UP-TO-DATE
|
||||
> Task :fineract-mix:processResources NO-SOURCE
|
||||
> Task :fineract-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-loan-origination:processResources UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-security:processResources NO-SOURCE
|
||||
> Task :fineract-report:processResources UP-TO-DATE
|
||||
> Task :fineract-savings:processResources UP-TO-DATE
|
||||
> Task :fineract-working-capital-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-validation:compileJava UP-TO-DATE
|
||||
> Task :fineract-command:compileJava UP-TO-DATE
|
||||
> Task :fineract-validation:processResources UP-TO-DATE
|
||||
> Task :fineract-command:processResources UP-TO-DATE
|
||||
> Task :fineract-command:classes UP-TO-DATE
|
||||
> Task :fineract-validation:classes UP-TO-DATE
|
||||
> Task :fineract-command:jar UP-TO-DATE
|
||||
> Task :fineract-validation:jar UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:spotlessJson UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:spotlessJsonApply UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:buildJavaSdk UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:compileJava UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:processResources UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:classes UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:jar UP-TO-DATE
|
||||
> Task :fineract-working-capital-loan:compileJava UP-TO-DATE
|
||||
> Task :fineract-working-capital-loan:classes UP-TO-DATE
|
||||
> Task :fineract-working-capital-loan:jar UP-TO-DATE
|
||||
> Task :fineract-core:compileJava UP-TO-DATE
|
||||
> Task :fineract-core:classes UP-TO-DATE
|
||||
> Task :fineract-core:jar UP-TO-DATE
|
||||
> Task :fineract-mix:compileJava UP-TO-DATE
|
||||
> Task :fineract-mix:classes UP-TO-DATE
|
||||
> Task :fineract-mix:jar UP-TO-DATE
|
||||
> Task :fineract-cob:compileJava UP-TO-DATE
|
||||
> Task :fineract-cob:classes UP-TO-DATE
|
||||
> Task :fineract-report:compileJava UP-TO-DATE
|
||||
> Task :fineract-tax:compileJava UP-TO-DATE
|
||||
> Task :fineract-rates:compileJava UP-TO-DATE
|
||||
> Task :fineract-tax:classes UP-TO-DATE
|
||||
> Task :fineract-rates:classes UP-TO-DATE
|
||||
> Task :fineract-cob:jar UP-TO-DATE
|
||||
> Task :fineract-report:classes UP-TO-DATE
|
||||
> Task :fineract-tax:jar UP-TO-DATE
|
||||
> Task :fineract-rates:jar UP-TO-DATE
|
||||
> Task :fineract-report:jar UP-TO-DATE
|
||||
> Task :fineract-charge:compileJava UP-TO-DATE
|
||||
> Task :fineract-charge:classes UP-TO-DATE
|
||||
> Task :fineract-security:compileJava UP-TO-DATE
|
||||
> Task :fineract-charge:jar UP-TO-DATE
|
||||
> Task :fineract-security:classes UP-TO-DATE
|
||||
> Task :fineract-document:compileJava UP-TO-DATE
|
||||
> Task :fineract-document:classes UP-TO-DATE
|
||||
> Task :fineract-security:jar UP-TO-DATE
|
||||
> Task :fineract-document:jar UP-TO-DATE
|
||||
> Task :fineract-accounting:compileJava UP-TO-DATE
|
||||
> Task :fineract-accounting:classes UP-TO-DATE
|
||||
> Task :fineract-accounting:jar UP-TO-DATE
|
||||
> Task :fineract-branch:compileJava UP-TO-DATE
|
||||
> Task :fineract-branch:classes UP-TO-DATE
|
||||
> Task :fineract-branch:jar UP-TO-DATE
|
||||
> Task :fineract-savings:compileJava UP-TO-DATE
|
||||
> Task :fineract-loan:compileJava UP-TO-DATE
|
||||
> Task :fineract-loan:classes UP-TO-DATE
|
||||
> Task :fineract-savings:classes UP-TO-DATE
|
||||
> Task :fineract-loan:jar UP-TO-DATE
|
||||
> Task :fineract-savings:jar UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:compileJava UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:classes UP-TO-DATE
|
||||
> Task :fineract-loan-origination:compileJava UP-TO-DATE
|
||||
> Task :fineract-loan-origination:classes UP-TO-DATE
|
||||
> Task :fineract-investor:compileJava UP-TO-DATE
|
||||
> Task :fineract-investor:classes UP-TO-DATE
|
||||
> Task :fineract-loan-origination:jar UP-TO-DATE
|
||||
> Task :fineract-investor:jar UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:jar UP-TO-DATE
|
||||
> Task :fineract-provider:compileJava UP-TO-DATE
|
||||
|
||||
[Incubating] Problems report is available at: file:///D:/CompanyProject/GitCBS/JAVA-Backend-CBS-MAIN/build/reports/problems/problems-report.html
|
||||
|
||||
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
|
||||
|
||||
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
|
||||
|
||||
For more on this, please refer to https://docs.gradle.org/8.14.3/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
|
||||
|
||||
BUILD SUCCESSFUL in 23s
|
||||
71 actionable tasks: 71 up-to-date
|
||||
@@ -16,16 +16,15 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
fineract:
|
||||
# user: "${FINERACT_USER}:${FINERACT_GROUP}"
|
||||
image: ${IMAGE_NAME:-apache/fineract:latest}
|
||||
image: fineract:latest
|
||||
user: "${FINERACT_USER}:${FINERACT_GROUP}"
|
||||
volumes:
|
||||
- ${PWD}/config/docker/logback/logback-override.xml:/app/logback-override.xml
|
||||
- ${PWD}/config/docker/aws/etc/credentials:/etc/aws/credentials:ro
|
||||
- ${PWD}/build/fineract/logs:/var/logs/fineract:rw
|
||||
# - ${PWD}/build/fineract/tmp:/tmp:rw
|
||||
healthcheck:
|
||||
test: ["CMD", 'sh', '-c', 'echo -e "Checking for the availability of Fineract server deployment"; while ! nc -z "localhost" 8443; do sleep 1; printf "-"; done; echo -e " >> Fineract server has started";' ]
|
||||
timeout: 1s
|
||||
|
||||
+2
-1
@@ -54,6 +54,7 @@ FINERACT_MANAGEMENT_ENDPOINT_WEB_EXPOSURE_INCLUDE=health,info,prometheus
|
||||
FINERACT_MANAGEMENT_METRICS_TAGS_APPLICATION=fineract
|
||||
FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=true
|
||||
FINERACT_INSECURE_HTTP_CLIENT=true
|
||||
FINERACT_CONTENT_FILESYSTEM_ROOT_FOLDER=/tmp
|
||||
SPRING_PROFILES_ACTIVE=test,diagnostics
|
||||
OTEL_SERVICE_NAME=fineract
|
||||
JAVA_TOOL_OPTIONS="-Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
|
||||
JAVA_TOOL_OPTIONS="-Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5000 --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
|
||||
|
||||
Vendored
+12
-7
@@ -17,11 +17,16 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
KAFKA_CFG_NODE_ID=0
|
||||
KAFKA_CFG_PROCESS_ROLES=controller,broker
|
||||
KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
|
||||
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
|
||||
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
|
||||
KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
|
||||
ALLOW_PLAINTEXT_LISTENER=yes
|
||||
|
||||
KAFKA_NODE_ID=0
|
||||
KAFKA_PROCESS_ROLES=controller,broker
|
||||
KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
|
||||
KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
|
||||
KAFKA_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
|
||||
KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
|
||||
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1
|
||||
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1
|
||||
KAFKA_LOG_DIRS=/var/lib/kafka/data
|
||||
KAFKA_BROKER_ID=0
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
MARIADB_ROOT_PASSWORD=mysql
|
||||
@@ -50,8 +50,10 @@ scrape_configs:
|
||||
|
||||
# Override the global default and scrape targets from this job every 5 seconds.
|
||||
scrape_interval: 5s
|
||||
metrics_path: "/actuator/prometheus"
|
||||
metrics_path: "/fineract-provider/actuator/prometheus"
|
||||
scheme: "https"
|
||||
tls_config:
|
||||
insecure_skip_verify: true
|
||||
|
||||
static_configs:
|
||||
- targets: ["fineract-server:8443"]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
+1
-2
@@ -20,8 +20,7 @@ package com.acme.fineract.event.starter;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScans;
|
||||
|
||||
@AutoConfiguration
|
||||
@ComponentScans({ @ComponentScan("com.acme.fineract.event.externalevent") })
|
||||
@ComponentScan("com.acme.fineract.event.externalevent")
|
||||
public class AcmeEventAutoConfiguration {}
|
||||
|
||||
+1
-3
@@ -26,11 +26,9 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScans;
|
||||
|
||||
@AutoConfiguration
|
||||
@ComponentScans({ @ComponentScan("com.acme.fineract.loan.cob"), @ComponentScan("com.acme.fineract.loan.processor"),
|
||||
@ComponentScan("com.acme.fineract.loan.job") })
|
||||
@ComponentScan(basePackages = { "com.acme.fineract.loan.cob", "com.acme.fineract.loan.processor", "com.acme.fineract.loan.job" })
|
||||
@ConditionalOnProperty("acme.loan.enabled")
|
||||
public class AcmeLoanAutoConfiguration {
|
||||
|
||||
|
||||
+1
-2
@@ -51,7 +51,7 @@ public class AcmeBusinessStepDefinitions implements En {
|
||||
});
|
||||
|
||||
When("/^The user retrieves the step service with step class (.*) and name (.*)$/", (String stepClass, String stepName) -> {
|
||||
contextRunner.run((ctx) -> {
|
||||
contextRunner.run(ctx -> {
|
||||
this.businessStepService = ctx.getBean(COBBusinessStepService.class);
|
||||
|
||||
this.businessStep = ctx.getBean((Class<COBBusinessStep<Loan>>) Class.forName(stepClass));
|
||||
@@ -64,7 +64,6 @@ public class AcmeBusinessStepDefinitions implements En {
|
||||
Then("/^The step service should have a result$/", () -> {
|
||||
assertThat(this.businessStep).isNotNull();
|
||||
assertThat(this.result).isNotNull();
|
||||
// log.warn(">>>>>>>>>>>>>>>>>> RESULT: {}", this.result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
-16
@@ -19,9 +19,12 @@
|
||||
package com.acme.fineract.portfolio.note.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
|
||||
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
|
||||
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
|
||||
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
|
||||
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
|
||||
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
|
||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -38,27 +41,17 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult createNote(JsonCommand command) {
|
||||
public NoteCreateResponse createNote(NoteCreateRequest request) {
|
||||
throw new UnsupportedOperationException("createNote() is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLoanTransactionNote(Long loanTransactionId, String note) {
|
||||
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult updateNote(JsonCommand command) {
|
||||
public NoteUpdateResponse updateNote(NoteUpdateRequest request) {
|
||||
throw new UnsupportedOperationException("updateNote() is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult deleteNote(JsonCommand command) {
|
||||
public NoteDeleteResponse deleteNote(NoteDeleteRequest request) {
|
||||
throw new UnsupportedOperationException("deleteNote() is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAndPersistClientNote(Client client, JsonCommand command) {
|
||||
log.warn("createAndPersistClientNote() is intentionally left empty and does nothing.");
|
||||
}
|
||||
}
|
||||
|
||||
-12
@@ -21,7 +21,6 @@ package com.acme.fineract.portfolio.note.starter;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
@@ -29,7 +28,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -38,11 +36,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
public class TestDefaultConfiguration {
|
||||
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work
|
||||
|
||||
@Bean
|
||||
public FromJsonHelper fromJsonHelper() {
|
||||
return mock(FromJsonHelper.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
|
||||
return mock(RoutingDataSourceServiceFactory.class);
|
||||
@@ -82,9 +75,4 @@ public class TestDefaultConfiguration {
|
||||
public LoanTransactionRepository loanTransactionRepository() {
|
||||
return mock(LoanTransactionRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer(FromJsonHelper fromJsonHelper) {
|
||||
return new NoteCommandFromApiJsonDeserializer(fromJsonHelper);
|
||||
}
|
||||
}
|
||||
|
||||
-12
@@ -20,7 +20,6 @@ package com.acme.fineract.portfolio.note.starter;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
@@ -28,7 +27,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -37,11 +35,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
public class TestOverrideConfiguration {
|
||||
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work
|
||||
|
||||
@Bean
|
||||
public FromJsonHelper fromJsonHelper() {
|
||||
return mock(FromJsonHelper.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
|
||||
return mock(RoutingDataSourceServiceFactory.class);
|
||||
@@ -81,9 +74,4 @@ public class TestOverrideConfiguration {
|
||||
public LoanTransactionRepository loanTransactionRepository() {
|
||||
return mock(LoanTransactionRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer(FromJsonHelper fromJsonHelper) {
|
||||
return new NoteCommandFromApiJsonDeserializer(fromJsonHelper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
-- Complete migration: Add all custom/enhanced fields to m_savings_product table
|
||||
-- Uses IF NOT EXISTS to safely handle columns that may already exist
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Scalar custom columns
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='account_type') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN account_type VARCHAR(50) DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='customer_segment') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN customer_segment VARCHAR(50) DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='product_code') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN product_code VARCHAR(20) DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='effective_date') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN effective_date DATE DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='effective_to') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN effective_to DATE DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='end_date') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN end_date DATE DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='introducer_required') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN introducer_required BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='share_linking_enabled') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN share_linking_enabled BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='nomination_facility') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN nomination_facility BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='dormancy_alert') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN dormancy_alert BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='interest_calculate_selector') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN interest_calculate_selector INT DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='average_period') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN average_period VARCHAR(20) DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='auto_post') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN auto_post BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='frequency_fix_day') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN frequency_fix_day INT DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='last_interest_date') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN last_interest_date DATE DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='enable_lockin_period') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN enable_lockin_period BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='balance_frequency') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN balance_frequency VARCHAR(50) DEFAULT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='m_savings_product' AND column_name='account_closed_zero_balance_in_day_closing') THEN
|
||||
ALTER TABLE m_savings_product ADD COLUMN account_closed_zero_balance_in_day_closing BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add comments for documentation (safe to run multiple times)
|
||||
COMMENT ON COLUMN m_savings_product.account_type IS 'Account type classification';
|
||||
COMMENT ON COLUMN m_savings_product.customer_segment IS 'Customer segment classification';
|
||||
COMMENT ON COLUMN m_savings_product.product_code IS 'Unique product code identifier';
|
||||
COMMENT ON COLUMN m_savings_product.effective_date IS 'Date from which the product becomes effective';
|
||||
COMMENT ON COLUMN m_savings_product.effective_to IS 'Date until which the product is effective';
|
||||
COMMENT ON COLUMN m_savings_product.end_date IS 'Product end/expiry date';
|
||||
COMMENT ON COLUMN m_savings_product.introducer_required IS 'Whether an introducer is required for this product';
|
||||
COMMENT ON COLUMN m_savings_product.share_linking_enabled IS 'Whether share linking is enabled for this product';
|
||||
COMMENT ON COLUMN m_savings_product.nomination_facility IS 'Whether nomination facility is available';
|
||||
COMMENT ON COLUMN m_savings_product.dormancy_alert IS 'Whether dormancy alerts are enabled';
|
||||
COMMENT ON COLUMN m_savings_product.interest_calculate_selector IS 'Interest calculation selector type';
|
||||
COMMENT ON COLUMN m_savings_product.average_period IS 'Average period for calculations';
|
||||
COMMENT ON COLUMN m_savings_product.auto_post IS 'Whether auto posting of interest is enabled';
|
||||
COMMENT ON COLUMN m_savings_product.frequency_fix_day IS 'Fixed day for frequency calculations';
|
||||
COMMENT ON COLUMN m_savings_product.last_interest_date IS 'Last date interest was calculated';
|
||||
COMMENT ON COLUMN m_savings_product.enable_lockin_period IS 'Whether lock-in period is enabled';
|
||||
COMMENT ON COLUMN m_savings_product.balance_frequency IS 'Frequency for balance calculation (e.g., Daily, Monthly, Quarterly)';
|
||||
COMMENT ON COLUMN m_savings_product.account_closed_zero_balance_in_day_closing IS 'Flag to indicate if account should be closed when balance reaches zero at day end';
|
||||
@@ -0,0 +1,173 @@
|
||||
-- ============================================================
|
||||
-- Facility Charge Auto-Seed Migration
|
||||
-- Step 1: Create mapping table
|
||||
-- Step 2: Insert charges into m_charge
|
||||
-- Step 3: Insert mapping with charge IDs
|
||||
-- charge_applies_to_enum: 2=SAVINGS, 5=CURRENT_ACCOUNT
|
||||
-- charge_time_enum: 3=SAVINGS_ACTIVATION, 6=ANNUAL_FEE, 7=MONTHLY_FEE
|
||||
-- charge_calculation_enum: 1=FLAT
|
||||
-- ============================================================
|
||||
|
||||
-- Step 1: Mapping table
|
||||
CREATE TABLE IF NOT EXISTS m_facility_charge_mapping (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
facility_type VARCHAR(60) NOT NULL UNIQUE,
|
||||
charge_id BIGINT,
|
||||
charge_name VARCHAR(200) NOT NULL,
|
||||
default_amount DECIMAL(19,6) NOT NULL DEFAULT 0.000000,
|
||||
charge_time_type INT NOT NULL DEFAULT 3,
|
||||
charge_applies_to INT NOT NULL DEFAULT 2,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
FOREIGN KEY (charge_id) REFERENCES m_charge(id)
|
||||
);
|
||||
|
||||
-- Step 2: Insert charges into m_charge (only if not already exists)
|
||||
-- income_or_liability_account_id = first available INCOME DETAIL GL account
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'ATM Facility Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'ATM Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'ATM Withdrawal Charge', 'INR', 20.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'ATM Withdrawal Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'UPI Facility Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'UPI Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'POS Transaction Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'POS Transaction Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Cheque Book Charge', 'INR', 300.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Cheque Book Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'IMPS Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'IMPS Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'SMS Alert Charge', 'INR', 15.00, 2, 7, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'SMS Alert Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'BBPS Facility Charge', 'INR', 2.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'BBPS Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Insurance Cover Charge', 'INR', 500.00, 2, 6, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Insurance Cover Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'BEPG Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'BEPG Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'NEFT Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'RTGS Facility Charge', 'INR', 25.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Inter-Bank Cap Charge', 'INR', 0.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Withdrawal Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Deposit KYC Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0);
|
||||
|
||||
INSERT IGNORE INTO m_charge
|
||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
||||
SELECT 'Virtual Debit Card Charge', 'INR', 199.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0);
|
||||
|
||||
-- Step 3: Insert mapping linking facility_type → charge_id
|
||||
INSERT IGNORE INTO m_facility_charge_mapping
|
||||
(facility_type, charge_name, default_amount, charge_time_type, charge_applies_to, charge_id)
|
||||
VALUES
|
||||
('ATM_FACILITY', 'ATM Facility Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'ATM Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('ATM_WITHDRAWAL', 'ATM Withdrawal Charge', 20.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'ATM Withdrawal Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('UPI_FACILITY', 'UPI Facility Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'UPI Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('POS_TRANSACTIONS', 'POS Transaction Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'POS Transaction Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('CHEQUE_BOOK', 'Cheque Book Charge', 300.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Cheque Book Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('IMPS_FACILITY', 'IMPS Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'IMPS Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('SMS_ALERTS', 'SMS Alert Charge', 15.00, 7, 2, (SELECT id FROM m_charge WHERE name = 'SMS Alert Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('BBPS_FACILITY', 'BBPS Facility Charge', 2.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'BBPS Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('INSURANCE_COVER', 'Insurance Cover Charge', 500.00, 6, 2, (SELECT id FROM m_charge WHERE name = 'Insurance Cover Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('BEPG_FACILITY', 'BEPG Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'BEPG Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('NEFT_FACILITY', 'NEFT Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('RTGS_FACILITY', 'RTGS Facility Charge', 25.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('INTER_BANK_TOTAL_CAP','Inter-Bank Cap Charge', 0.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('WITHDRAWAL_FACILITY', 'Withdrawal Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('DEPOSIT_FACILITY_KYC','Deposit KYC Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0 LIMIT 1)),
|
||||
('VIRTUAL_DEBIT_CARD', 'Virtual Debit Card Charge', 199.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0 LIMIT 1));
|
||||
@@ -0,0 +1,79 @@
|
||||
-- Migration: Add facility charge columns to product facility config tables
|
||||
-- Each facility (ATM, UPI, IMPS, etc.) links to an existing m_charge entry
|
||||
-- When account is activated, enabled facilities auto-apply their linked charge
|
||||
|
||||
-- Current Product Facility Config
|
||||
ALTER TABLE m_current_product_facility_config
|
||||
ADD COLUMN IF NOT EXISTS atm_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS atm_withdrawal_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS upi_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS pos_transactions_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS cheque_book_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS imps_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS sms_alerts_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS bbps_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS insurance_cover_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS bepg_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
||||
|
||||
-- Foreign key constraints for current product
|
||||
ALTER TABLE m_current_product_facility_config
|
||||
ADD CONSTRAINT fk_cpfc_atm_charge FOREIGN KEY (atm_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_atm_wd_charge FOREIGN KEY (atm_withdrawal_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_upi_charge FOREIGN KEY (upi_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_pos_charge FOREIGN KEY (pos_transactions_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_chq_charge FOREIGN KEY (cheque_book_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_imps_charge FOREIGN KEY (imps_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_sms_charge FOREIGN KEY (sms_alerts_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_bbps_charge FOREIGN KEY (bbps_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_ins_charge FOREIGN KEY (insurance_cover_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_bepg_charge FOREIGN KEY (bepg_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_cpfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
||||
|
||||
-- Savings Product Facility Config
|
||||
ALTER TABLE m_savings_product_facility_config
|
||||
ADD COLUMN IF NOT EXISTS atm_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS atm_withdrawal_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS upi_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS pos_transactions_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS cheque_book_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS imps_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS sms_alerts_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS bbps_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS insurance_cover_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS bepg_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
||||
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
||||
|
||||
-- Foreign key constraints for savings product
|
||||
ALTER TABLE m_savings_product_facility_config
|
||||
ADD CONSTRAINT fk_spfc_atm_charge FOREIGN KEY (atm_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_atm_wd_charge FOREIGN KEY (atm_withdrawal_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_upi_charge FOREIGN KEY (upi_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_pos_charge FOREIGN KEY (pos_transactions_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_chq_charge FOREIGN KEY (cheque_book_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_imps_charge FOREIGN KEY (imps_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_sms_charge FOREIGN KEY (sms_alerts_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_bbps_charge FOREIGN KEY (bbps_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_ins_charge FOREIGN KEY (insurance_cover_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_bepg_charge FOREIGN KEY (bepg_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
||||
ADD CONSTRAINT fk_spfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
||||
@@ -16,7 +16,6 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
version: "3.8"
|
||||
services:
|
||||
# Frontend service
|
||||
community-app:
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#
|
||||
|
||||
# You can replace and test a more recent version of docker compose
|
||||
version: "3.8"
|
||||
services:
|
||||
db:
|
||||
extends:
|
||||
|
||||
@@ -27,8 +27,6 @@ x-logging: &default-logging
|
||||
- regex:
|
||||
expression: '^(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}.\d{3}) (?P<message>(?s:.*))$$'
|
||||
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
loki:
|
||||
extends:
|
||||
@@ -88,3 +86,7 @@ services:
|
||||
# - ./config/docker/env/cloudwatch.env
|
||||
- ./config/docker/env/debug.env
|
||||
logging: *default-logging
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- fineract-server
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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:
|
||||
mariadb:
|
||||
container_name: mariadb
|
||||
image: mariadb:11.4
|
||||
volumes:
|
||||
- ./config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
|
||||
- ./config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
|
||||
restart: always
|
||||
env_file:
|
||||
- ./config/docker/env/mariadb.env
|
||||
healthcheck:
|
||||
test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
ports:
|
||||
- "3306:3306"
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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:
|
||||
mysql:
|
||||
container_name: mysql
|
||||
image: mysql:8
|
||||
volumes:
|
||||
- ${PWD}/config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
|
||||
- ${PWD}/config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
|
||||
restart: always
|
||||
env_file:
|
||||
- ${PWD}/config/docker/env/mysql.env
|
||||
healthcheck:
|
||||
test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
ports:
|
||||
- "3306:3306"
|
||||
@@ -20,7 +20,7 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
kafka:
|
||||
image: "bitnamilegacy/kafka:4.0.0-debian-12-r10"
|
||||
image: "apache/kafka:4.1.1-rc2"
|
||||
ports:
|
||||
- "9092:9092"
|
||||
env_file:
|
||||
|
||||
+1
-2
@@ -16,8 +16,6 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
@@ -31,6 +29,7 @@ services:
|
||||
service: fineract
|
||||
ports:
|
||||
- "8443:8443"
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Builds the cbsnexora-dsc-helper.jar consumed by the JNLP from /api/v1/dsc/helper.jar.
|
||||
*
|
||||
* Targets Java 1.8 so the broadest set of client JREs (incl. legacy OpenWebStart installs)
|
||||
* can launch the helper. The JAR is a "fat" jar with Gson shaded in — JNLP <resources> can only
|
||||
* list one <jar href="..."> cleanly, and avoiding network round-trips for tiny Gson is a win.
|
||||
*/
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'com.cbsnexora'
|
||||
version = '1.0.0'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveBaseName = 'cbsnexora-dsc-helper'
|
||||
archiveClassifier = ''
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
'Main-Class' : 'com.cbsnexora.dsc.CbsnexoraDscHelper',
|
||||
'Permissions' : 'all-permissions',
|
||||
'Application-Name' : 'CBSnexora DSC Helper',
|
||||
'Codebase' : '*',
|
||||
'Implementation-Version': project.version
|
||||
)
|
||||
}
|
||||
|
||||
// Shade Gson into the same jar so JNLP only needs one <jar> entry.
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from {
|
||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
}
|
||||
// Strip signatures from shaded deps so re-signing the fat jar is clean.
|
||||
exclude 'META-INF/*.SF'
|
||||
exclude 'META-INF/*.DSA'
|
||||
exclude 'META-INF/*.RSA'
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# Generate a realistic self-signed DSC for testing the CBSnexora helper.
|
||||
#
|
||||
# Replaces the UUID-CN bare-bones dev cert with one that has:
|
||||
# - Real-looking subject DN: CN=Test User, O=CBSnexora Pvt Ltd, OU=IT, L=Mumbai, ST=Maharashtra, C=IN
|
||||
# - KeyUsage = DigitalSignature (matches real Indian Class 3 DSCs)
|
||||
# - Strong private-key protection enabled — Windows pops a PIN dialog at creation time, then
|
||||
# prompts you for that PIN on every signing operation (matches the UX of a real USB token).
|
||||
#
|
||||
# Run from PowerShell (no elevation needed; user store only):
|
||||
# .\generate-test-dsc.ps1
|
||||
#
|
||||
# Why certreq + INF instead of New-SelfSignedCertificate:
|
||||
# New-SelfSignedCertificate (Windows PowerShell 5.x) doesn't expose ProtectKey / strong-protection
|
||||
# knobs — it creates a CNG/KSP key that won't prompt. certreq.exe with an INF using the legacy
|
||||
# "Microsoft Enhanced RSA and AES Cryptographic Provider" CSP IS the supported path for forcing
|
||||
# user-confirmation on key access.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "=== Step 1: Clean up old UUID-CN test certs from Windows-MY ===" -ForegroundColor Cyan
|
||||
$old = Get-ChildItem Cert:\CurrentUser\My | Where-Object {
|
||||
$_.Subject -match "^CN=[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
||||
}
|
||||
if ($old) {
|
||||
Write-Host "Found $($old.Count) old UUID-CN cert(s) - removing:" -ForegroundColor Yellow
|
||||
foreach ($c in $old) {
|
||||
Write-Host " $($c.Thumbprint) $($c.Subject)"
|
||||
Remove-Item "Cert:\CurrentUser\My\$($c.Thumbprint)" -Force
|
||||
}
|
||||
} else {
|
||||
Write-Host "No old UUID-CN test certs in the user store."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Step 2: Build INF for certreq ===" -ForegroundColor Cyan
|
||||
|
||||
# Random container name avoids collisions when the script is re-run before old containers expire.
|
||||
$containerName = "CBSnexora-DSC-Test-$(Get-Random -Maximum 999999)"
|
||||
$inf = @"
|
||||
[Version]
|
||||
Signature = "`$Windows NT`$"
|
||||
|
||||
[NewRequest]
|
||||
Subject = "CN=Test User, OU=IT Department, O=CBSnexora Pvt Ltd, L=Mumbai, ST=Maharashtra, C=IN"
|
||||
KeyAlgorithm = RSA
|
||||
KeyLength = 2048
|
||||
KeyUsage = "CERT_DIGITAL_SIGNATURE_KEY_USAGE"
|
||||
MachineKeySet = false
|
||||
KeyContainer = "$containerName"
|
||||
ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
|
||||
KeySpec = AT_SIGNATURE
|
||||
ProtectKey = true
|
||||
SMIME = false
|
||||
RequestType = Cert
|
||||
Exportable = false
|
||||
HashAlgorithm = SHA256
|
||||
ValidityPeriod = Years
|
||||
ValidityPeriodUnits = 2
|
||||
|
||||
[Extensions]
|
||||
2.5.29.37 = "{text}1.3.6.1.5.5.7.3.4"
|
||||
"@
|
||||
|
||||
$infPath = Join-Path $env:TEMP "cbsnexora-test-dsc.inf"
|
||||
$cerPath = Join-Path $env:TEMP "cbsnexora-test-dsc.cer"
|
||||
$inf | Out-File -FilePath $infPath -Encoding ASCII -Force
|
||||
Write-Host "INF written to $infPath"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Step 3: certreq -new ===" -ForegroundColor Cyan
|
||||
Write-Host "WINDOWS WILL POP A DIALOG asking 'Creating an RSA exchange key':" -ForegroundColor Yellow
|
||||
Write-Host " -> Click 'Set Security Level...'" -ForegroundColor Yellow
|
||||
Write-Host " -> Select 'High'" -ForegroundColor Yellow
|
||||
Write-Host " -> Click 'Next', then set a PIN you'll remember" -ForegroundColor Yellow
|
||||
Write-Host " -> Click 'Finish' then 'OK'" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
& certreq.exe -new $infPath $cerPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "certreq -new failed with exit code $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Step 4: New cert summary ===" -ForegroundColor Cyan
|
||||
$new = Get-ChildItem Cert:\CurrentUser\My | Where-Object {
|
||||
$_.Subject -like "*CN=Test User*"
|
||||
} | Sort-Object NotBefore -Descending | Select-Object -First 1
|
||||
|
||||
if ($new) {
|
||||
Write-Host "Subject: $($new.Subject)" -ForegroundColor Green
|
||||
Write-Host "Issuer: $($new.Issuer)"
|
||||
Write-Host "Serial: $($new.SerialNumber)"
|
||||
Write-Host "Thumbprint: $($new.Thumbprint)"
|
||||
Write-Host "Valid From: $($new.NotBefore)"
|
||||
Write-Host "Valid Until: $($new.NotAfter)"
|
||||
Write-Host "KeyUsage: $($new.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.15' } | ForEach-Object { $_.KeyUsages })"
|
||||
} else {
|
||||
Write-Warning "Cert created but not findable - check certmgr.msc -> Personal -> Certificates"
|
||||
}
|
||||
|
||||
# Cleanup temp files
|
||||
Remove-Item $infPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $cerPath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Done ===" -ForegroundColor Green
|
||||
Write-Host "Next:"
|
||||
Write-Host " 1. Clear JWS cache:"
|
||||
Write-Host " Remove-Item -Recurse -Force `"`$env:USERPROFILE\.cache\icedtea-web`""
|
||||
Write-Host " 2. Trigger DSC enroll from the web UI."
|
||||
Write-Host " 3. Pick 'Test User' from the cert table - now shows real Holder Name and Organisation."
|
||||
Write-Host " 4. Windows pops the PIN prompt; enter the PIN you set above. Helper proceeds to enroll."
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Standalone Gradle project for the CBSnexora DSC helper JAR. Intentionally NOT included in the
|
||||
* root fineract settings.gradle — different Java target (1.8) and a tiny dependency tree.
|
||||
*
|
||||
* Build:
|
||||
* cd dsc-helper && gradle clean build
|
||||
* → dsc-helper/build/libs/cbsnexora-dsc-helper.jar (fat jar with Gson embedded)
|
||||
*
|
||||
* Sign (self-signed for now; replace cert later without rebuilding):
|
||||
* keytool -genkeypair -alias cbsnexora -keyalg RSA -keysize 2048 \
|
||||
* -validity 730 -keystore cbsnexora-keystore.jks
|
||||
* jarsigner -keystore cbsnexora-keystore.jks build/libs/cbsnexora-dsc-helper.jar cbsnexora
|
||||
*
|
||||
* Deploy:
|
||||
* cp build/libs/cbsnexora-dsc-helper.jar ~/.cbsnexora/dsc-helper.jar
|
||||
* (path is configurable via fineract.dsc.helper.jar.path; default is ~/.cbsnexora/dsc-helper.jar)
|
||||
*
|
||||
* Fineract will then stream it on demand at GET /api/v1/dsc/helper.jar — the JNLP <jar href="..."/>
|
||||
* already points at that URL.
|
||||
*/
|
||||
rootProject.name = 'cbsnexora-dsc-helper'
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package com.cbsnexora.dsc;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Font;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Window;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.SwingConstants;
|
||||
|
||||
/**
|
||||
* Shared branding chrome for every CBSnexora DSC helper window — logo header bar, primary/secondary
|
||||
* button styling, palette constants. Centralised so the look is identical across the token picker
|
||||
* and the certificate selection screen, and so a future logo swap is one file change.
|
||||
*/
|
||||
final class Branding {
|
||||
|
||||
/** Brand colours sampled from cbsnexora-logo.png. */
|
||||
static final Color ACCENT_BLUE = new Color(0x2BAEEC);
|
||||
static final Color ACCENT_BLUE_DARK = new Color(0x1E8FC4);
|
||||
static final Color TITLE_DARK = new Color(0x1F2D3D);
|
||||
static final Color SUBTITLE_GREY = new Color(0x6B7280);
|
||||
static final Color HEADER_BG = Color.WHITE;
|
||||
static final Color BODY_BG = new Color(0xF7F9FC);
|
||||
static final Color DIVIDER = new Color(0xE2E8F0);
|
||||
static final Color BUTTON_NEUTRAL_BG = new Color(0xF1F5F9);
|
||||
static final Color BUTTON_NEUTRAL_FG = new Color(0x334155);
|
||||
|
||||
private static final String LOGO_RESOURCE = "/cbsnexora-logo.png";
|
||||
private static final int HEADER_LOGO_HEIGHT = 36;
|
||||
|
||||
private Branding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Header bar: logo on the left, title + subtitle in the middle, a thin divider at the bottom.
|
||||
* Title falls back to a plain text wordmark if the logo resource can't be loaded — keeps the
|
||||
* UI usable on a stripped-down JRE that didn't bundle the resources jar entry for some reason.
|
||||
*/
|
||||
static JPanel buildHeader(final String title, final String subtitle) {
|
||||
final JPanel header = new JPanel(new BorderLayout());
|
||||
header.setBackground(HEADER_BG);
|
||||
header.setBorder(BorderFactory.createEmptyBorder(14, 18, 14, 18));
|
||||
|
||||
final JPanel row = new JPanel(new GridBagLayout());
|
||||
row.setOpaque(false);
|
||||
final GridBagConstraints g = new GridBagConstraints();
|
||||
g.gridy = 0;
|
||||
g.anchor = GridBagConstraints.WEST;
|
||||
g.insets = new Insets(0, 0, 0, 14);
|
||||
|
||||
final JLabel logo = loadLogo();
|
||||
if (logo != null) {
|
||||
g.gridx = 0;
|
||||
row.add(logo, g);
|
||||
}
|
||||
|
||||
final JPanel text = new JPanel();
|
||||
text.setOpaque(false);
|
||||
text.setLayout(new javax.swing.BoxLayout(text, javax.swing.BoxLayout.Y_AXIS));
|
||||
|
||||
final JLabel titleLabel = new JLabel(title != null ? title : "CBSnexora");
|
||||
titleLabel.setForeground(TITLE_DARK);
|
||||
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f));
|
||||
titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
text.add(titleLabel);
|
||||
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
final JLabel sub = new JLabel(subtitle);
|
||||
sub.setForeground(SUBTITLE_GREY);
|
||||
sub.setFont(sub.getFont().deriveFont(Font.PLAIN, 12f));
|
||||
sub.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
sub.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
|
||||
text.add(sub);
|
||||
}
|
||||
|
||||
g.gridx = 1;
|
||||
g.insets = new Insets(0, 0, 0, 0);
|
||||
g.weightx = 1.0;
|
||||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
row.add(text, g);
|
||||
|
||||
header.add(row, BorderLayout.CENTER);
|
||||
|
||||
final JSeparator divider = new JSeparator(SwingConstants.HORIZONTAL);
|
||||
divider.setForeground(DIVIDER);
|
||||
divider.setBackground(DIVIDER);
|
||||
header.add(divider, BorderLayout.SOUTH);
|
||||
return header;
|
||||
}
|
||||
|
||||
/** Primary CTA — solid accent fill, white text. */
|
||||
static JButton primaryButton(final String text) {
|
||||
final JButton b = new JButton(text);
|
||||
b.setBackground(ACCENT_BLUE);
|
||||
b.setForeground(Color.WHITE);
|
||||
b.setFocusPainted(false);
|
||||
b.setBorderPainted(false);
|
||||
b.setOpaque(true);
|
||||
b.setFont(b.getFont().deriveFont(Font.BOLD, 13f));
|
||||
b.setBorder(BorderFactory.createEmptyBorder(8, 18, 8, 18));
|
||||
b.addPropertyChangeListener("enabled", e -> {
|
||||
b.setBackground(b.isEnabled() ? ACCENT_BLUE : ACCENT_BLUE_DARK.darker());
|
||||
b.setForeground(b.isEnabled() ? Color.WHITE : new Color(0xD0D5DB));
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Secondary button — light fill, slate text. Used for Cancel and the like. */
|
||||
static JButton secondaryButton(final String text) {
|
||||
final JButton b = new JButton(text);
|
||||
b.setBackground(BUTTON_NEUTRAL_BG);
|
||||
b.setForeground(BUTTON_NEUTRAL_FG);
|
||||
b.setFocusPainted(false);
|
||||
b.setBorderPainted(false);
|
||||
b.setOpaque(true);
|
||||
b.setFont(b.getFont().deriveFont(Font.PLAIN, 13f));
|
||||
b.setBorder(BorderFactory.createEmptyBorder(8, 18, 8, 18));
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Set the same logo as the window icon so the taskbar / titlebar stays branded. */
|
||||
static void applyWindowIcon(final Window window) {
|
||||
final URL url = Branding.class.getResource(LOGO_RESOURCE);
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.setIconImage(ImageIO.read(url));
|
||||
} catch (final IOException ignored) {
|
||||
// Non-fatal — falls back to the JVM default icon.
|
||||
}
|
||||
}
|
||||
|
||||
private static JLabel loadLogo() {
|
||||
final URL url = Branding.class.getResource(LOGO_RESOURCE);
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final Image raw = ImageIO.read(url);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
// Scale to a fixed header height preserving aspect ratio. SCALE_SMOOTH for the text in
|
||||
// the wordmark — the bitmap render is otherwise legibly poor at small heights.
|
||||
final int targetH = HEADER_LOGO_HEIGHT;
|
||||
final int targetW = Math.max(1, raw.getWidth(null) * targetH / Math.max(1, raw.getHeight(null)));
|
||||
final Image scaled = raw.getScaledInstance(targetW, targetH, Image.SCALE_SMOOTH);
|
||||
return new JLabel(new ImageIcon(scaled));
|
||||
} catch (final IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package com.cbsnexora.dsc;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Entry point launched by Java Web Start. Reads its single argument as JSON:
|
||||
* <pre>
|
||||
* {
|
||||
* "randomNo": "...",
|
||||
* "mode": "ENROLL" | "SIGN_CHALLENGE",
|
||||
* "callbackUrl": "https://.../api/v1/dsc/{enroll|login/dsc}/{rn}/{callback|verify}",
|
||||
* "challenge": "<hex>", // SIGN_CHALLENGE only
|
||||
* "expectedSerial": "<hex>", // optional — locks selection to a single cert
|
||||
* "title": "...",
|
||||
* "codebase": "..."
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Windows uses the OS keystore ({@code Windows-MY}). Linux/Mac falls back to a PKCS11 token
|
||||
* driver the user points at via {@link TokenFrame}.
|
||||
*
|
||||
* <p>Server is the source of truth for what gets persisted — this helper extracts and POSTs;
|
||||
* the server parses, validates, and stores.
|
||||
*/
|
||||
public final class CbsnexoraDscHelper {
|
||||
|
||||
/** A freshly-launched JNLP must be less than this many minutes old. Prevents stale-link reuse. */
|
||||
private static final long MAX_LINK_AGE_MINUTES = 5;
|
||||
|
||||
private static HelperArgs cfg;
|
||||
private static KeyStore keyStore;
|
||||
private static Provider pkcs11Provider; // Linux/Mac
|
||||
|
||||
public static void main(final String[] args) {
|
||||
try {
|
||||
if (!validateJnlpAge()) {
|
||||
showError("This signing link has expired. Please request a fresh one from the portal.",
|
||||
"Link Expired");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
if (args == null || args.length < 1 || args[0] == null || args[0].isEmpty()) {
|
||||
showError("Helper launched without arguments.", "Bad Launch");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
cfg = HelperArgs.parse(args[0]);
|
||||
|
||||
// Token picker is now shown on every OS — Windows users can pick either the OS-native
|
||||
// certificate store or a PKCS11 .dll for vendor-specific USB tokens (ePass2003, eToken,
|
||||
// ProxKey, etc.). Linux / macOS only see PKCS11 options.
|
||||
SwingUtilities.invokeLater(CbsnexoraDscHelper::launchTokenPicker);
|
||||
} catch (final Exception e) {
|
||||
e.printStackTrace();
|
||||
showError("Unexpected error: " + e.getMessage(), "Error");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Token picker (cross-platform) ───────────────────────────────────────
|
||||
|
||||
private static void launchTokenPicker() {
|
||||
final TokenFrame tokenFrame = new TokenFrame();
|
||||
tokenFrame.setProceedAction(selection -> {
|
||||
try {
|
||||
if (TokenFrame.SELECTION_WINDOWS_MY.equals(selection)) {
|
||||
// OS-native keystore — no PIN dialog needed; the platform handles authn.
|
||||
keyStore = KeyStore.getInstance("Windows-MY");
|
||||
keyStore.load(null, null);
|
||||
} else {
|
||||
// PKCS11 library — drives a Swing PIN dialog via the callback handler.
|
||||
initializePkcs11(selection);
|
||||
keyStore = loadPkcs11KeyStore();
|
||||
if (keyStore == null) {
|
||||
showError("Failed to open token keystore. Wrong PIN?", "Login Failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
tokenFrame.dispose();
|
||||
showCertSelectionWindow();
|
||||
} catch (final Exception e) {
|
||||
e.printStackTrace();
|
||||
showError("Could not open keystore: " + e.getMessage(), "Token Error");
|
||||
}
|
||||
});
|
||||
tokenFrame.setVisible(true);
|
||||
}
|
||||
|
||||
private static void initializePkcs11(final String libraryPath) throws Exception {
|
||||
final String config = "name = CBSnexoraDSC\nlibrary = " + libraryPath + "\n";
|
||||
final ByteArrayInputStream in = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8));
|
||||
try {
|
||||
// Java 9+
|
||||
final Class<?> sunPkcs11 = Class.forName("sun.security.pkcs11.SunPKCS11");
|
||||
pkcs11Provider = (Provider) sunPkcs11.getConstructor(InputStream.class).newInstance(in);
|
||||
} catch (final ClassNotFoundException e) {
|
||||
// Fallback — same path; preserved for old JREs that resolve the class differently.
|
||||
pkcs11Provider = (Provider) Class.forName("sun.security.pkcs11.SunPKCS11")
|
||||
.getConstructor(InputStream.class).newInstance(in);
|
||||
}
|
||||
Security.removeProvider(pkcs11Provider.getName());
|
||||
Security.addProvider(pkcs11Provider);
|
||||
}
|
||||
|
||||
private static KeyStore loadPkcs11KeyStore() throws Exception {
|
||||
final AuthProvider authProvider = (AuthProvider) pkcs11Provider;
|
||||
final KeyStore.CallbackHandlerProtection protection = new KeyStore.CallbackHandlerProtection(
|
||||
new SwingPasswordCallbackHandler());
|
||||
final KeyStore.Builder builder = KeyStore.Builder.newInstance("PKCS11", authProvider, protection);
|
||||
return builder.getKeyStore();
|
||||
}
|
||||
|
||||
// ── Certificate selection UI (shared between Windows and PKCS11) ────────
|
||||
|
||||
private static void showCertSelectionWindow() {
|
||||
final String windowTitle = cfg.title != null ? cfg.title : "Select Digital Signature Certificate";
|
||||
final JFrame frame = new JFrame("CBSnexora — " + windowTitle);
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
Branding.applyWindowIcon(frame);
|
||||
frame.setLayout(new BorderLayout());
|
||||
frame.getContentPane().setBackground(Branding.BODY_BG);
|
||||
|
||||
final String subtitle = "ENROLL".equals(cfg.mode)
|
||||
? "Pick the certificate to map to your CBSnexora account."
|
||||
: "Pick the certificate to sign the login challenge.";
|
||||
frame.add(Branding.buildHeader(windowTitle, subtitle), BorderLayout.NORTH);
|
||||
|
||||
final DefaultTableModel model = new DefaultTableModel(
|
||||
new Object[] { "Holder Name", "Alias", "Organisation", "DSC Type", "Serial No",
|
||||
"Issuer (CA)", "Valid From", "Expires" }, 0) {
|
||||
@Override
|
||||
public boolean isCellEditable(final int row, final int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
final JTable table = new JTable(model);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(26);
|
||||
table.setGridColor(Branding.DIVIDER);
|
||||
table.setShowVerticalLines(false);
|
||||
table.getTableHeader().setFont(table.getTableHeader().getFont().deriveFont(java.awt.Font.BOLD, 12f));
|
||||
table.getTableHeader().setBackground(Branding.HEADER_BG);
|
||||
table.getTableHeader().setForeground(Branding.TITLE_DARK);
|
||||
// Holder Name / Alias / Organisation / DSC Type / Serial / Issuer / Valid From / Expires.
|
||||
// Alias is the keystore label (Windows friendly name or auto-generated handle) — useful when
|
||||
// the CN is opaque (UUID test cert, hashed-CN tokens) but the user has set a recognisable
|
||||
// friendly name in certmgr.msc.
|
||||
final int[] widths = { 160, 130, 160, 110, 150, 210, 100, 100 };
|
||||
for (int i = 0; i < widths.length; i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
}
|
||||
|
||||
final SimpleDateFormat dateFmt = new SimpleDateFormat("dd-MMM-yyyy");
|
||||
// Fully qualified — java.awt.List exists too and collides with the wildcard import.
|
||||
final java.util.List<String> aliasList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
final Enumeration<String> en = keyStore.aliases();
|
||||
while (en.hasMoreElements()) {
|
||||
final String alias = en.nextElement();
|
||||
final java.security.cert.Certificate cert = keyStore.getCertificate(alias);
|
||||
if (!(cert instanceof X509Certificate)) {
|
||||
continue;
|
||||
}
|
||||
final X509Certificate x = (X509Certificate) cert;
|
||||
final String serial = x.getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
|
||||
|
||||
// SIGN_CHALLENGE with expectedSerial → only show the one cert the server expects.
|
||||
if ("SIGN_CHALLENGE".equals(cfg.mode) && cfg.expectedSerial != null
|
||||
&& !cfg.expectedSerial.equalsIgnoreCase(serial)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String subjectDn = x.getSubjectX500Principal().getName();
|
||||
model.addRow(new Object[] {
|
||||
extractCN(subjectDn),
|
||||
alias,
|
||||
extractRdn(subjectDn, "O"),
|
||||
extractDscType(x),
|
||||
serial,
|
||||
extractCN(x.getIssuerX500Principal().getName()),
|
||||
dateFmt.format(x.getNotBefore()),
|
||||
dateFmt.format(x.getNotAfter())
|
||||
});
|
||||
aliasList.add(alias);
|
||||
}
|
||||
} catch (final KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
showError("Could not read certificates: " + e.getMessage(), "Keystore Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.getRowCount() == 0) {
|
||||
showError("SIGN_CHALLENGE".equals(cfg.mode)
|
||||
? "No DSC matching the expected serial was found in this keystore."
|
||||
: "No certificates were found in the keystore.", "Nothing to Sign");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
final JButton proceed = Branding.primaryButton(
|
||||
cfg.mode.equals("ENROLL") ? "Map Selected DSC" : "Sign with Selected DSC");
|
||||
proceed.setEnabled(false);
|
||||
table.getSelectionModel().addListSelectionListener(e -> proceed.setEnabled(table.getSelectedRow() >= 0));
|
||||
|
||||
final JButton cancel = Branding.secondaryButton("Cancel");
|
||||
cancel.addActionListener(e -> System.exit(0));
|
||||
|
||||
proceed.addActionListener(e -> {
|
||||
final int row = table.getSelectedRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
final String alias = aliasList.get(row);
|
||||
try {
|
||||
if ("ENROLL".equals(cfg.mode)) {
|
||||
performEnroll(alias);
|
||||
} else if ("SIGN_CHALLENGE".equals(cfg.mode)) {
|
||||
performSignChallenge(alias);
|
||||
} else {
|
||||
showError("Unknown mode: " + cfg.mode, "Bad Mode");
|
||||
return;
|
||||
}
|
||||
JOptionPane.showMessageDialog(frame, "Done. You can close this window and return to the browser.",
|
||||
"Success", JOptionPane.INFORMATION_MESSAGE);
|
||||
frame.dispose();
|
||||
System.exit(0);
|
||||
} catch (final Exception ex) {
|
||||
ex.printStackTrace();
|
||||
showError("Operation failed: " + ex.getMessage(), "Error");
|
||||
}
|
||||
});
|
||||
|
||||
final JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 14));
|
||||
buttons.setBackground(Branding.BODY_BG);
|
||||
buttons.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 18, 10, 18));
|
||||
buttons.add(cancel);
|
||||
buttons.add(proceed);
|
||||
|
||||
final JScrollPane scroll = new JScrollPane(table);
|
||||
scroll.getViewport().setBackground(Color.WHITE);
|
||||
scroll.setBorder(javax.swing.BorderFactory.createCompoundBorder(
|
||||
javax.swing.BorderFactory.createEmptyBorder(12, 18, 6, 18),
|
||||
javax.swing.BorderFactory.createLineBorder(Branding.DIVIDER)));
|
||||
frame.add(scroll, BorderLayout.CENTER);
|
||||
frame.add(buttons, BorderLayout.SOUTH);
|
||||
frame.setSize(1220, 480);
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
// ── ENROLL: send the cert (base64 DER) up to the server ─────────────────
|
||||
|
||||
private static void performEnroll(final String alias) throws Exception {
|
||||
final X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
|
||||
|
||||
// Proof-of-possession: sign a 32-byte random nonce with the private key and verify with
|
||||
// the cert's public key. Two effects in one call —
|
||||
// 1. forces the PKCS11 PIN dialog (initSign needs the private key, triggers the callback)
|
||||
// 2. proves the user actually has the matching private key, not just a readable cert
|
||||
//
|
||||
// Signing (not RSA encrypt/decrypt) is used because it works for every cert we care about:
|
||||
// - RSA Signing-only certs (Indian Class 3 DSCs — most common case)
|
||||
// - RSA Signing+Encryption certs
|
||||
// - EC / ECDSA certs (newer DSCs, test certs)
|
||||
// The encrypt/decrypt approach used by the legacy MultiTenderActionSign fails on Signing-
|
||||
// only certs (no keyEncipherment KeyUsage) and on EC keys (no RSA cipher support).
|
||||
proveKeyPossessionViaSignature(alias, cert);
|
||||
|
||||
final String certB64 = Base64.getEncoder().encodeToString(cert.getEncoded());
|
||||
final Map<String, String> body = new LinkedHashMap<>();
|
||||
body.put("certB64", certB64);
|
||||
body.put("dscName", extractCN(cert.getSubjectX500Principal().getName()));
|
||||
body.put("dscType", resolveDscTypeForBackend(cert));
|
||||
postJson(cfg.callbackUrl, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a random nonce with the alias's private key and verifies it with the cert's public
|
||||
* key. Chooses the signature algorithm from the key's algorithm — SHA256withRSA for RSA,
|
||||
* SHA256withECDSA for EC. The {@code initSign} call cascades into the PKCS11
|
||||
* {@link SwingPasswordCallbackHandler}, which is what shows the PIN dialog at enroll time.
|
||||
*/
|
||||
private static void proveKeyPossessionViaSignature(final String alias, final X509Certificate cert)
|
||||
throws GeneralSecurityException {
|
||||
final java.security.PrivateKey privateKey = (java.security.PrivateKey) keyStore.getKey(alias, null);
|
||||
if (privateKey == null) {
|
||||
throw new GeneralSecurityException("Private key for alias " + alias + " is not accessible");
|
||||
}
|
||||
final String sigAlgo = signatureAlgorithmFor(cert.getPublicKey().getAlgorithm());
|
||||
final byte[] nonce = new byte[32];
|
||||
new SecureRandom().nextBytes(nonce);
|
||||
|
||||
final Signature signer = Signature.getInstance(sigAlgo);
|
||||
signer.initSign(privateKey); // PIN prompt happens here on PKCS11
|
||||
signer.update(nonce);
|
||||
final byte[] signature = signer.sign();
|
||||
|
||||
final Signature verifier = Signature.getInstance(sigAlgo);
|
||||
verifier.initVerify(cert.getPublicKey());
|
||||
verifier.update(nonce);
|
||||
if (!verifier.verify(signature)) {
|
||||
throw new GeneralSecurityException(
|
||||
"Cert/key pair verification failed — the certificate's public key did not verify "
|
||||
+ "the signature produced by the private key on the token.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a public key's algorithm name to the SHA-256 signature variant that key supports. */
|
||||
private static String signatureAlgorithmFor(final String keyAlgorithm) throws GeneralSecurityException {
|
||||
if (keyAlgorithm == null) {
|
||||
throw new GeneralSecurityException("Certificate public key has no algorithm identifier");
|
||||
}
|
||||
final String normalised = keyAlgorithm.toUpperCase(Locale.ROOT);
|
||||
if ("RSA".equals(normalised)) {
|
||||
return "SHA256withRSA";
|
||||
}
|
||||
if ("EC".equals(normalised) || "ECDSA".equals(normalised)) {
|
||||
return "SHA256withECDSA";
|
||||
}
|
||||
if ("DSA".equals(normalised)) {
|
||||
return "SHA256withDSA";
|
||||
}
|
||||
throw new GeneralSecurityException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
|
||||
// ── SIGN_CHALLENGE: RSA-SHA256(challenge bytes) → POST signature ────────
|
||||
|
||||
private static void performSignChallenge(final String alias) throws Exception {
|
||||
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
|
||||
if (privateKey == null) {
|
||||
throw new GeneralSecurityException("Private key for alias " + alias + " is not accessible");
|
||||
}
|
||||
final X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
|
||||
// Sanity: chain + cert exist (also surfaces token issues early).
|
||||
final Certificate[] chain = keyStore.getCertificateChain(alias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
throw new GeneralSecurityException("Cert chain for alias " + alias + " is missing");
|
||||
}
|
||||
final Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(privateKey);
|
||||
sig.update(cfg.challenge.getBytes(StandardCharsets.UTF_8));
|
||||
final String signatureB64 = Base64.getEncoder().encodeToString(sig.sign());
|
||||
|
||||
final Map<String, String> body = new LinkedHashMap<>();
|
||||
body.put("certSerial", cert.getSerialNumber().toString(16).toUpperCase(Locale.ROOT));
|
||||
body.put("signatureB64", signatureB64);
|
||||
postJson(cfg.callbackUrl, body);
|
||||
}
|
||||
|
||||
// ── HTTP helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private static void postJson(final String urlStr, final Object body) throws IOException {
|
||||
final String json = new Gson().toJson(body);
|
||||
final URL url = new URL(urlStr);
|
||||
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
// The helper runs outside the browser (JWS-launched JVM process) so it doesn't get the
|
||||
// tenant header from any HTTP interceptor — pass it explicitly from the JNLP args. The
|
||||
// backend's TenantAwareBasicAuthenticationFilter requires this header (or a
|
||||
// ?tenantIdentifier= query param) on every /api/* request.
|
||||
if (cfg != null && cfg.tenantIdentifier != null && !cfg.tenantIdentifier.isEmpty()) {
|
||||
conn.setRequestProperty("Fineract-Platform-TenantId", cfg.tenantIdentifier);
|
||||
}
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStream out = conn.getOutputStream()) {
|
||||
out.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
final int code = conn.getResponseCode();
|
||||
// Read body for diagnostics; surface non-2xx to the user.
|
||||
final String resp;
|
||||
try (InputStream in = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
|
||||
if (in != null) {
|
||||
final byte[] buf = new byte[2048];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) {
|
||||
sink.write(buf, 0, n);
|
||||
}
|
||||
}
|
||||
resp = sink.toString("UTF-8");
|
||||
}
|
||||
if (code < 200 || code >= 300) {
|
||||
throw new IOException("Server returned HTTP " + code + ": " + resp);
|
||||
}
|
||||
}
|
||||
|
||||
// ── JNLP age check — same shape as your existing tender helper ──────────
|
||||
|
||||
private static boolean validateJnlpAge() {
|
||||
// Look for our JNLP in Downloads — and crucially, match browser-renamed copies too
|
||||
// ("cbsnexora-dsc (1).jnlp", "cbsnexora-dsc (2).jnlp", ...). Browsers add a numeric suffix
|
||||
// when a file by the same name already exists, so a strict exact-name match would pick up
|
||||
// the original (stale) download and ignore the freshly-saved one.
|
||||
//
|
||||
// Decision rule: take the youngest matching JNLP across all prefixes. Accept if it's
|
||||
// within MAX_LINK_AGE_MINUTES, else fail. If no JNLP on disk at all, accept (JWS may have
|
||||
// streamed it directly without saving). The server-side challenge TTL is the real
|
||||
// authority on staleness — this is a fast pre-flight to avoid prompting for the PIN on
|
||||
// a link the server has already expired.
|
||||
try {
|
||||
final String[] prefixes = { "cbsnexora-dsc", "SignTender" };
|
||||
final String userProfile = System.getenv("USERPROFILE");
|
||||
final String downloads = (userProfile != null ? userProfile : System.getProperty("user.home"))
|
||||
+ File.separator + "Downloads";
|
||||
final File dir = new File(downloads);
|
||||
if (!dir.isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
final File[] candidates = dir.listFiles((d, name) -> {
|
||||
if (name == null || !name.toLowerCase(Locale.ROOT).endsWith(".jnlp")) {
|
||||
return false;
|
||||
}
|
||||
final String base = name.toLowerCase(Locale.ROOT);
|
||||
for (final String prefix : prefixes) {
|
||||
if (base.startsWith(prefix.toLowerCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (candidates == null || candidates.length == 0) {
|
||||
return true; // no JNLP on disk → JWS likely streamed it
|
||||
}
|
||||
long youngestMs = Long.MIN_VALUE;
|
||||
for (final File f : candidates) {
|
||||
if (f.lastModified() > youngestMs) {
|
||||
youngestMs = f.lastModified();
|
||||
}
|
||||
}
|
||||
final long youngestMinutes = (System.currentTimeMillis() - youngestMs) / 60_000L;
|
||||
return youngestMinutes <= MAX_LINK_AGE_MINUTES;
|
||||
} catch (final Exception ignored) {
|
||||
return true; // never block on env weirdness
|
||||
}
|
||||
}
|
||||
|
||||
// ── Misc ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static String extractCN(final String dn) {
|
||||
return extractRdn(dn, "CN");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a single RDN value (CN, O, OU…) out of an X.500 distinguished name string. Uses
|
||||
* {@link javax.naming.ldap.LdapName} so escaped commas inside values (e.g.
|
||||
* {@code CN=Smith\, John, O=Foo}) parse correctly — the legacy split-on-comma approach
|
||||
* would have returned "Smith\\" in that case. Falls back to the simple parser only if
|
||||
* LdapName rejects the string.
|
||||
*/
|
||||
private static String extractRdn(final String dn, final String key) {
|
||||
if (dn == null || dn.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
final javax.naming.ldap.LdapName ldap = new javax.naming.ldap.LdapName(dn);
|
||||
for (final javax.naming.ldap.Rdn rdn : ldap.getRdns()) {
|
||||
if (key.equalsIgnoreCase(rdn.getType())) {
|
||||
final Object value = rdn.getValue();
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
} catch (final javax.naming.InvalidNameException ignored) {
|
||||
// Fall back to a permissive comma-split parse — covers malformed-but-readable DNs.
|
||||
for (final String part : dn.split(",")) {
|
||||
final String[] kv = part.trim().split("=", 2);
|
||||
if (kv.length == 2 && key.equalsIgnoreCase(kv[0].trim())) {
|
||||
return kv[1].trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display-friendly DSC type. Resolution order:
|
||||
* 1. {@code cfg.dscType} from JNLP args (server-supplied hint) if present
|
||||
* 2. cert's KeyUsage extension bits (RFC 5280: 0=digitalSignature, 2=keyEncipherment, 3=dataEncipherment)
|
||||
* 3. fallback to "Signing" — the dominant DSC purpose, matches backend's default
|
||||
*/
|
||||
private static String extractDscType(final X509Certificate cert) {
|
||||
final String hint = cfg != null ? cfg.dscType : null;
|
||||
if (hint != null && !hint.isEmpty()) {
|
||||
// Normalise "SIGNING"/"signing"/"Signing" to display form
|
||||
final String upper = hint.toUpperCase(Locale.ROOT);
|
||||
if ("SIGNING".equals(upper)) {
|
||||
return "Signing";
|
||||
}
|
||||
if ("ENCRYPTION".equals(upper)) {
|
||||
return "Encryption";
|
||||
}
|
||||
return hint; // unknown value — show as-is rather than silently dropping it
|
||||
}
|
||||
final boolean[] ku = cert.getKeyUsage();
|
||||
if (ku == null) {
|
||||
return "Signing";
|
||||
}
|
||||
final boolean signing = ku.length > 0 && ku[0];
|
||||
final boolean encryption = (ku.length > 2 && ku[2]) || (ku.length > 3 && ku[3]);
|
||||
if (signing && encryption) {
|
||||
return "Signing + Encryption";
|
||||
}
|
||||
if (signing) {
|
||||
return "Signing";
|
||||
}
|
||||
if (encryption) {
|
||||
return "Encryption";
|
||||
}
|
||||
return "Signing";
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the cert's type to the {@code DscType} enum the backend persists. JNLP arg wins, falls
|
||||
* back to KeyUsage, finally defaults to SIGNING. Same resolution order as {@link #extractDscType}.
|
||||
*/
|
||||
private static String resolveDscTypeForBackend(final X509Certificate cert) {
|
||||
final String hint = cfg != null ? cfg.dscType : null;
|
||||
if (hint != null && !hint.isEmpty()) {
|
||||
final String upper = hint.toUpperCase(Locale.ROOT);
|
||||
if ("SIGNING".equals(upper) || "ENCRYPTION".equals(upper)) {
|
||||
return upper;
|
||||
}
|
||||
}
|
||||
final boolean[] ku = cert.getKeyUsage();
|
||||
if (ku != null) {
|
||||
final boolean signing = ku.length > 0 && ku[0];
|
||||
final boolean encryption = (ku.length > 2 && ku[2]) || (ku.length > 3 && ku[3]);
|
||||
// Backend enum has only SIGNING / ENCRYPTION. A combined cert is treated as SIGNING
|
||||
// because our login-2FA flow uses the signing key — encryption is incidental.
|
||||
if (signing) {
|
||||
return "SIGNING";
|
||||
}
|
||||
if (encryption) {
|
||||
return "ENCRYPTION";
|
||||
}
|
||||
}
|
||||
return "SIGNING";
|
||||
}
|
||||
|
||||
private static void showError(final String message, final String title) {
|
||||
JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/** Parsed JSON-arg payload. Mutable for convenience; not exposed beyond this class. */
|
||||
static final class HelperArgs {
|
||||
String randomNo;
|
||||
String mode; // ENROLL | SIGN_CHALLENGE
|
||||
String callbackUrl;
|
||||
String challenge; // SIGN_CHALLENGE only
|
||||
String expectedSerial; // optional
|
||||
String codebase;
|
||||
String tenantIdentifier; // Fineract tenant — sent as Fineract-Platform-TenantId on callback
|
||||
String title;
|
||||
String dscType; // optional server hint: "SIGNING" | "ENCRYPTION". Falls back to
|
||||
// KeyUsage derivation when absent. Matches the legacy pattern
|
||||
// where the server told the helper which type to label/store.
|
||||
|
||||
static HelperArgs parse(final String json) {
|
||||
final JsonElement root = JsonParser.parseString(json);
|
||||
if (!root.isJsonObject()) {
|
||||
throw new IllegalArgumentException("argument is not a JSON object");
|
||||
}
|
||||
final JsonObject o = root.getAsJsonObject();
|
||||
final HelperArgs a = new HelperArgs();
|
||||
a.randomNo = optString(o, "randomNo");
|
||||
a.mode = optString(o, "mode");
|
||||
a.callbackUrl = optString(o, "callbackUrl");
|
||||
a.challenge = optString(o, "challenge");
|
||||
a.expectedSerial = optString(o, "expectedSerial");
|
||||
a.codebase = optString(o, "codebase");
|
||||
a.tenantIdentifier = optString(o, "tenantIdentifier");
|
||||
a.title = optString(o, "title");
|
||||
a.dscType = optString(o, "dscType");
|
||||
if (a.mode == null || a.callbackUrl == null || a.randomNo == null) {
|
||||
throw new IllegalArgumentException("randomNo, mode, callbackUrl are required");
|
||||
}
|
||||
if ("SIGN_CHALLENGE".equals(a.mode) && (a.challenge == null || a.challenge.isEmpty())) {
|
||||
throw new IllegalArgumentException("challenge is required for SIGN_CHALLENGE mode");
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private static String optString(final JsonObject o, final String k) {
|
||||
return o.has(k) && !o.get(k).isJsonNull() ? o.get(k).getAsString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
private CbsnexoraDscHelper() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package com.cbsnexora.dsc;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
/**
|
||||
* Pops a Swing dialog asking for the PKCS11 token PIN. Used by the {@code KeyStore.Builder} on
|
||||
* Linux/Mac when opening a hardware-token keystore — Windows-MY doesn't need it (the OS keystore
|
||||
* handles auth via the platform UI).
|
||||
*/
|
||||
public class SwingPasswordCallbackHandler implements CallbackHandler {
|
||||
|
||||
@Override
|
||||
public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (final Callback cb : callbacks) {
|
||||
if (cb instanceof PasswordCallback) {
|
||||
handlePasswordCallback((PasswordCallback) cb);
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(cb, "Callback not supported: " + cb.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePasswordCallback(final PasswordCallback passCb) {
|
||||
final JPanel panel = new JPanel(new GridLayout(2, 1, 5, 5));
|
||||
panel.add(new JLabel(passCb.getPrompt() != null ? passCb.getPrompt() : "Enter token PIN:"));
|
||||
final JPasswordField pin = new JPasswordField(20);
|
||||
panel.add(pin);
|
||||
|
||||
final JOptionPane pane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
|
||||
final JDialog dialog = pane.createDialog(null, "Token PIN");
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowOpened(final WindowEvent e) {
|
||||
pin.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
dialog.setVisible(true);
|
||||
dialog.dispose();
|
||||
|
||||
final Object val = pane.getValue();
|
||||
final int choice = val instanceof Integer ? (Integer) val : JOptionPane.CANCEL_OPTION;
|
||||
if (choice == JOptionPane.OK_OPTION) {
|
||||
passCb.setPassword(pin.getPassword());
|
||||
} else {
|
||||
// Mirrors the original tender helper: user cancel → exit. Throwing the cancel up to
|
||||
// KeyStore.Builder would leave the JNLP launch hanging.
|
||||
throw new CancellationException("Token PIN entry cancelled by user");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
package com.cbsnexora.dsc;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.io.File;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
|
||||
/**
|
||||
* Cross-platform DSC token / store picker. On Windows the first entry is "Windows Certificate
|
||||
* Store" (which routes to the {@code Windows-MY} keystore — what most Indian DSC USB tokens auto-
|
||||
* register with). Below that are common PKCS11 libraries for ePass2003 / eToken / ProxKey / etc.,
|
||||
* plus an "Other" option that takes a custom {@code .dll} / {@code .so} / {@code .dylib} path.
|
||||
*
|
||||
* <p>On Linux / macOS the Windows entry is omitted and only PKCS11 options are listed.
|
||||
*
|
||||
* <p>The caller registers a {@link #setProceedAction(Consumer)} callback that receives either:
|
||||
* <ul>
|
||||
* <li>{@link #SELECTION_WINDOWS_MY} — caller opens {@code KeyStore.getInstance("Windows-MY")}</li>
|
||||
* <li>An absolute filesystem path to a PKCS11 module — caller spins up {@code SunPKCS11}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class TokenFrame extends JFrame {
|
||||
|
||||
/** Sentinel returned to the proceed callback when the user picks the OS-native Windows store. */
|
||||
public static final String SELECTION_WINDOWS_MY = "WINDOWS-MY";
|
||||
|
||||
private final Map<String, String> presets = new LinkedHashMap<>();
|
||||
private final JComboBox<String> combo = new JComboBox<>();
|
||||
private final JTextField customPath = new JTextField(28);
|
||||
private final JButton browseButton = Branding.secondaryButton("Browse...");
|
||||
private final JButton proceedButton = Branding.primaryButton("Proceed");
|
||||
private final JButton cancelButton = Branding.secondaryButton("Cancel");
|
||||
private final JPanel customPanel;
|
||||
private Consumer<String> proceedAction = sel -> {};
|
||||
|
||||
public TokenFrame() {
|
||||
super("CBSnexora — Select Digital Signature Token");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
Branding.applyWindowIcon(this);
|
||||
getContentPane().setBackground(Branding.BODY_BG);
|
||||
|
||||
loadPresets();
|
||||
for (final String name : presets.keySet()) {
|
||||
combo.addItem(name);
|
||||
}
|
||||
combo.addItem("Other (browse for library file)");
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
add(Branding.buildHeader("Select Digital Signature Token",
|
||||
"Choose the keystore or token driver used to read your DSC."), BorderLayout.NORTH);
|
||||
|
||||
// Body: form rows on a GridBag so labels align cleanly even after the optional
|
||||
// 'Library:' row appears.
|
||||
final JPanel body = new JPanel(new GridBagLayout());
|
||||
body.setBackground(Branding.BODY_BG);
|
||||
body.setBorder(BorderFactory.createEmptyBorder(20, 24, 20, 24));
|
||||
|
||||
final GridBagConstraints g = new GridBagConstraints();
|
||||
g.insets = new Insets(6, 6, 6, 6);
|
||||
g.anchor = GridBagConstraints.WEST;
|
||||
|
||||
final JLabel comboLabel = new JLabel("Token / Keystore:");
|
||||
comboLabel.setFont(comboLabel.getFont().deriveFont(Font.BOLD, 13f));
|
||||
comboLabel.setForeground(Branding.TITLE_DARK);
|
||||
g.gridx = 0; g.gridy = 0; body.add(comboLabel, g);
|
||||
combo.setPreferredSize(new Dimension(360, 28));
|
||||
g.gridx = 1; g.weightx = 1.0; g.fill = GridBagConstraints.HORIZONTAL;
|
||||
body.add(combo, g);
|
||||
|
||||
customPanel = new JPanel(new GridBagLayout());
|
||||
customPanel.setOpaque(false);
|
||||
final GridBagConstraints cg = new GridBagConstraints();
|
||||
cg.insets = new Insets(0, 0, 0, 6);
|
||||
cg.anchor = GridBagConstraints.WEST;
|
||||
final JLabel libLabel = new JLabel("Library:");
|
||||
libLabel.setFont(libLabel.getFont().deriveFont(Font.PLAIN, 12f));
|
||||
libLabel.setForeground(Branding.SUBTITLE_GREY);
|
||||
cg.gridx = 0; cg.gridy = 0; customPanel.add(libLabel, cg);
|
||||
cg.gridx = 1; cg.weightx = 1.0; cg.fill = GridBagConstraints.HORIZONTAL;
|
||||
customPanel.add(customPath, cg);
|
||||
cg.gridx = 2; cg.weightx = 0; cg.fill = GridBagConstraints.NONE;
|
||||
customPanel.add(browseButton, cg);
|
||||
customPanel.setVisible(false);
|
||||
|
||||
g.gridx = 0; g.gridy = 1; g.gridwidth = 2; g.weightx = 1.0;
|
||||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
body.add(customPanel, g);
|
||||
|
||||
add(body, BorderLayout.CENTER);
|
||||
|
||||
final JPanel actions = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT, 10, 14));
|
||||
actions.setBackground(Branding.BODY_BG);
|
||||
actions.setBorder(BorderFactory.createEmptyBorder(0, 18, 10, 18));
|
||||
actions.add(cancelButton);
|
||||
actions.add(proceedButton);
|
||||
add(actions, BorderLayout.SOUTH);
|
||||
|
||||
// Hooks
|
||||
combo.addActionListener(e -> {
|
||||
final boolean isOther = combo.getSelectedItem() != null
|
||||
&& combo.getSelectedItem().toString().startsWith("Other");
|
||||
customPanel.setVisible(isOther);
|
||||
revalidate();
|
||||
pack();
|
||||
});
|
||||
browseButton.addActionListener(e -> {
|
||||
final JFileChooser fc = new JFileChooser();
|
||||
if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
customPath.setText(fc.getSelectedFile().getAbsolutePath());
|
||||
}
|
||||
});
|
||||
cancelButton.addActionListener(e -> System.exit(0));
|
||||
proceedButton.addActionListener(e -> {
|
||||
final String selection = resolveSelection();
|
||||
if (selection == null) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Please pick a token / keystore option (or browse to a library file).",
|
||||
"No Selection", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
proceedAction.accept(selection);
|
||||
});
|
||||
|
||||
setMinimumSize(new Dimension(620, 240));
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
public void setProceedAction(final Consumer<String> action) {
|
||||
this.proceedAction = action != null ? action : sel -> {};
|
||||
}
|
||||
|
||||
/** Translates the UI state into the value handed to the proceed callback. */
|
||||
private String resolveSelection() {
|
||||
final Object item = combo.getSelectedItem();
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
if (item.toString().startsWith("Other")) {
|
||||
final String path = customPath.getText();
|
||||
return (path != null && !path.trim().isEmpty()) ? path.trim() : null;
|
||||
}
|
||||
return presets.get(item.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Default token / keystore presets. Adjust paths per install if your customers run different
|
||||
* driver locations — most CCA-licensed CA tokens stick to the defaults below.
|
||||
*/
|
||||
private void loadPresets() {
|
||||
final String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
|
||||
if (os.contains("win")) {
|
||||
presets.put("Windows Certificate Store (default)", SELECTION_WINDOWS_MY);
|
||||
presets.put("ePass2003 (eMudhra / Capricorn)", "C:\\Windows\\System32\\eps2003csp11.dll");
|
||||
presets.put("ePass2003 v2", "C:\\Windows\\System32\\eps2003csp11v2.dll");
|
||||
presets.put("SafeNet eToken 5110", "C:\\Windows\\System32\\eTPKCS11.dll");
|
||||
presets.put("WatchData ProxKey", "C:\\Windows\\System32\\WDPKCS.dll");
|
||||
presets.put("Aladdin eToken", "C:\\Windows\\System32\\eTPkcs11.dll");
|
||||
presets.put("HYP2003 / Trust Key", "C:\\Windows\\System32\\SignatureP11.dll");
|
||||
presets.put("mToken CryptoID", "C:\\Windows\\System32\\mToken.dll");
|
||||
} else if (os.contains("mac") || os.contains("darwin")) {
|
||||
presets.put("ePass2003 (eMudhra / Capricorn)", "/usr/local/lib/libcastle_v2.dylib");
|
||||
presets.put("SafeNet eToken", "/usr/local/lib/libeTPkcs11.dylib");
|
||||
presets.put("WatchData ProxKey", "/usr/local/lib/libwdpkcs.dylib");
|
||||
} else {
|
||||
// Linux
|
||||
presets.put("ePass2003 (eMudhra / Capricorn)", "/usr/lib/x86_64-linux-gnu/libcastle_v2.so");
|
||||
presets.put("ePass2003 alt path", "/usr/lib/libcastle_v2.so");
|
||||
presets.put("SafeNet eToken", "/usr/lib/libeToken.so");
|
||||
presets.put("SafeNet eToken (Ubuntu)", "/usr/lib/x86_64-linux-gnu/libeToken.so");
|
||||
presets.put("WatchData ProxKey", "/usr/lib/libwdpkcs.so");
|
||||
presets.put("OpenSC (generic)", "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so");
|
||||
}
|
||||
|
||||
// Drop entries whose underlying file doesn't exist — keeps the picker honest.
|
||||
presets.values().removeIf(path -> !SELECTION_WINDOWS_MY.equals(path) && !new File(path).exists());
|
||||
|
||||
// Always keep at least Windows-MY on Windows even if the rest were stripped.
|
||||
if (os.contains("win") && presets.isEmpty()) {
|
||||
presets.put("Windows Certificate Store (default)", SELECTION_WINDOWS_MY);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
+6
-3
@@ -24,7 +24,6 @@ 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.POST;
|
||||
@@ -52,9 +51,13 @@ public class AccrualAccountingApiResource {
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Executes Periodic Accrual Accounting", method = "POST", description = "Mandatory Fields\n" + "\n" + "tillDate\n")
|
||||
@Operation(summary = "Executes Periodic Accrual Accounting", method = "POST", description = """
|
||||
Mandatory Fields
|
||||
|
||||
tillDate
|
||||
""")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = AccrualAccountingApiResourceSwagger.PostRunaccrualsRequest.class)))
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public CommandProcessingResult executePeriodicAccrualAccounting(@Parameter(hidden = true) AccrualAccountRequest accrualAccountRequest) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().excuteAccrualAccounting()
|
||||
.withJson(apiJsonSerializerService.serialize(accrualAccountRequest)).build();
|
||||
|
||||
+27
-18
@@ -25,7 +25,6 @@ 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.DELETE;
|
||||
@@ -57,10 +56,17 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/glclosures")
|
||||
@Component
|
||||
@Tag(name = "Accounting Closure", description = "An accounting closure indicates that no more journal entries may be logged (or reversed) in the system, either manually or via the portfolio with an entry date prior to the defined closure date\n"
|
||||
+ "\n" + "Field Descriptions\n" + "closingDate\n" + "The date for which the accounting closure is defined\n" + "officeId\n"
|
||||
+ "The identifer of the branch for which accounting has been closed\n" + "comments\n"
|
||||
+ "Description associated with an Accounting closure")
|
||||
@Tag(name = "Accounting Closure", description = """
|
||||
An accounting closure indicates that no more journal entries may be logged (or reversed) in the system, either manually or via the portfolio with an entry date prior to the defined closure date
|
||||
|
||||
Field Descriptions
|
||||
closingDate
|
||||
The date for which the accounting closure is defined
|
||||
officeId
|
||||
The identifer of the branch for which accounting has been closed
|
||||
comments
|
||||
Description associated with an Accounting closure
|
||||
""")
|
||||
@RequiredArgsConstructor
|
||||
public class GLClosuresApiResource {
|
||||
|
||||
@@ -76,9 +82,11 @@ public class GLClosuresApiResource {
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Accounting closures", description = "Example Requests:\n" + "\n" + "glclosures")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class)))) })
|
||||
@Operation(summary = "List Accounting closures", description = """
|
||||
Example Requests:
|
||||
|
||||
glclosures""")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class))))
|
||||
public List<GLClosureData> retrieveAllClosures(@QueryParam("officeId") @Parameter(name = "officeId") final Long officeId) {
|
||||
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
||||
@@ -89,10 +97,14 @@ public class GLClosuresApiResource {
|
||||
@Path("{glClosureId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an Accounting Closure", description = "Example Requests:\n" + "\n" + "glclosures/1\n" + "\n" + "\n"
|
||||
+ "/glclosures/1?fields=officeName,closingDate")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class))) })
|
||||
@Operation(summary = "Retrieve an Accounting Closure", description = """
|
||||
Example Requests:
|
||||
|
||||
glclosures/1
|
||||
|
||||
|
||||
/glclosures/1?fields=officeName,closingDate""")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class)))
|
||||
public GLClosureData retreiveClosure(@PathParam("glClosureId") @Parameter(description = "glClosureId") final Long glClosureId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
||||
@@ -112,8 +124,7 @@ public class GLClosuresApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create an Accounting Closure", description = "Mandatory Fields\n" + "officeId,closingDate")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PostGlClosuresRequest.class, description = "Request Body")))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PostGlClosuresResponse.class))) })
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PostGlClosuresResponse.class)))
|
||||
public CommandProcessingResult createGLClosure(@Parameter(hidden = true) GLClosureRequest glClosureRequest) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().createGLClosure()
|
||||
.withJson(apiJsonSerializerService.serialize(glClosureRequest)).build();
|
||||
@@ -127,8 +138,7 @@ public class GLClosuresApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update an Accounting closure", description = "Once an accounting closure is created, only the comments associated with it may be edited")
|
||||
@RequestBody(content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PutGlClosuresRequest.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PutGlClosuresResponse.class))) })
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PutGlClosuresResponse.class)))
|
||||
public CommandProcessingResult updateGLClosure(@PathParam("glClosureId") @Parameter(description = "glClosureId") final Long glClosureId,
|
||||
@Parameter(hidden = true) GLClosureRequest glClosureRequest) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateGLClosure(glClosureId)
|
||||
@@ -141,8 +151,7 @@ public class GLClosuresApiResource {
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Delete an accounting closure", description = "Note: Only the latest accounting closure associated with a branch may be deleted.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.DeleteGlClosuresResponse.class))) })
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.DeleteGlClosuresResponse.class)))
|
||||
public CommandProcessingResult deleteGLClosure(
|
||||
@PathParam("glClosureId") @Parameter(description = "glclosureId") final Long glClosureId) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteGLClosure(glClosureId).build();
|
||||
|
||||
+1
-2
@@ -72,8 +72,7 @@ public final class FinancialActivityAccountDataValidator {
|
||||
}
|
||||
|
||||
private DataValidatorBuilder getDataValidator(final List<ApiParameterError> dataValidationErrors) {
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("financialactivityaccount");
|
||||
return baseDataValidator;
|
||||
return new DataValidatorBuilder(dataValidationErrors).resource("financialactivityaccount");
|
||||
}
|
||||
|
||||
public void validateForUpdate(final String json) {
|
||||
|
||||
+31
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.accounting.glaccount.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -28,4 +30,33 @@ public interface TrialBalanceRepository extends JpaRepository<TrialBalance, Long
|
||||
|
||||
@Query(value = "select * from m_trial_balance where office_id=:officeId and account_id=:accountId and closing_balance is null order by created_date, entry_date", nativeQuery = true)
|
||||
List<TrialBalance> findNewByOfficeAndAccount(@Param("officeId") Long officeId, @Param("accountId") Long accountId);
|
||||
|
||||
@Query("SELECT MAX(tb.transactionDate) FROM TrialBalance tb")
|
||||
LocalDate findMaxCreatedDate();
|
||||
|
||||
@Query("""
|
||||
SELECT tb.closingBalance
|
||||
FROM TrialBalance tb
|
||||
WHERE tb.officeId = :officeId
|
||||
AND tb.glAccountId = :accountId
|
||||
AND tb.closingBalance IS NOT NULL
|
||||
ORDER BY tb.transactionDate DESC, tb.entryDate DESC
|
||||
""")
|
||||
List<BigDecimal> findLastClosingBalance(Long officeId, Long accountId);
|
||||
|
||||
@Query("""
|
||||
SELECT DISTINCT tb.officeId
|
||||
FROM TrialBalance tb
|
||||
WHERE tb.closingBalance IS NULL
|
||||
""")
|
||||
List<Long> findDistinctOfficeIdsWithNullClosingBalance();
|
||||
|
||||
@Query("""
|
||||
SELECT DISTINCT tb.glAccountId
|
||||
FROM TrialBalance tb
|
||||
WHERE tb.officeId = :officeId
|
||||
AND tb.closingBalance IS NULL
|
||||
""")
|
||||
List<Long> findDistinctAccountIdsWithNullClosingBalanceByOfficeId(Long officeId);
|
||||
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,7 +19,9 @@
|
||||
package org.apache.fineract.accounting.glaccount.jobs.updatetrialbalancedetails;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepository;
|
||||
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.springframework.batch.core.Job;
|
||||
@@ -40,6 +42,8 @@ public class UpdateTrialBalanceDetailsConfig {
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
|
||||
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
|
||||
private final TrialBalanceRepository trialBalanceRepository;
|
||||
private final JournalEntryRepository journalEntryRepository;
|
||||
|
||||
@Bean
|
||||
protected Step updateTrialBalanceDetailsStep() {
|
||||
@@ -55,6 +59,7 @@ public class UpdateTrialBalanceDetailsConfig {
|
||||
|
||||
@Bean
|
||||
public UpdateTrialBalanceDetailsTasklet updateTrialBalanceDetailsTasklet() {
|
||||
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper);
|
||||
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper, trialBalanceRepository,
|
||||
journalEntryRepository);
|
||||
}
|
||||
}
|
||||
|
||||
+79
-49
@@ -24,7 +24,9 @@ import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.accounting.glaccount.domain.TrialBalance;
|
||||
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepository;
|
||||
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
|
||||
@@ -41,59 +43,87 @@ public class UpdateTrialBalanceDetailsTasklet implements Tasklet {
|
||||
|
||||
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
|
||||
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
|
||||
private final TrialBalanceRepository trialBalanceRepository;
|
||||
private final JournalEntryRepository journalEntryRepository;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
|
||||
final StringBuilder tbGapSqlBuilder = new StringBuilder(500);
|
||||
tbGapSqlBuilder.append("select distinct(je.transaction_date) ").append("from acc_gl_journal_entry je ")
|
||||
.append("where je.transaction_date > (select coalesce(MAX(created_date),'2010-01-01') from m_trial_balance)");
|
||||
final List<LocalDate> tbGaps = jdbcTemplate.queryForList(tbGapSqlBuilder.toString(), LocalDate.class);
|
||||
for (LocalDate tbGap : tbGaps) {
|
||||
int days = DateUtils.getExactDifferenceInDays(tbGap, DateUtils.getBusinessLocalDate());
|
||||
if (days < 1) {
|
||||
continue;
|
||||
}
|
||||
final StringBuilder sqlBuilder = new StringBuilder(600);
|
||||
sqlBuilder.append("Insert Into m_trial_balance(office_id, account_id, Amount, entry_date, created_date,closing_balance) ")
|
||||
.append("Select je.office_id, je.account_id, SUM(CASE WHEN je.type_enum=1 THEN (-1) * je.amount ELSE je.amount END) ")
|
||||
.append("as Amount, Date(je.entry_date) as Entry_Date, je.transaction_date as Created_Date,sum(je.amount) as closing_balance ")
|
||||
.append("from acc_gl_journal_entry je WHERE je.transaction_date = ? ")
|
||||
.append("group by je.account_id, je.office_id, je.transaction_date, Date(je.entry_date)");
|
||||
final int result = jdbcTemplate.update(sqlBuilder.toString(), tbGap);
|
||||
log.debug("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
|
||||
}
|
||||
String distinctOfficeQuery = "select distinct(office_id) from m_trial_balance where closing_balance is null group by office_id";
|
||||
final List<Long> officeIds = jdbcTemplate.queryForList(distinctOfficeQuery, Long.class);
|
||||
for (Long officeId : officeIds) {
|
||||
String distinctAccountQuery = "select distinct(account_id) from m_trial_balance where office_id=? and closing_balance is null group by account_id";
|
||||
final List<Long> accountIds = jdbcTemplate.queryForList(distinctAccountQuery, Long.class, officeId);
|
||||
for (Long accountId : accountIds) {
|
||||
final String closingBalanceQuery = "select closing_balance from m_trial_balance where office_id=? and account_id=? and closing_balance "
|
||||
+ "is not null order by created_date desc, entry_date desc limit 1";
|
||||
List<BigDecimal> closingBalanceData = jdbcTemplate.queryForList(closingBalanceQuery, BigDecimal.class, officeId, accountId);
|
||||
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
|
||||
BigDecimal closingBalance = null;
|
||||
if (!CollectionUtils.isEmpty(closingBalanceData)) {
|
||||
closingBalance = closingBalanceData.get(0);
|
||||
}
|
||||
if (CollectionUtils.isEmpty(closingBalanceData)) {
|
||||
closingBalance = BigDecimal.ZERO;
|
||||
for (TrialBalance row : tbRows) {
|
||||
closingBalance = closingBalance.add(row.getAmount());
|
||||
row.setClosingBalance(closingBalance);
|
||||
}
|
||||
} else {
|
||||
for (TrialBalance tbRow : tbRows) {
|
||||
if (closingBalance != null) {
|
||||
closingBalance = closingBalance.add(tbRow.getAmount());
|
||||
}
|
||||
tbRow.setClosingBalance(closingBalance);
|
||||
}
|
||||
}
|
||||
trialBalanceRepositoryWrapper.save(tbRows);
|
||||
}
|
||||
}
|
||||
|
||||
processTrialBalanceGaps(jdbcTemplate);
|
||||
updateClosingBalances(jdbcTemplate);
|
||||
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
|
||||
private void processTrialBalanceGaps(JdbcTemplate jdbcTemplate) {
|
||||
LocalDate maxCreatedDate = trialBalanceRepository.findMaxCreatedDate();
|
||||
LocalDate baselineDate = maxCreatedDate != null ? maxCreatedDate : LocalDate.of(2010, 1, 1);
|
||||
List<LocalDate> tbGaps = journalEntryRepository.findTransactionDatesAfter(baselineDate);
|
||||
for (LocalDate tbGap : tbGaps) {
|
||||
if (DateUtils.getExactDifferenceInDays(tbGap, DateUtils.getBusinessLocalDate()) < 1) {
|
||||
continue;
|
||||
}
|
||||
insertTrialBalanceForDate(tbGap);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertTrialBalanceForDate(LocalDate tbGap) {
|
||||
List<Object[]> rows = journalEntryRepository.findTrialBalanceLinesForDate(tbGap);
|
||||
|
||||
List<TrialBalance> trialBalances = rows.stream().map(row -> {
|
||||
TrialBalance tb = new TrialBalance();
|
||||
tb.setOfficeId((Long) row[0]);
|
||||
tb.setGlAccountId((Long) row[1]);
|
||||
tb.setAmount((BigDecimal) row[2]);
|
||||
tb.setEntryDate((LocalDate) row[3]);
|
||||
tb.setTransactionDate((LocalDate) row[4]);
|
||||
tb.setClosingBalance((BigDecimal) row[5]);
|
||||
return tb;
|
||||
}).toList();
|
||||
|
||||
trialBalanceRepositoryWrapper.save(trialBalances);
|
||||
|
||||
log.debug("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(),
|
||||
trialBalances.size());
|
||||
}
|
||||
|
||||
private void updateClosingBalances(JdbcTemplate jdbcTemplate) {
|
||||
final List<Long> officeIds = trialBalanceRepository.findDistinctOfficeIdsWithNullClosingBalance();
|
||||
|
||||
for (Long officeId : officeIds) {
|
||||
updateClosingBalancesForOffice(jdbcTemplate, officeId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateClosingBalancesForOffice(JdbcTemplate jdbcTemplate, Long officeId) {
|
||||
final List<Long> accountIds = trialBalanceRepository.findDistinctAccountIdsWithNullClosingBalanceByOfficeId(officeId);
|
||||
|
||||
for (Long accountId : accountIds) {
|
||||
updateClosingBalanceForAccount(jdbcTemplate, officeId, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateClosingBalanceForAccount(JdbcTemplate jdbcTemplate, Long officeId, Long accountId) {
|
||||
BigDecimal closingBalance = getPreviousClosingBalance(officeId, accountId);
|
||||
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
|
||||
|
||||
updateTrialBalanceRows(tbRows, closingBalance);
|
||||
}
|
||||
|
||||
private BigDecimal getPreviousClosingBalance(Long officeId, Long accountId) {
|
||||
List<BigDecimal> closingBalanceData = trialBalanceRepository.findLastClosingBalance(officeId, accountId);
|
||||
return CollectionUtils.isEmpty(closingBalanceData) ? BigDecimal.ZERO : closingBalanceData.getFirst();
|
||||
}
|
||||
|
||||
private void updateTrialBalanceRows(List<TrialBalance> tbRows, BigDecimal initialClosingBalance) {
|
||||
BigDecimal closingBalance = initialClosingBalance;
|
||||
|
||||
for (TrialBalance row : tbRows) {
|
||||
if (closingBalance != null) {
|
||||
closingBalance = closingBalance.add(row.getAmount());
|
||||
}
|
||||
row.setClosingBalance(closingBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+109
-107
@@ -1,107 +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 org.apache.fineract.accounting.journalentry;
|
||||
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
||||
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface JournalEntryMapper {
|
||||
|
||||
@Mapping(target = "id", source = "id")
|
||||
@Mapping(target = "officeId", source = "office.id")
|
||||
@Mapping(target = "officeName", source = "office.name")
|
||||
@Mapping(target = "glAccountId", source = "glAccount.id")
|
||||
@Mapping(target = "glAccountCode", source = "glAccount.glCode")
|
||||
@Mapping(target = "glAccountName", source = "glAccount.name")
|
||||
@Mapping(target = "glAccountType", source = "glAccount.type", qualifiedByName = "glAccountType")
|
||||
@Mapping(target = "transactionDate", source = "transactionDate")
|
||||
@Mapping(target = "entryType", source = "type", qualifiedByName = "journalEntryType")
|
||||
@Mapping(target = "amount", source = "amount")
|
||||
@Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType")
|
||||
@Mapping(target = "entityId", source = "entityId")
|
||||
@Mapping(target = "submittedOnDate", source = "submittedOnDate")
|
||||
@Mapping(target = "transactionId", source = "transactionId")
|
||||
@Mapping(target = "currency", source = "currencyCode")
|
||||
@Mapping(target = "manualEntry", source = "manualEntry")
|
||||
@Mapping(target = "reversed", source = "reversed")
|
||||
@Mapping(target = "referenceNumber", source = "referenceNumber")
|
||||
@Mapping(target = "paymentTypeId", source = "paymentDetail.paymentType.id")
|
||||
@Mapping(target = "accountNumber", source = "paymentDetail.accountNumber")
|
||||
@Mapping(target = "checkNumber", source = "paymentDetail.checkNumber")
|
||||
@Mapping(target = "routingCode", source = "paymentDetail.routingCode")
|
||||
@Mapping(target = "receiptNumber", source = "paymentDetail.receiptNumber")
|
||||
@Mapping(target = "bankNumber", source = "paymentDetail.bankNumber")
|
||||
@Mapping(target = "createdDate", ignore = true)
|
||||
@Mapping(target = "createdByUserId", ignore = true)
|
||||
@Mapping(target = "createdByUserName", ignore = true)
|
||||
@Mapping(target = "comments", ignore = true)
|
||||
@Mapping(target = "officeRunningBalance", ignore = true)
|
||||
@Mapping(target = "organizationRunningBalance", ignore = true)
|
||||
@Mapping(target = "runningBalanceComputed", ignore = true)
|
||||
@Mapping(target = "rowIndex", ignore = true)
|
||||
@Mapping(target = "dateFormat", ignore = true)
|
||||
@Mapping(target = "locale", ignore = true)
|
||||
@Mapping(target = "credits", ignore = true)
|
||||
@Mapping(target = "debits", ignore = true)
|
||||
@Mapping(target = "transactionDetails", ignore = true)
|
||||
@Mapping(target = "savingTransactionId", ignore = true)
|
||||
@Mapping(target = "externalAssetOwner", ignore = true)
|
||||
JournalEntryData map(JournalEntry journalEntry);
|
||||
|
||||
@Named("entityType")
|
||||
default PortfolioProductType mapEntityType(Integer entityTypeId) {
|
||||
return PortfolioProductType.fromInt(entityTypeId);
|
||||
}
|
||||
|
||||
@Named("glAccountType")
|
||||
default GLAccountType mapGlAccountType(Integer typeId) {
|
||||
return GLAccountType.fromInt(typeId);
|
||||
}
|
||||
|
||||
@Named("journalEntryType")
|
||||
default JournalEntryType mapJournalEntryType(Integer typeId) {
|
||||
return JournalEntryType.fromInt(typeId);
|
||||
}
|
||||
|
||||
default EnumOptionData mapGlAccountType(GLAccountType accountType) {
|
||||
return new EnumOptionData((long) accountType.getValue(), accountType.getCode(), accountType.name());
|
||||
}
|
||||
|
||||
default EnumOptionData mapJournalEntryType(JournalEntryType journalEntryType) {
|
||||
return new EnumOptionData((long) journalEntryType.getValue(), journalEntryType.getCode(), journalEntryType.name());
|
||||
}
|
||||
|
||||
default EnumOptionData mapEntityType(PortfolioProductType entityType) {
|
||||
return new EnumOptionData((long) entityType.getValue(), entityType.getCode(), entityType.name());
|
||||
}
|
||||
|
||||
default CurrencyData mapCurrency(String currencyCode) {
|
||||
return new CurrencyData(currencyCode);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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 org.apache.fineract.accounting.journalentry;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
||||
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface JournalEntryMapper {
|
||||
|
||||
@Mapping(target = "id", source = "id")
|
||||
@Mapping(target = "officeId", source = "office.id")
|
||||
@Mapping(target = "officeName", source = "office.name")
|
||||
@Mapping(target = "glAccountId", source = "glAccount.id")
|
||||
@Mapping(target = "glAccountCode", source = "glAccount.glCode")
|
||||
@Mapping(target = "glAccountName", source = "glAccount.name")
|
||||
@Mapping(target = "glAccountType", source = "glAccount.type", qualifiedByName = "glAccountType")
|
||||
@Mapping(target = "transactionDate", source = "transactionDate")
|
||||
@Mapping(target = "entryType", source = "type", qualifiedByName = "journalEntryType")
|
||||
@Mapping(target = "amount", source = "amount")
|
||||
@Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType")
|
||||
@Mapping(target = "entityId", source = "entityId")
|
||||
@Mapping(target = "submittedOnDate", expression = "java(journalEntry.getCreatedDate().orElse(null))")
|
||||
@Mapping(target = "transactionId", source = "transactionId")
|
||||
@Mapping(target = "currency", source = "currencyCode")
|
||||
@Mapping(target = "manualEntry", source = "manualEntry")
|
||||
@Mapping(target = "reversed", source = "reversed")
|
||||
@Mapping(target = "referenceNumber", source = "referenceNumber")
|
||||
@Mapping(target = "paymentTypeId", source = "paymentDetail.paymentType.id")
|
||||
@Mapping(target = "accountNumber", source = "paymentDetail.accountNumber")
|
||||
@Mapping(target = "checkNumber", source = "paymentDetail.checkNumber")
|
||||
@Mapping(target = "routingCode", source = "paymentDetail.routingCode")
|
||||
@Mapping(target = "receiptNumber", source = "paymentDetail.receiptNumber")
|
||||
@Mapping(target = "bankNumber", source = "paymentDetail.bankNumber")
|
||||
@Mapping(target = "createdDate", ignore = true)
|
||||
@Mapping(target = "createdByUserId", ignore = true)
|
||||
@Mapping(target = "createdByUserName", ignore = true)
|
||||
@Mapping(target = "comments", ignore = true)
|
||||
@Mapping(target = "officeRunningBalance", ignore = true)
|
||||
@Mapping(target = "organizationRunningBalance", ignore = true)
|
||||
@Mapping(target = "runningBalanceComputed", ignore = true)
|
||||
@Mapping(target = "rowIndex", ignore = true)
|
||||
@Mapping(target = "dateFormat", ignore = true)
|
||||
@Mapping(target = "locale", ignore = true)
|
||||
@Mapping(target = "credits", ignore = true)
|
||||
@Mapping(target = "debits", ignore = true)
|
||||
@Mapping(target = "transactionDetails", ignore = true)
|
||||
@Mapping(target = "savingTransactionId", ignore = true)
|
||||
@Mapping(target = "externalAssetOwner", ignore = true)
|
||||
@Mapping(target = "savingsAccountNo", ignore = true)
|
||||
JournalEntryData map(JournalEntry journalEntry);
|
||||
|
||||
@Named("entityType")
|
||||
default PortfolioProductType mapEntityType(Integer entityTypeId) {
|
||||
return PortfolioProductType.fromInt(entityTypeId);
|
||||
}
|
||||
|
||||
@Named("glAccountType")
|
||||
default GLAccountType mapGlAccountType(Integer typeId) {
|
||||
return GLAccountType.fromInt(typeId);
|
||||
}
|
||||
|
||||
@Named("journalEntryType")
|
||||
default JournalEntryType mapJournalEntryType(Integer typeId) {
|
||||
return JournalEntryType.fromInt(typeId);
|
||||
}
|
||||
|
||||
default EnumOptionData mapGlAccountType(GLAccountType accountType) {
|
||||
return new EnumOptionData((long) accountType.getValue(), accountType.getCode(), accountType.name());
|
||||
}
|
||||
|
||||
default EnumOptionData mapJournalEntryType(JournalEntryType journalEntryType) {
|
||||
return new EnumOptionData((long) journalEntryType.getValue(), journalEntryType.getCode(), journalEntryType.name());
|
||||
}
|
||||
|
||||
default EnumOptionData mapEntityType(PortfolioProductType entityType) {
|
||||
return new EnumOptionData((long) entityType.getValue(), entityType.getCode(), entityType.name());
|
||||
}
|
||||
|
||||
default CurrencyData mapCurrency(String currencyCode) {
|
||||
return new CurrencyData(currencyCode);
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.accounting.journalentry.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DayWiseSummaryData {
|
||||
|
||||
private LocalDate date;
|
||||
private BigDecimal openingBalance;
|
||||
private BigDecimal totalDebit;
|
||||
private BigDecimal totalCredit;
|
||||
private BigDecimal netMovement;
|
||||
private BigDecimal closingBalance;
|
||||
private int entryCount;
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.accounting.journalentry.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GLAccountLedgerReportData {
|
||||
|
||||
/** GL Account details (name, code, type, description, usage) */
|
||||
private GLAccountData glAccount;
|
||||
|
||||
/** Balance of the account before the fromDate (0 if no fromDate or no glAccountId) */
|
||||
private BigDecimal openingBalance;
|
||||
|
||||
/** Balance of the account as of toDate = openingBalance + netMovement */
|
||||
private BigDecimal closingBalance;
|
||||
|
||||
/** Sum of all DEBIT entries in the requested period */
|
||||
private BigDecimal totalDebit;
|
||||
|
||||
/** Sum of all CREDIT entries in the requested period */
|
||||
private BigDecimal totalCredit;
|
||||
|
||||
/**
|
||||
* Net movement: for ASSET/EXPENSE (debit-normal) = totalDebit - totalCredit;
|
||||
* for LIABILITY/INCOME/EQUITY (credit-normal) = totalCredit - totalDebit
|
||||
*/
|
||||
private BigDecimal netMovement;
|
||||
|
||||
/** true for ASSET and EXPENSE accounts (debit-normal balance convention) */
|
||||
private boolean debitNormal;
|
||||
|
||||
private LocalDate fromDate;
|
||||
private LocalDate toDate;
|
||||
|
||||
/** Day-wise breakdown of balances for the period */
|
||||
private List<DayWiseSummaryData> dayWiseSummary;
|
||||
|
||||
/** Total number of journal entries matching the filter (for the KPI card) */
|
||||
private long totalEntries;
|
||||
|
||||
/** Paginated journal entries for the period */
|
||||
private Page<JournalEntryData> entries;
|
||||
}
|
||||
+291
-286
@@ -1,286 +1,291 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.accounting.journalentry.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
|
||||
/**
|
||||
* Immutable object representing a General Ledger Account
|
||||
*
|
||||
* Note: no getter/setters required as google will produce json from fields of object.
|
||||
*/
|
||||
@Data
|
||||
public class JournalEntryData {
|
||||
|
||||
private Long id;
|
||||
private Long officeId;
|
||||
@SuppressWarnings("unused")
|
||||
private String officeName;
|
||||
@SuppressWarnings("unused")
|
||||
private String glAccountName;
|
||||
private Long glAccountId;
|
||||
@SuppressWarnings("unused")
|
||||
private String glAccountCode;
|
||||
private EnumOptionData glAccountType;
|
||||
@SuppressWarnings("unused")
|
||||
private LocalDate transactionDate;
|
||||
private EnumOptionData entryType;
|
||||
private BigDecimal amount;
|
||||
@SuppressWarnings("unused")
|
||||
private CurrencyData currency;
|
||||
private String transactionId;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean manualEntry;
|
||||
@SuppressWarnings("unused")
|
||||
private EnumOptionData entityType;
|
||||
@SuppressWarnings("unused")
|
||||
private Long entityId;
|
||||
@SuppressWarnings("unused")
|
||||
private Long createdByUserId;
|
||||
@SuppressWarnings("unused")
|
||||
private LocalDate createdDate;
|
||||
@SuppressWarnings("unused")
|
||||
private String createdByUserName;
|
||||
@SuppressWarnings("unused")
|
||||
private String comments;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean reversed;
|
||||
@SuppressWarnings("unused")
|
||||
private String referenceNumber;
|
||||
@SuppressWarnings("unused")
|
||||
private BigDecimal officeRunningBalance;
|
||||
@SuppressWarnings("unused")
|
||||
private BigDecimal organizationRunningBalance;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean runningBalanceComputed;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TransactionDetailData transactionDetails;
|
||||
@SuppressWarnings("unused")
|
||||
private LocalDate submittedOnDate;
|
||||
|
||||
// import fields
|
||||
private transient Integer rowIndex;
|
||||
private String dateFormat;
|
||||
private String locale;
|
||||
private List<CreditDebit> credits;
|
||||
private List<CreditDebit> debits;
|
||||
private Long paymentTypeId;
|
||||
private String currencyCode;
|
||||
private String accountNumber;
|
||||
private String checkNumber;
|
||||
private String routingCode;
|
||||
private String receiptNumber;
|
||||
private String bankNumber;
|
||||
private String externalAssetOwner;
|
||||
private transient Long savingTransactionId;
|
||||
|
||||
public JournalEntryData() {}
|
||||
|
||||
// for opening bal bulk import
|
||||
public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
|
||||
List<CreditDebit> debits, String locale, String dateFormat) {
|
||||
this.officeId = officeId;
|
||||
this.dateFormat = dateFormat;
|
||||
this.locale = locale;
|
||||
this.transactionDate = transactionDate;
|
||||
this.currencyCode = currencyCode;
|
||||
this.credits = credits;
|
||||
this.debits = debits;
|
||||
this.rowIndex = null;
|
||||
this.paymentTypeId = null;
|
||||
this.accountNumber = null;
|
||||
this.checkNumber = null;
|
||||
this.routingCode = null;
|
||||
this.receiptNumber = null;
|
||||
this.bankNumber = null;
|
||||
this.comments = null;
|
||||
this.id = null;
|
||||
this.officeName = null;
|
||||
this.glAccountName = null;
|
||||
this.glAccountId = null;
|
||||
this.glAccountCode = null;
|
||||
this.glAccountType = null;
|
||||
this.entryType = null;
|
||||
this.amount = null;
|
||||
this.currency = null;
|
||||
this.transactionId = null;
|
||||
this.manualEntry = null;
|
||||
this.entityType = null;
|
||||
this.entityId = null;
|
||||
this.createdByUserId = null;
|
||||
this.createdDate = null;
|
||||
this.createdByUserName = null;
|
||||
this.reversed = null;
|
||||
this.referenceNumber = null;
|
||||
this.officeRunningBalance = null;
|
||||
this.organizationRunningBalance = null;
|
||||
this.runningBalanceComputed = null;
|
||||
this.transactionDetails = null;
|
||||
this.submittedOnDate = null;
|
||||
}
|
||||
|
||||
private JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, Integer rowIndex,
|
||||
List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber, String routingCode,
|
||||
String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
|
||||
|
||||
this.officeId = officeId;
|
||||
this.dateFormat = dateFormat;
|
||||
this.locale = locale;
|
||||
this.transactionDate = transactionDate;
|
||||
this.currencyCode = currencyCode;
|
||||
this.rowIndex = rowIndex;
|
||||
this.credits = credits;
|
||||
this.debits = debits;
|
||||
this.paymentTypeId = paymentTypeId;
|
||||
this.accountNumber = accountNumber;
|
||||
this.checkNumber = checkNumber;
|
||||
this.routingCode = routingCode;
|
||||
this.receiptNumber = receiptNumber;
|
||||
this.bankNumber = bankNumber;
|
||||
this.comments = comments;
|
||||
this.id = null;
|
||||
this.officeName = null;
|
||||
this.glAccountName = null;
|
||||
this.glAccountId = null;
|
||||
this.glAccountCode = null;
|
||||
this.glAccountType = null;
|
||||
this.entryType = null;
|
||||
this.amount = null;
|
||||
this.currency = null;
|
||||
this.transactionId = null;
|
||||
this.manualEntry = null;
|
||||
this.entityType = null;
|
||||
this.entityId = null;
|
||||
this.createdByUserId = null;
|
||||
this.submittedOnDate = null;
|
||||
this.createdDate = null;
|
||||
this.createdByUserName = null;
|
||||
this.reversed = null;
|
||||
this.referenceNumber = null;
|
||||
this.officeRunningBalance = null;
|
||||
this.organizationRunningBalance = null;
|
||||
this.runningBalanceComputed = null;
|
||||
this.transactionDetails = null;
|
||||
}
|
||||
|
||||
public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId,
|
||||
final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate,
|
||||
final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry,
|
||||
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final LocalDate submittedOnDate,
|
||||
final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber,
|
||||
final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed,
|
||||
final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner) {
|
||||
this.id = id;
|
||||
this.officeId = officeId;
|
||||
this.officeName = officeName;
|
||||
this.glAccountName = glAccountName;
|
||||
this.glAccountId = glAccountId;
|
||||
this.glAccountCode = glAccountCode;
|
||||
this.glAccountType = glAccountClassification;
|
||||
this.transactionDate = transactionDate;
|
||||
this.entryType = entryType;
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
this.manualEntry = manualEntry;
|
||||
this.entityType = entityType;
|
||||
this.entityId = entityId;
|
||||
this.createdByUserId = createdByUserId;
|
||||
this.createdDate = submittedOnDate;
|
||||
this.submittedOnDate = submittedOnDate;
|
||||
this.createdByUserName = createdByUserName;
|
||||
this.comments = comments;
|
||||
this.reversed = reversed;
|
||||
this.referenceNumber = referenceNumber;
|
||||
this.officeRunningBalance = officeRunningBalance;
|
||||
this.organizationRunningBalance = organizationRunningBalance;
|
||||
this.runningBalanceComputed = runningBalanceComputed;
|
||||
this.transactionDetails = transactionDetailData;
|
||||
this.currency = currency;
|
||||
this.externalAssetOwner = externalAssetOwner;
|
||||
}
|
||||
|
||||
public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId,
|
||||
Integer rowIndex, List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber,
|
||||
String routingCode, String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
|
||||
return new JournalEntryData(officeId, transactionDate, currencyCode, paymentTypeId, rowIndex, credits, debits, accountNumber,
|
||||
checkNumber, routingCode, receiptNumber, bankNumber, comments, locale, dateFormat);
|
||||
}
|
||||
|
||||
public static JournalEntryData importInstance1(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
|
||||
List<CreditDebit> debits, String locale, String dateFormat) {
|
||||
return new JournalEntryData(officeId, transactionDate, currencyCode, credits, debits, locale, dateFormat);
|
||||
}
|
||||
|
||||
public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) {
|
||||
|
||||
final Long id = null;
|
||||
final Long officeId = null;
|
||||
final String officeName = null;
|
||||
final String glAccountName = glAccountData.getName();
|
||||
final Long glAccountId = glAccountData.getId();
|
||||
final String glAccountCode = glAccountData.getGlCode();
|
||||
final EnumOptionData glAccountClassification = glAccountData.getType();
|
||||
final LocalDate transactionDate = null;
|
||||
final EnumOptionData entryType = null;
|
||||
final BigDecimal amount = null;
|
||||
final String transactionId = null;
|
||||
final Boolean manualEntry = null;
|
||||
final EnumOptionData entityType = null;
|
||||
final Long entityId = null;
|
||||
final Long createdByUserId = null;
|
||||
final LocalDate submittedOnDate = null;
|
||||
final String createdByUserName = null;
|
||||
final String comments = null;
|
||||
final Boolean reversed = null;
|
||||
final String referenceNumber = null;
|
||||
final BigDecimal officeRunningBalance = null;
|
||||
final BigDecimal organizationRunningBalance = null;
|
||||
final Boolean runningBalanceComputed = null;
|
||||
final TransactionDetailData transactionDetailData = null;
|
||||
final CurrencyData currency = null;
|
||||
final String externalAssetOwner = null;
|
||||
return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification,
|
||||
transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
|
||||
createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance,
|
||||
runningBalanceComputed, transactionDetailData, currency, externalAssetOwner);
|
||||
}
|
||||
|
||||
public Integer getRowIndex() {
|
||||
return rowIndex;
|
||||
}
|
||||
|
||||
public LocalDate getTransactionDate() {
|
||||
return transactionDate;
|
||||
}
|
||||
|
||||
public void addDebits(CreditDebit debit) {
|
||||
this.debits.add(debit);
|
||||
}
|
||||
|
||||
public void addCredits(CreditDebit credit) {
|
||||
this.credits.add(credit);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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 org.apache.fineract.accounting.journalentry.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
|
||||
/**
|
||||
* Immutable object representing a General Ledger Account
|
||||
*
|
||||
* Note: no getter/setters required as google will produce json from fields of object.
|
||||
*/
|
||||
@Data
|
||||
public class JournalEntryData {
|
||||
|
||||
private Long id;
|
||||
private Long officeId;
|
||||
@SuppressWarnings("unused")
|
||||
private String officeName;
|
||||
@SuppressWarnings("unused")
|
||||
private String glAccountName;
|
||||
private Long glAccountId;
|
||||
@SuppressWarnings("unused")
|
||||
private String glAccountCode;
|
||||
private EnumOptionData glAccountType;
|
||||
@SuppressWarnings("unused")
|
||||
private LocalDate transactionDate;
|
||||
private EnumOptionData entryType;
|
||||
private BigDecimal amount;
|
||||
@SuppressWarnings("unused")
|
||||
private CurrencyData currency;
|
||||
private String transactionId;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean manualEntry;
|
||||
@SuppressWarnings("unused")
|
||||
private EnumOptionData entityType;
|
||||
@SuppressWarnings("unused")
|
||||
private Long entityId;
|
||||
@SuppressWarnings("unused")
|
||||
private String savingsAccountNo;
|
||||
@SuppressWarnings("unused")
|
||||
private Long createdByUserId;
|
||||
@SuppressWarnings("unused")
|
||||
private OffsetDateTime createdDate;
|
||||
@SuppressWarnings("unused")
|
||||
private String createdByUserName;
|
||||
@SuppressWarnings("unused")
|
||||
private String comments;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean reversed;
|
||||
@SuppressWarnings("unused")
|
||||
private String referenceNumber;
|
||||
@SuppressWarnings("unused")
|
||||
private BigDecimal officeRunningBalance;
|
||||
@SuppressWarnings("unused")
|
||||
private BigDecimal organizationRunningBalance;
|
||||
@SuppressWarnings("unused")
|
||||
private Boolean runningBalanceComputed;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TransactionDetailData transactionDetails;
|
||||
@SuppressWarnings("unused")
|
||||
private OffsetDateTime submittedOnDate;
|
||||
|
||||
// import fields
|
||||
private transient Integer rowIndex;
|
||||
private String dateFormat;
|
||||
private String locale;
|
||||
private List<CreditDebit> credits;
|
||||
private List<CreditDebit> debits;
|
||||
private Long paymentTypeId;
|
||||
private String currencyCode;
|
||||
private String accountNumber;
|
||||
private String checkNumber;
|
||||
private String routingCode;
|
||||
private String receiptNumber;
|
||||
private String bankNumber;
|
||||
private String externalAssetOwner;
|
||||
private transient Long savingTransactionId;
|
||||
|
||||
public JournalEntryData() {}
|
||||
|
||||
// for opening bal bulk import
|
||||
public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
|
||||
List<CreditDebit> debits, String locale, String dateFormat) {
|
||||
this.officeId = officeId;
|
||||
this.dateFormat = dateFormat;
|
||||
this.locale = locale;
|
||||
this.transactionDate = transactionDate;
|
||||
this.currencyCode = currencyCode;
|
||||
this.credits = credits;
|
||||
this.debits = debits;
|
||||
this.rowIndex = null;
|
||||
this.paymentTypeId = null;
|
||||
this.accountNumber = null;
|
||||
this.checkNumber = null;
|
||||
this.routingCode = null;
|
||||
this.receiptNumber = null;
|
||||
this.bankNumber = null;
|
||||
this.comments = null;
|
||||
this.id = null;
|
||||
this.officeName = null;
|
||||
this.glAccountName = null;
|
||||
this.glAccountId = null;
|
||||
this.glAccountCode = null;
|
||||
this.glAccountType = null;
|
||||
this.entryType = null;
|
||||
this.amount = null;
|
||||
this.currency = null;
|
||||
this.transactionId = null;
|
||||
this.manualEntry = null;
|
||||
this.entityType = null;
|
||||
this.entityId = null;
|
||||
this.createdByUserId = null;
|
||||
this.createdDate = null;
|
||||
this.createdByUserName = null;
|
||||
this.reversed = null;
|
||||
this.referenceNumber = null;
|
||||
this.officeRunningBalance = null;
|
||||
this.organizationRunningBalance = null;
|
||||
this.runningBalanceComputed = null;
|
||||
this.transactionDetails = null;
|
||||
this.submittedOnDate = null;
|
||||
}
|
||||
|
||||
private JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, Integer rowIndex,
|
||||
List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber, String routingCode,
|
||||
String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
|
||||
|
||||
this.officeId = officeId;
|
||||
this.dateFormat = dateFormat;
|
||||
this.locale = locale;
|
||||
this.transactionDate = transactionDate;
|
||||
this.currencyCode = currencyCode;
|
||||
this.rowIndex = rowIndex;
|
||||
this.credits = credits;
|
||||
this.debits = debits;
|
||||
this.paymentTypeId = paymentTypeId;
|
||||
this.accountNumber = accountNumber;
|
||||
this.checkNumber = checkNumber;
|
||||
this.routingCode = routingCode;
|
||||
this.receiptNumber = receiptNumber;
|
||||
this.bankNumber = bankNumber;
|
||||
this.comments = comments;
|
||||
this.id = null;
|
||||
this.officeName = null;
|
||||
this.glAccountName = null;
|
||||
this.glAccountId = null;
|
||||
this.glAccountCode = null;
|
||||
this.glAccountType = null;
|
||||
this.entryType = null;
|
||||
this.amount = null;
|
||||
this.currency = null;
|
||||
this.transactionId = null;
|
||||
this.manualEntry = null;
|
||||
this.entityType = null;
|
||||
this.entityId = null;
|
||||
this.createdByUserId = null;
|
||||
this.submittedOnDate = null;
|
||||
this.createdDate = null;
|
||||
this.createdByUserName = null;
|
||||
this.reversed = null;
|
||||
this.referenceNumber = null;
|
||||
this.officeRunningBalance = null;
|
||||
this.organizationRunningBalance = null;
|
||||
this.runningBalanceComputed = null;
|
||||
this.transactionDetails = null;
|
||||
}
|
||||
|
||||
public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId,
|
||||
final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate,
|
||||
final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry,
|
||||
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate,
|
||||
final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber,
|
||||
final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed,
|
||||
final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner,
|
||||
final String savingsAccountNo) {
|
||||
this.id = id;
|
||||
this.officeId = officeId;
|
||||
this.officeName = officeName;
|
||||
this.glAccountName = glAccountName;
|
||||
this.glAccountId = glAccountId;
|
||||
this.glAccountCode = glAccountCode;
|
||||
this.glAccountType = glAccountClassification;
|
||||
this.transactionDate = transactionDate;
|
||||
this.entryType = entryType;
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
this.manualEntry = manualEntry;
|
||||
this.entityType = entityType;
|
||||
this.entityId = entityId;
|
||||
this.createdByUserId = createdByUserId;
|
||||
this.createdDate = submittedOnDate;
|
||||
this.submittedOnDate = submittedOnDate;
|
||||
this.createdByUserName = createdByUserName;
|
||||
this.comments = comments;
|
||||
this.reversed = reversed;
|
||||
this.referenceNumber = referenceNumber;
|
||||
this.officeRunningBalance = officeRunningBalance;
|
||||
this.organizationRunningBalance = organizationRunningBalance;
|
||||
this.runningBalanceComputed = runningBalanceComputed;
|
||||
this.transactionDetails = transactionDetailData;
|
||||
this.currency = currency;
|
||||
this.externalAssetOwner = externalAssetOwner;
|
||||
this.savingsAccountNo = savingsAccountNo;
|
||||
}
|
||||
|
||||
public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId,
|
||||
Integer rowIndex, List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber,
|
||||
String routingCode, String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
|
||||
return new JournalEntryData(officeId, transactionDate, currencyCode, paymentTypeId, rowIndex, credits, debits, accountNumber,
|
||||
checkNumber, routingCode, receiptNumber, bankNumber, comments, locale, dateFormat);
|
||||
}
|
||||
|
||||
public static JournalEntryData importInstance1(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
|
||||
List<CreditDebit> debits, String locale, String dateFormat) {
|
||||
return new JournalEntryData(officeId, transactionDate, currencyCode, credits, debits, locale, dateFormat);
|
||||
}
|
||||
|
||||
public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) {
|
||||
|
||||
final Long id = null;
|
||||
final Long officeId = null;
|
||||
final String officeName = null;
|
||||
final String glAccountName = glAccountData.getName();
|
||||
final Long glAccountId = glAccountData.getId();
|
||||
final String glAccountCode = glAccountData.getGlCode();
|
||||
final EnumOptionData glAccountClassification = glAccountData.getType();
|
||||
final LocalDate transactionDate = null;
|
||||
final EnumOptionData entryType = null;
|
||||
final BigDecimal amount = null;
|
||||
final String transactionId = null;
|
||||
final Boolean manualEntry = null;
|
||||
final EnumOptionData entityType = null;
|
||||
final Long entityId = null;
|
||||
final Long createdByUserId = null;
|
||||
final OffsetDateTime submittedOnDate = null;
|
||||
final String createdByUserName = null;
|
||||
final String comments = null;
|
||||
final Boolean reversed = null;
|
||||
final String referenceNumber = null;
|
||||
final BigDecimal officeRunningBalance = null;
|
||||
final BigDecimal organizationRunningBalance = null;
|
||||
final Boolean runningBalanceComputed = null;
|
||||
final TransactionDetailData transactionDetailData = null;
|
||||
final CurrencyData currency = null;
|
||||
final String externalAssetOwner = null;
|
||||
return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification,
|
||||
transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
|
||||
createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance,
|
||||
runningBalanceComputed, transactionDetailData, currency, externalAssetOwner, null);
|
||||
}
|
||||
|
||||
public Integer getRowIndex() {
|
||||
return rowIndex;
|
||||
}
|
||||
|
||||
public LocalDate getTransactionDate() {
|
||||
return transactionDate;
|
||||
}
|
||||
|
||||
public void addDebits(CreditDebit debit) {
|
||||
this.debits.add(debit);
|
||||
}
|
||||
|
||||
public void addCredits(CreditDebit credit) {
|
||||
this.credits.add(credit);
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.accounting.journalentry.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -41,4 +42,27 @@ public interface JournalEntryRepository extends JpaRepository<JournalEntry, Long
|
||||
@Query("select journalEntry from JournalEntry journalEntry where journalEntry.transactionId= :transactionId and journalEntry.reversed=false and journalEntry.entityType = :entityType order by journalEntry.transactionDate asc, journalEntry.createdDate asc, journalEntry.id asc")
|
||||
List<JournalEntry> findJournalEntries(@Param("transactionId") String transactionId, @Param("entityType") Integer entityType);
|
||||
|
||||
@Query("""
|
||||
SELECT DISTINCT je.transactionDate
|
||||
FROM JournalEntry je
|
||||
WHERE je.transactionDate>:afterDate
|
||||
""")
|
||||
List<LocalDate> findTransactionDatesAfter(LocalDate afterDate);
|
||||
|
||||
@Query("""
|
||||
SELECT je.office.id,
|
||||
je.glAccount.id,
|
||||
SUM(CASE WHEN je.type = 1
|
||||
THEN -1 * je.amount
|
||||
ELSE je.amount
|
||||
END),
|
||||
je.transactionDate,
|
||||
je.createdDate,
|
||||
SUM(je.amount)
|
||||
FROM JournalEntry je
|
||||
WHERE je.transactionDate = :transactionDate
|
||||
GROUP BY je.office.id, je.glAccount.id, je.transactionDate, je.createdDate
|
||||
""")
|
||||
List<Object[]> findTrialBalanceLinesForDate(@Param("transactionDate") LocalDate transactionDate);
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -45,10 +45,14 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class JournalEntryCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<JournalEntryCommand> {
|
||||
public class JournalEntryCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<JournalEntryCommand> {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
protected Set<String> getSupportedParameters() {
|
||||
return JournalEntryJsonInputParams.getAllValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JournalEntryCommand commandFromApiJson(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
@@ -56,7 +60,7 @@ public final class JournalEntryCommandFromApiJsonDeserializer extends AbstractFr
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
final Set<String> supportedParameters = JournalEntryJsonInputParams.getAllValues();
|
||||
final Set<String> supportedParameters = this.getSupportedParameters();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user