@
feat(fundtransfer+imps+mfa): fund transfer module, IMPS switch client, MFA log API - Fund transfer: NEFT/RTGS/IMPS/internal strategies, limit resolution, bulk transfer, verification tokens, suspense GL, recall/approval flow, jobs - IMPS CBS switch client with crypto (JWT/envelope) and transaction domain - MFA log read API (audit of MFA events) - Liquibase migrations 0300-0311 for fund transfer / IMPS tables, permissions, roles, and suspense account - JobName, SecurityConfig, dependencies.gradle wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -1 +1 @@
|
||||
{"sessionId":"0fda6e48-a79d-4980-a9ee-31a6d5f735e1","pid":15548,"acquiredAt":1776168814856}
|
||||
{"sessionId":"b5222d29-c337-4c5d-afde-70cde98cdf46","pid":426448,"acquiredAt":1780052566378}
|
||||
@@ -52,7 +52,32 @@
|
||||
"Bash(git -c commit.gpgsign=false commit -m ' *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)",
|
||||
"Bash(git ls-remote *)"
|
||||
"Bash(git ls-remote *)",
|
||||
"Bash(PGPASSWORD=root psql -h localhost -p 5432 -U postgres -d fineract_default -c \"SELECT cv.id, cv.code_value FROM m_code_value cv JOIN m_code c ON cv.code_id=c.id WHERE c.code_name='Constitution' ORDER BY cv.id;\")",
|
||||
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AQRPV8345E\",\"name\":\"Aditya Vyas\",\"dob\":\"19/12/1988\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
|
||||
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"GIZMOGLADE PRIVATE LIMITED\",\"dob\":\"01/12/2025\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
|
||||
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"\",\"dob\":\"\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
|
||||
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"21AAMCG5406BIZ6\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
|
||||
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"23AADCL9984A1ZH\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
|
||||
"PowerShell(Get-Process | Where-Object { $_.ProcessName -match '^\\(java|gradle\\)' } | Select-Object Id, ProcessName, StartTime, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}} | Format-Table -AutoSize)",
|
||||
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -ErrorAction SilentlyContinue)",
|
||||
"PowerShell(Stop-Process -Id 3468,3864,39964 -Force)",
|
||||
"PowerShell(Get-Process -Id 3468,3864,39964 -ErrorAction SilentlyContinue)",
|
||||
"PowerShell(foreach \\($id in 3468,3864,39964\\) { try { Stop-Process -Id $id -Force -ErrorAction Stop; \"killed $id\" } catch { \"skip $id - $\\($_.Exception.Message\\)\" } })",
|
||||
"PowerShell(Get-Process java -ErrorAction SilentlyContinue)",
|
||||
"PowerShell(if \\(-not \\(Test-Path .\\\\fineract-provider\\\\logs\\)\\) { New-Item -ItemType Directory -Force .\\\\fineract-provider\\\\logs | Out-Null }; Remove-Item .\\\\fineract-provider\\\\logs\\\\devrun.out -ErrorAction SilentlyContinue; \"ready\")",
|
||||
"PowerShell($j = Get-Process java -ErrorAction SilentlyContinue | Select-Object Id, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}}, StartTime; $j | Format-Table -AutoSize; \"---\"; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, OwningProcess)",
|
||||
"PowerShell(if \\(Test-Path .\\\\fineract-provider\\\\logs\\\\devrun.out\\) { $f = Get-Item .\\\\fineract-provider\\\\logs\\\\devrun.out; \"size=$\\($f.Length\\) bytes mtime=$\\($f.LastWriteTime\\)\" } else { \"devrun.out missing\" })",
|
||||
"PowerShell(netstat -ano)",
|
||||
"PowerShell(Get-Process java -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 800; $left = Get-Process java -ErrorAction SilentlyContinue; if \\($left\\) { $left | Select-Object Id, ProcessName } else { \"no java processes\" }; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Measure-Object | ForEach-Object { \"8443 listeners: $\\($_.Count\\)\" })",
|
||||
"Bash(curl -sk -o /tmp/resp.txt -w \"STATUS=%{http_code}\\\\nCONTENT_TYPE=%{content_type}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
|
||||
"Bash(curl -sk -o /tmp/r1.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
|
||||
"Bash(curl -s -o /tmp/r2.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
|
||||
"Bash(curl -s -o /tmp/r3.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Origin: http://localhost:4200' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
|
||||
"Bash(curl -s -o /tmp/r4.txt -w \"STATUS=%{http_code}\\\\n\" -X OPTIONS 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Origin: http://localhost:4200' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,fineract-platform-tenantid' -i)",
|
||||
"Bash(git cherry-pick *)",
|
||||
"PowerShell($parent = 'C:\\\\Users\\\\adity\\\\Music\\\\CBS'; if \\(Test-Path $parent\\) { \"parent exists: $parent\"; Get-ChildItem $parent -Directory | Select-Object -ExpandProperty Name } else { \"MISSING parent: $parent\" }; \"---\"; $target = Join-Path $parent 'LincpayEKYC'; if \\(Test-Path $target\\) { \"TARGET ALREADY EXISTS: $target\" } else { \"target free: $target\" })",
|
||||
"Bash(git clone *)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api",
|
||||
|
||||
+3
@@ -61,6 +61,9 @@ public enum JobName {
|
||||
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
|
||||
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
|
||||
FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), //
|
||||
IMPS_DEEMED_RECONCILE("IMPS DEEMED Reconciliation"), //
|
||||
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
||||
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
||||
; //
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -99,6 +99,10 @@ dependencies {
|
||||
// Bundles ZXing transitively for QR PNG output (used by ZxingPngQrGenerator).
|
||||
'dev.samstevens.totp:totp:1.7.1',
|
||||
|
||||
// Nimbus JOSE+JWT — RS256 sign/verify for the IMPS_BANK_API_SPEC_CBS_1.0 envelope
|
||||
// (see org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil).
|
||||
'com.nimbusds:nimbus-jose-jwt:9.37',
|
||||
|
||||
'org.apache.commons:commons-lang3',
|
||||
'commons-io:commons-io',
|
||||
'org.apache.poi:poi',
|
||||
|
||||
@@ -0,0 +1,975 @@
|
||||
> 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 - 7dea88d30f72365945bed2160d2ba72dccdc80b7
|
||||
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: 1.15.0-SNAPSHOT
|
||||
|
||||
> Task :spotlessInternalRegisterDependencies UP-TO-DATE
|
||||
> Task :fineract-current-product:processResources NO-SOURCE
|
||||
> Task :fineract-mix:processResources NO-SOURCE
|
||||
> Task :fineract-rates:processResources UP-TO-DATE
|
||||
> Task :fineract-core:processResources UP-TO-DATE
|
||||
> Task :fineract-cob:processResources UP-TO-DATE
|
||||
> Task :fineract-branch:processResources UP-TO-DATE
|
||||
> Task :fineract-security:processResources NO-SOURCE
|
||||
> Task :fineract-loan-origination:processResources UP-TO-DATE
|
||||
> Task :fineract-savings:processResources UP-TO-DATE
|
||||
> Task :fineract-accounting:processResources UP-TO-DATE
|
||||
> Task :fineract-report:processResources UP-TO-DATE
|
||||
> Task :fineract-charge:processResources UP-TO-DATE
|
||||
> Task :fineract-investor:processResources UP-TO-DATE
|
||||
> Task :fineract-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-client:processResources UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-working-capital-loan:processResources UP-TO-DATE
|
||||
> Task :fineract-document:processResources NO-SOURCE
|
||||
> Task :fineract-tax:processResources UP-TO-DATE
|
||||
> Task :fineract-avro-schemas:preprocessAvroSchemas 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-validation:classes UP-TO-DATE
|
||||
> Task :fineract-command:processResources UP-TO-DATE
|
||||
> Task :fineract-command: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-tax:compileJava UP-TO-DATE
|
||||
> Task :fineract-mix:classes UP-TO-DATE
|
||||
> Task :fineract-report:compileJava UP-TO-DATE
|
||||
> Task :fineract-tax:classes UP-TO-DATE
|
||||
> Task :fineract-mix:jar UP-TO-DATE
|
||||
> Task :fineract-rates:compileJava UP-TO-DATE
|
||||
> Task :fineract-report:classes UP-TO-DATE
|
||||
> Task :fineract-cob:compileJava UP-TO-DATE
|
||||
> Task :fineract-rates:classes UP-TO-DATE
|
||||
> Task :fineract-cob:classes UP-TO-DATE
|
||||
> Task :fineract-tax:jar UP-TO-DATE
|
||||
> Task :fineract-report:jar UP-TO-DATE
|
||||
> Task :fineract-cob:jar UP-TO-DATE
|
||||
> Task :fineract-rates:jar UP-TO-DATE
|
||||
> Task :fineract-document:compileJava UP-TO-DATE
|
||||
> Task :fineract-document:classes UP-TO-DATE
|
||||
> Task :fineract-security:compileJava UP-TO-DATE
|
||||
> Task :fineract-document:jar UP-TO-DATE
|
||||
> Task :fineract-security:classes UP-TO-DATE
|
||||
> Task :fineract-security:jar UP-TO-DATE
|
||||
> Task :fineract-charge:compileJava UP-TO-DATE
|
||||
> Task :fineract-charge:classes UP-TO-DATE
|
||||
> Task :fineract-charge: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-loan:compileJava UP-TO-DATE
|
||||
> Task :fineract-loan:classes UP-TO-DATE
|
||||
> Task :fineract-branch:jar UP-TO-DATE
|
||||
> Task :fineract-loan:jar UP-TO-DATE
|
||||
> Task :fineract-savings:compileJava UP-TO-DATE
|
||||
> Task :fineract-savings:classes UP-TO-DATE
|
||||
> Task :fineract-savings:jar UP-TO-DATE
|
||||
> Task :fineract-loan-origination:compileJava UP-TO-DATE
|
||||
> Task :fineract-investor:compileJava UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:compileJava UP-TO-DATE
|
||||
> Task :fineract-loan-origination:classes UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:classes UP-TO-DATE
|
||||
> Task :fineract-investor:classes UP-TO-DATE
|
||||
> Task :fineract-progressive-loan:jar UP-TO-DATE
|
||||
> Task :fineract-loan-origination:jar UP-TO-DATE
|
||||
> Task :fineract-investor:jar UP-TO-DATE
|
||||
> Task :fineract-current-product:compileJava UP-TO-DATE
|
||||
> Task :fineract-current-product:classes UP-TO-DATE
|
||||
> Task :fineract-current-product:jar UP-TO-DATE
|
||||
> Task :fineract-provider:generateGitProperties
|
||||
> Task :fineract-provider:compileJava UP-TO-DATE
|
||||
> Task :fineract-provider:generateRebelMain UP-TO-DATE
|
||||
> Task :fineract-provider:processResources
|
||||
> Task :fineract-provider:classes
|
||||
> Task :fineract-provider:compileTestJava UP-TO-DATE
|
||||
> Task :fineract-provider:prepareInputYaml UP-TO-DATE
|
||||
> Task :fineract-provider:processTestResources UP-TO-DATE
|
||||
> Task :fineract-provider:testClasses UP-TO-DATE
|
||||
> Task :fineract-provider:resolve
|
||||
> Task :fineract-provider:resolveMainClassName
|
||||
|
||||
> Task :fineract-provider:bootRun
|
||||
19:17:05,132 |-WARN in ch.qos.logback.core.joran.action.ConversionRuleAction - [converterClass] attribute is deprecated and replaced by [class]. See element [conversionRule] near line 30
|
||||
|
||||
_ _ _____ _ _
|
||||
/ \ _ __ __ _ ___| |__ ___ | ___(_)_ __ ___ _ __ __ _ ___| |_
|
||||
/ _ \ | '_ \ / _` |/ __| '_ \ / _ \ | |_ | | '_ \ / _ \ '__/ _` |/ __| __|
|
||||
/ ___ \| |_) | (_| | (__| | | | __/ | _| | | | | | __/ | | (_| | (__| |_
|
||||
/_/ \_\ .__/ \__,_|\___|_| |_|\___| |_| |_|_| |_|\___|_| \__,_|\___|\__|
|
||||
|_|
|
||||
|
||||
[2m[37m(c) 2015-2024 Apache Fineract (https://fineract.apache.org)[30m
|
||||
|
||||
[2m[37mPowered by Spring Boot 3.5.6[30m
|
||||
|
||||
|
||||
[2m2026-05-29 19:17:05.618[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36morg.apache.fineract.ServerApplication [0;39m [2m:[0;39m Starting ServerApplication using Java 21.0.9 with PID 7760 (C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main started by adity in C:\Users\adity\Music\CBS\fineract\fineract-provider)
|
||||
[2m2026-05-29 19:17:05.622[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36morg.apache.fineract.ServerApplication [0;39m [2m:[0;39m No active profile set, falling back to 1 default profile: "default"
|
||||
[2m2026-05-29 19:17:10.836[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
[2m2026-05-29 19:17:10.836[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
[2m2026-05-29 19:17:11.794[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:11.797[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:11.811[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.mix.domain.MixReportXBRLNamespaceRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:11.813[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.mix.domain.MixTaxonomyMappingRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:11.815[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.mix.domain.MixTaxonomyRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:11.887[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JPA - Could not safely identify store assignment for repository candidate interface org.apache.fineract.command.persistence.domain.CommandRepository; If you want this repository to be a JPA repository, consider annotating your entities with one of these annotations: jakarta.persistence.Entity, jakarta.persistence.MappedSuperclass (preferred), or consider extending one of the following types with your repository: org.springframework.data.jpa.repository.JpaRepository
|
||||
[2m2026-05-29 19:17:13.120[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Finished Spring Data repository scanning in 2267 ms. Found 224 JPA repository interfaces.
|
||||
[2m2026-05-29 19:17:13.130[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.f.s.DefaultListableBeanFactory [0;39m [2m:[0;39m Overriding bean definition for bean 'jpaAuditingHandler' with a different definition: replacing [Root bean: class=org.springframework.data.auditing.AuditingHandler; scope=; abstract=false; lazyInit=null; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=from; initMethodNames=null; destroyMethodNames=null] with [Root bean: class=org.apache.fineract.infrastructure.core.auditing.CustomAuditingHandler; scope=prototype; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null]
|
||||
[2m2026-05-29 19:17:13.204[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.f.s.DefaultListableBeanFactory [0;39m [2m:[0;39m Overriding bean definition for bean 'transactionManager' with a different definition: replacing [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=JPAConfig; factoryMethodName=transactionManager; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [org/apache/fineract/infrastructure/core/config/jpa/JPAConfig.class]] with [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=scheduledJobRunnerConfig; factoryMethodName=transactionManager; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.class]]
|
||||
[2m2026-05-29 19:17:13.206[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.c.c.annotation.BatchRegistrar [0;39m [2m:[0;39m Bean jobRepository already defined in the application context, skipping the registration of a jobRepository
|
||||
[2m2026-05-29 19:17:13.206[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.c.c.annotation.BatchRegistrar [0;39m [2m:[0;39m Bean jobExplorer already defined in the application context, skipping the registration of a jobExplorer
|
||||
[2m2026-05-29 19:17:13.206[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.c.c.annotation.BatchRegistrar [0;39m [2m:[0;39m Bean jobLauncher already defined in the application context, skipping the registration of a jobLauncher
|
||||
[2m2026-05-29 19:17:13.208[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.c.c.annotation.BatchRegistrar [0;39m [2m:[0;39m Finished Spring Batch infrastructure beans configuration in 3 ms.
|
||||
[2m2026-05-29 19:17:14.107[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
[2m2026-05-29 19:17:14.108[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Bootstrapping Spring Data JDBC repositories in DEFAULT mode.
|
||||
[2m2026-05-29 19:17:14.408[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.provisioning.domain.ProvisioningEntryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.409[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.adhocquery.domain.AdHocRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.409[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.cob.domain.LoanAccountLockRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.bulkimport.domain.ImportDocumentRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.campaigns.email.domain.EmailConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.410[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.configuration.domain.ExternalServicesPropertiesRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.412[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.CreditBureauConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.412[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.CreditBureauLoanProductMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.412[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.CreditBureauRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.413[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.CreditReportRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.413[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.OrganisationCreditBureauRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.413[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.creditbureau.domain.TokenRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.414[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.dataqueries.domain.EntityDatatableChecksRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.414[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.dataqueries.domain.RegisteredDatatableRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.414[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.dataqueries.domain.ReportParameterRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.414[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.dataqueries.domain.ReportParameterUsageRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.414[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.415[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.415[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.415[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.415[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.hooks.domain.HookConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.415[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.hooks.domain.HookRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.416[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.hooks.domain.HookTemplateRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.416[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.imps.domain.ImpsTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.416[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.domain.JobParameterRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.416[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.417[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.domain.ScheduledJobRunHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.417[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.domain.SchedulerDetailRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.417[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntryAggregationTrackingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.417[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntrySummaryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.417[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.419[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.419[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.420[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.security.otp.domain.OtpLogRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.421[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.421[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.survey.domain.LikelihoodRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.421[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.interoperation.domain.InteropIdentifierRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.421[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.notification.domain.NotificationMapperRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.422[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.notification.domain.NotificationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.422[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.holiday.domain.HolidayRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.422[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.office.domain.OfficeTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.423[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.provisioning.domain.ProvisioningCategoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.423[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.provisioning.domain.ProvisioningCriteriaDefinitionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.423[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.provisioning.domain.ProvisioningCriteriaRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.424[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.staff.domain.StaffRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.424[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.424[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.425[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.425[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.account.domain.AccountTransferRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.426[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.426[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.address.domain.AddressRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.426[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.address.domain.FieldConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.calendar.domain.CalendarHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.calendar.domain.CalendarRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.ckyc.domain.CkycBatchRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientAddressRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.427[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientChargeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.428[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientFamilyMembersRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.428[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientNonPersonRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.428[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.428[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientTransferDetailsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.429[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.430[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomainRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.430[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.collateral.domain.LoanCollateralRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.431[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.BankIfscRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.431[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.CustomerTransferLimitRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.431[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEventRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.FundTransferRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.TransferLimitConfigRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.fund.domain.FundRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.group.domain.GroupLevelRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.432[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.group.domain.GroupRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.433[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.group.domain.GroupRoleRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.433[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.guarantor.domain.GuarantorFundingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.433[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.guarantor.domain.GuarantorFundingTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.433[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.guarantor.domain.GuarantorRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.433[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanproduct.productmix.domain.ProductMixRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.meeting.attendance.domain.ClientAttendanceRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.meeting.domain.MeetingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.note.domain.NoteRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.rate.domain.RateRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.434[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.435[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.438[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.FixedDepositAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.438[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.FixedDepositProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.438[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateSlabHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RdInterestRateEffectiveRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RdInterestRateSlabHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RecurringDepositAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.self.account.domain.SelfBeneficiariesTPTRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.self.device.domain.DeviceRegistrationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.439[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.self.pockets.domain.PocketAccountMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.440[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.self.pockets.domain.PocketRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.440[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.self.registration.domain.SelfServiceRegistrationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.440[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.440[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.440[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.441[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.441[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.spm.domain.LookupTableRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.442[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.spm.domain.ScorecardRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.444[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.spm.domain.SurveyRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.444[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.template.domain.TemplateRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.445[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.domain.AppUserPreviousPasswordRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.445[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.domain.AppUserRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.446[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.domain.PasswordValidationPolicyRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.446[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.domain.PermissionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.446[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.domain.RoleRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.446[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.446[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.447[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.447[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.447[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.447[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.448[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.448[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.448[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.449[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.449[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.449[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanAccrualActivityRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.450[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidByRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagementRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.452[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanStatusChangeHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.453[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.453[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.453[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.453[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.453[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleHistoryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.454[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.454[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.454[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.454[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.455[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlabRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.RecurringDepositProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfigRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.456[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.cob.domain.BatchBusinessStepRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.teller.domain.CashierRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.457[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.teller.domain.CashierTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.teller.domain.TellerRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.teller.domain.TellerTransactionRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.closure.domain.GLClosureRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.458[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.459[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.459[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.rule.domain.AccountingRuleRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.459[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.floatingrates.domain.FloatingRateRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.459[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.charge.domain.ChargeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.462[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.tax.domain.TaxComponentRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.462[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.tax.domain.TaxGroupRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.462[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.security.domain.TFAccessTokenRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.462[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.security.domain.TwoFactorConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.464[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.accounting.glaccount.domain.GLAccountRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.commands.domain.CommandSourceRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.businessdate.domain.BusinessDateRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.cache.domain.PlatformCacheRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.codes.domain.CodeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.466[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.infrastructure.rbifeed.domain.RbiFeedItemRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.organisation.office.domain.OfficeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.cfcfrms.domain.CfcfrmsSimulatorLogRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.467[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.cfcfrms.domain.ComplaintAuditLogRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.468[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.cfcfrms.domain.ComplaintTicketRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.468[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientForm60Repository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.470[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientIdentifierRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.470[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientKycRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientNomineeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.domain.ClientRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientEouRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExportRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsmeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.471[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrowerRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySectorRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySectorRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndiaRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetailRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.472[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.sanctions.domain.SanctionsEntryRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.473[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.473[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.dsc.domain.BankUserDscRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.473[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.dsc.domain.DscChallengeRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.473[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.RepositoryConfigurationExtensionSupport[0;39m [2m:[0;39m Spring Data JDBC - Could not safely identify store assignment for repository candidate interface org.apache.fineract.useradministration.mfa.domain.UserMfaFactorRepository; If you want this repository to be a JDBC repository, consider annotating your entities with one of these annotations: org.springframework.data.relational.core.mapping.Table.
|
||||
[2m2026-05-29 19:17:14.768[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.d.r.c.RepositoryConfigurationDelegate[0;39m [2m:[0;39m Finished Spring Data repository scanning in 657 ms. Found 6 JDBC repository interfaces.
|
||||
[2m2026-05-29 19:17:16.886[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mfaultConfiguringBeanFactoryPostProcessor[0;39m [2m:[0;39m No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.
|
||||
[2m2026-05-29 19:17:16.895[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mfaultConfiguringBeanFactoryPostProcessor[0;39m [2m:[0;39m No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.
|
||||
[2m2026-05-29 19:17:18.104[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'fineract-org.apache.fineract.infrastructure.core.config.FineractProperties' of type [org.apache.fineract.infrastructure.core.config.FineractProperties] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.117[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'cacheConfig' of type [org.apache.fineract.infrastructure.core.config.cache.CacheConfig$$SpringCGLIB$$0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.335[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36morg.reflections.Reflections [0;39m [2m:[0;39m Reflections took 7 ms to scan 1 urls, producing 0 keys and 0 values
|
||||
[2m2026-05-29 19:17:18.339[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.a.f.i.core.config.cache.CacheConfig [0;39m [2m:[0;39m The following cache configurations are defined but cache does not exists: [userTFAccessToken]
|
||||
[2m2026-05-29 19:17:18.343[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'ehCacheManager' of type [org.springframework.cache.jcache.JCacheCacheManager] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.354[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'defaultCacheManager' of type [org.apache.fineract.infrastructure.core.config.cache.TransactionBoundCacheManager] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.360[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'runtimeDelegatingCacheManager' of type [org.apache.fineract.infrastructure.cache.service.RuntimeDelegatingCacheManager] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.364[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'platformCacheConfiguration' of type [org.apache.fineract.infrastructure.cache.PlatformCacheConfiguration$$SpringCGLIB$$0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.407[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'samplingConfiguration' of type [org.apache.fineract.infrastructure.core.diagnostics.performance.sampling.core.SamplingConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:18.415[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mtrationDelegate$BeanPostProcessorChecker[0;39m [2m:[0;39m Bean 'samplingServiceFactory' of type [org.apache.fineract.infrastructure.core.diagnostics.performance.sampling.core.SamplingServiceFactory] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [samplingBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
|
||||
[2m2026-05-29 19:17:19.255[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.w.embedded.tomcat.TomcatWebServer [0;39m [2m:[0;39m Tomcat initialized with port 8443 (https)
|
||||
[2m2026-05-29 19:17:19.278[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.apache.catalina.core.StandardService [0;39m [2m:[0;39m Starting service [Tomcat]
|
||||
[2m2026-05-29 19:17:19.279[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.apache.catalina.core.StandardEngine [0;39m [2m:[0;39m Starting Servlet engine: [Apache Tomcat/10.1.49]
|
||||
[2m2026-05-29 19:17:19.328[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.a.c.c.C.[.[.[/fineract-provider] [0;39m [2m:[0;39m Initializing Spring embedded WebApplicationContext
|
||||
[2m2026-05-29 19:17:19.329[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mw.s.c.ServletWebServerApplicationContext[0;39m [2m:[0;39m Root WebApplicationContext: initialization completed in 13673 ms
|
||||
[2m2026-05-29 19:17:19.577[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-1 - Starting...
|
||||
[2m2026-05-29 19:17:19.860[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mcom.zaxxer.hikari.pool.HikariPool [0;39m [2m:[0;39m HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@5917b984
|
||||
[2m2026-05-29 19:17:19.861[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-1 - Start completed.
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m Upgrading tenant store DB at localhost:5432
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.username: postgres
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.password: ****
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.parameters:
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.timezone: Asia/Kolkata
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.description: Default Demo Tenant
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.identifier: default
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - fineract.tenant.name: fineract_default
|
||||
[2m2026-05-29 19:17:20.050[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m - liquibase.analytics.enabled: false
|
||||
[2m2026-05-29 19:17:25.487[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m Tenant store upgrade finished
|
||||
[2m2026-05-29 19:17:25.487[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m Upgrading all tenants
|
||||
[2m2026-05-29 19:17:25.501[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m Upgrade for tenant default has started
|
||||
[2m2026-05-29 19:17:25.842[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-2 - Starting...
|
||||
[2m2026-05-29 19:17:25.946[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mcom.zaxxer.hikari.pool.HikariPool [0;39m [2m:[0;39m HikariPool-2 - Added connection org.postgresql.jdbc.PgConnection@625805c1
|
||||
[2m2026-05-29 19:17:25.946[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-2 - Start completed.
|
||||
[2m2026-05-29 19:17:31.700[0;39m [31m-ERROR[0;39m [35m7760[0;39m [31m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mliquibase.changelog [0;39m [2m:[0;39m ChangeSet db/changelog/tenant/parts/0309_phase3_jobs_and_recall_permission.xml::0309_imps_deemed_reconcile_job::fineract encountered an exception.
|
||||
|
||||
liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:520)
|
||||
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:85)
|
||||
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:188)
|
||||
at liquibase.executor.AbstractExecutor.execute(AbstractExecutor.java:148)
|
||||
at liquibase.database.AbstractJdbcDatabase.executeStatements(AbstractJdbcDatabase.java:1198)
|
||||
at liquibase.changelog.ChangeSet.execute(ChangeSet.java:816)
|
||||
at liquibase.changelog.visitor.UpdateVisitor.executeAcceptedChange(UpdateVisitor.java:127)
|
||||
at liquibase.changelog.visitor.UpdateVisitor.visit(UpdateVisitor.java:71)
|
||||
at liquibase.changelog.ChangeLogIterator.lambda$run$0(ChangeLogIterator.java:133)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.changelog.ChangeLogIterator.lambda$run$1(ChangeLogIterator.java:122)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.Scope.child(Scope.java:282)
|
||||
at liquibase.Scope.child(Scope.java:286)
|
||||
at liquibase.changelog.ChangeLogIterator.run(ChangeLogIterator.java:91)
|
||||
at liquibase.command.core.AbstractUpdateCommandStep.lambda$run$0(AbstractUpdateCommandStep.java:114)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.command.core.AbstractUpdateCommandStep.run(AbstractUpdateCommandStep.java:112)
|
||||
at liquibase.command.core.UpdateCommandStep.run(UpdateCommandStep.java:100)
|
||||
at liquibase.command.CommandScope.lambda$execute$6(CommandScope.java:263)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:201)
|
||||
at liquibase.command.CommandScope.execute(CommandScope.java:251)
|
||||
at liquibase.Liquibase.lambda$update$0(Liquibase.java:216)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.Liquibase.runInScope(Liquibase.java:1366)
|
||||
at liquibase.Liquibase.update(Liquibase.java:205)
|
||||
at liquibase.Liquibase.update(Liquibase.java:188)
|
||||
at liquibase.integration.spring.SpringLiquibase.performUpdate(SpringLiquibase.java:327)
|
||||
at liquibase.integration.spring.SpringLiquibase.lambda$afterPropertiesSet$0(SpringLiquibase.java:279)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.integration.spring.SpringLiquibase.afterPropertiesSet(SpringLiquibase.java:272)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenant(TenantDatabaseUpgradeService.java:205)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.lambda$upgradeIndividualTenants$1(TenantDatabaseUpgradeService.java:146)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null).
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2736)
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2421)
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372)
|
||||
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:525)
|
||||
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:435)
|
||||
at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:357)
|
||||
at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:342)
|
||||
at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:318)
|
||||
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:313)
|
||||
at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:95)
|
||||
at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
|
||||
at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:512)
|
||||
... 52 common frames omitted
|
||||
|
||||
[2m2026-05-29 19:17:31.748[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-2 - Shutdown initiated...
|
||||
[2m2026-05-29 19:17:31.750[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ default] [2m---[0;39m [2m[lTaskExecutor-1][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-2 - Shutdown completed.
|
||||
[2m2026-05-29 19:17:31.752[0;39m [31m-ERROR[0;39m [35m7760[0;39m [31m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36ma.f.i.c.s.m.TenantDatabaseUpgradeService[0;39m [2m:[0;39m Exception:
|
||||
|
||||
java.util.concurrent.ExecutionException: java.lang.RuntimeException: Exception while upgrading tenant default
|
||||
at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
|
||||
at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenants(TenantDatabaseUpgradeService.java:155)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.afterPropertiesSet(TenantDatabaseUpgradeService.java:93)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1873)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1822)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:691)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:513)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1725)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1474)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:606)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1698)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansWithAnnotation(DefaultListableBeanFactory.java:796)
|
||||
at org.springframework.context.support.AbstractApplicationContext.getBeansWithAnnotation(AbstractApplicationContext.java:1440)
|
||||
at org.apache.fineract.infrastructure.core.jersey.JerseyConfig.setup(JerseyConfig.java:58)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:429)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1818)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:413)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:230)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:221)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:106)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:95)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:271)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:245)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:52)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4452)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:770)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:410)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:868)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:436)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:128)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:107)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:517)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:219)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:193)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:167)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)
|
||||
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149)
|
||||
at org.apache.fineract.ServerApplication.main(ServerApplication.java:57)
|
||||
Caused by: java.lang.RuntimeException: Exception while upgrading tenant default
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenant(TenantDatabaseUpgradeService.java:208)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.lambda$upgradeIndividualTenants$1(TenantDatabaseUpgradeService.java:146)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
Caused by: liquibase.exception.LiquibaseException: liquibase.exception.CommandExecutionException: liquibase.exception.LiquibaseException: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/tenant/parts/0309_phase3_jobs_and_recall_permission.xml::0309_imps_deemed_reconcile_job::fineract:
|
||||
Reason: liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.integration.spring.SpringLiquibase.afterPropertiesSet(SpringLiquibase.java:289)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenant(TenantDatabaseUpgradeService.java:205)
|
||||
... 5 common frames omitted
|
||||
Caused by: liquibase.exception.CommandExecutionException: liquibase.exception.LiquibaseException: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/tenant/parts/0309_phase3_jobs_and_recall_permission.xml::0309_imps_deemed_reconcile_job::fineract:
|
||||
Reason: liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.command.CommandScope.lambda$execute$6(CommandScope.java:310)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:201)
|
||||
at liquibase.command.CommandScope.execute(CommandScope.java:251)
|
||||
at liquibase.Liquibase.lambda$update$0(Liquibase.java:216)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.Liquibase.runInScope(Liquibase.java:1366)
|
||||
at liquibase.Liquibase.update(Liquibase.java:205)
|
||||
at liquibase.Liquibase.update(Liquibase.java:188)
|
||||
at liquibase.integration.spring.SpringLiquibase.performUpdate(SpringLiquibase.java:327)
|
||||
at liquibase.integration.spring.SpringLiquibase.lambda$afterPropertiesSet$0(SpringLiquibase.java:279)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.integration.spring.SpringLiquibase.afterPropertiesSet(SpringLiquibase.java:272)
|
||||
... 6 common frames omitted
|
||||
Caused by: liquibase.exception.LiquibaseException: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/tenant/parts/0309_phase3_jobs_and_recall_permission.xml::0309_imps_deemed_reconcile_job::fineract:
|
||||
Reason: liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.changelog.ChangeLogIterator.run(ChangeLogIterator.java:155)
|
||||
at liquibase.command.core.AbstractUpdateCommandStep.lambda$run$0(AbstractUpdateCommandStep.java:114)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.command.core.AbstractUpdateCommandStep.run(AbstractUpdateCommandStep.java:112)
|
||||
at liquibase.command.core.UpdateCommandStep.run(UpdateCommandStep.java:100)
|
||||
at liquibase.command.CommandScope.lambda$execute$6(CommandScope.java:263)
|
||||
... 24 common frames omitted
|
||||
Caused by: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/tenant/parts/0309_phase3_jobs_and_recall_permission.xml::0309_imps_deemed_reconcile_job::fineract:
|
||||
Reason: liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.changelog.ChangeSet.execute(ChangeSet.java:873)
|
||||
at liquibase.changelog.visitor.UpdateVisitor.executeAcceptedChange(UpdateVisitor.java:127)
|
||||
at liquibase.changelog.visitor.UpdateVisitor.visit(UpdateVisitor.java:71)
|
||||
at liquibase.changelog.ChangeLogIterator.lambda$run$0(ChangeLogIterator.java:133)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.changelog.ChangeLogIterator.lambda$run$1(ChangeLogIterator.java:122)
|
||||
at liquibase.Scope.lambda$child$0(Scope.java:216)
|
||||
at liquibase.Scope.child(Scope.java:225)
|
||||
at liquibase.Scope.child(Scope.java:215)
|
||||
at liquibase.Scope.child(Scope.java:194)
|
||||
at liquibase.Scope.child(Scope.java:282)
|
||||
at liquibase.Scope.child(Scope.java:286)
|
||||
at liquibase.changelog.ChangeLogIterator.run(ChangeLogIterator.java:91)
|
||||
... 32 common frames omitted
|
||||
Caused by: liquibase.exception.DatabaseException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null). [Failed SQL: (0) INSERT INTO public.job (name, display_name, cron_expression, create_time, task_priority, group_name, previous_run_start_time, job_key, initializing_errorlog, is_active, currently_running, updates_allowed, scheduler_group, is_misfired, node_id, is_mismatched_job) VALUES ('IMPS DEEMED Reconciliation', 'IMPS DEEMED Reconciliation', '0/30 * * * * ?', NOW(), 5, NULL, NULL, 'IMPS DEEMED ReconciliationDetail1 _ DEFAULT', NULL, FALSE, FALSE, TRUE, 0, FALSE, 1, TRUE)]
|
||||
at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:520)
|
||||
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:85)
|
||||
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:188)
|
||||
at liquibase.executor.AbstractExecutor.execute(AbstractExecutor.java:148)
|
||||
at liquibase.database.AbstractJdbcDatabase.executeStatements(AbstractJdbcDatabase.java:1198)
|
||||
at liquibase.changelog.ChangeSet.execute(ChangeSet.java:816)
|
||||
... 47 common frames omitted
|
||||
Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "short_name" of relation "job" violates not-null constraint
|
||||
Detail: Failing row contains (45, IMPS DEEMED Reconciliation, IMPS DEEMED Reconciliation, 0/30 * * * * ?, 2026-05-29 19:17:31.688453, 5, null, null, null, IMPS DEEMED ReconciliationDetail1 _ DEFAULT, null, f, f, t, 0, f, 1, t, null).
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2736)
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2421)
|
||||
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372)
|
||||
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:525)
|
||||
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:435)
|
||||
at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:357)
|
||||
at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:342)
|
||||
at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:318)
|
||||
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:313)
|
||||
at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:95)
|
||||
at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
|
||||
at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:512)
|
||||
... 52 common frames omitted
|
||||
|
||||
[2m2026-05-29 19:17:31.761[0;39m [31m-ERROR[0;39m [35m7760[0;39m [31m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.b.web.embedded.tomcat.TomcatStarter [0;39m [2m:[0;39m Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration': Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'jerseyConfig': Invocation of init method failed
|
||||
[2m2026-05-29 19:17:31.812[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.apache.catalina.core.StandardService [0;39m [2m:[0;39m Stopping service [Tomcat]
|
||||
[2m2026-05-29 19:17:31.817[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.a.c.loader.WebappClassLoaderBase [0;39m [2m:[0;39m The web application [fineract-provider] appears to have started a thread named [HikariPool-1:housekeeper] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
|
||||
java.base/jdk.internal.misc.Unsafe.park(Native Method)
|
||||
java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)
|
||||
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1797)
|
||||
java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)
|
||||
java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||
java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
[2m2026-05-29 19:17:31.820[0;39m [33m- WARN[0;39m [35m7760[0;39m [33m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mConfigServletWebServerApplicationContext[0;39m [2m:[0;39m Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
|
||||
[2m2026-05-29 19:17:31.820[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-1 - Shutdown initiated...
|
||||
[2m2026-05-29 19:17:31.822[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mcom.zaxxer.hikari.HikariDataSource [0;39m [2m:[0;39m HikariPool-1 - Shutdown completed.
|
||||
[2m2026-05-29 19:17:31.847[0;39m [32m- INFO[0;39m [35m7760[0;39m [32m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36m.s.b.a.l.ConditionEvaluationReportLogger[0;39m [2m:[0;39m
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
[2m2026-05-29 19:17:31.875[0;39m [31m-ERROR[0;39m [35m7760[0;39m [31m[0;39m [ no-tenant] [2m---[0;39m [2m[ main][0;39m [36mo.s.boot.SpringApplication [0;39m [2m:[0;39m Application run failed
|
||||
|
||||
org.springframework.context.ApplicationContextException: Unable to start web server
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:170)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)
|
||||
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149)
|
||||
at org.apache.fineract.ServerApplication.main(ServerApplication.java:57)
|
||||
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:147)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:107)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:517)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:219)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:193)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:167)
|
||||
... 7 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration': Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'jerseyConfig': Invocation of init method failed
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:413)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:230)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:221)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:106)
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:95)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:271)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:245)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:52)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4452)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:770)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:410)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:868)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:436)
|
||||
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:128)
|
||||
... 12 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jerseyConfig': Invocation of init method failed
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:222)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:429)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1818)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
... 61 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'journalEntriesApiResource' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\accounting\journalentry\api\JournalEntriesApiResource.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'springSecurityPlatformSecurityContext' defined in URL [jar:file:/C:/Users/adity/Music/CBS/fineract/fineract-security/build/libs/fineract-security-1.15.0-SNAPSHOT.jar!/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansWithAnnotation(DefaultListableBeanFactory.java:796)
|
||||
at org.springframework.context.support.AbstractApplicationContext.getBeansWithAnnotation(AbstractApplicationContext.java:1440)
|
||||
at org.apache.fineract.infrastructure.core.jersey.JerseyConfig.setup(JerseyConfig.java:58)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401)
|
||||
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219)
|
||||
... 74 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springSecurityPlatformSecurityContext' defined in URL [jar:file:/C:/Users/adity/Music/CBS/fineract/fineract-security/build/libs/fineract-security-1.15.0-SNAPSHOT.jar!/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
... 91 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
... 105 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:377)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1725)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1474)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:606)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1698)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
|
||||
... 119 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaSharedEM_entityManagerFactory': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:377)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:691)
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:513)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
|
||||
... 132 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tenantDatabaseUpgradeService' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\core\service\migration\TenantDatabaseUpgradeService.class]: Tenant upgrades had exceptions
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1826)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
|
||||
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
|
||||
... 144 common frames omitted
|
||||
Caused by: java.lang.RuntimeException: Tenant upgrades had exceptions
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenants(TenantDatabaseUpgradeService.java:167)
|
||||
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.afterPropertiesSet(TenantDatabaseUpgradeService.java:93)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1873)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1822)
|
||||
... 153 common frames omitted
|
||||
|
||||
|
||||
> Task :fineract-provider:bootRun FAILED
|
||||
|
||||
[Incubating] Problems report is available at: file:///C:/Users/adity/Music/CBS/fineract/build/reports/problems/problems-report.html
|
||||
|
||||
FAILURE: Build failed with an exception.
|
||||
|
||||
* What went wrong:
|
||||
Execution failed for task ':fineract-provider:bootRun'.
|
||||
> Process 'command 'C:\Program Files\Eclipse Adoptium\jdk-21.0.9.10-hotspot\bin\java.exe'' finished with non-zero exit value 1
|
||||
|
||||
* Try:
|
||||
> Run with --stacktrace option to get the stack trace.
|
||||
> Run with --info or --debug option to get more log output.
|
||||
> Get more help at https://help.gradle.org.
|
||||
|
||||
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 FAILED in 1m 30s
|
||||
83 actionable tasks: 5 executed, 78 up-to-date
|
||||
Resource usage collection shutdown not awaited
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1885
-1751
File diff suppressed because it is too large
Load Diff
+5
@@ -146,6 +146,11 @@ public class SecurityConfig {
|
||||
auth.requestMatchers(API_MATCHER.matcher(HttpMethod.OPTIONS, "/api/**")).permitAll()
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/echo")).permitAll()
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/authentication")).permitAll()
|
||||
// IMPS receiver — Fineract acts as the bank-side endpoint under
|
||||
// IMPS_BANK_API_SPEC_CBS_1.0 §6.2.x. LincPay (the switch) calls these without
|
||||
// a Fineract Bearer token; security is enforced at the JWT envelope
|
||||
// (signature + encryption) by ImpsJwtUtil / ImpsCryptoUtil inside the handler.
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/imps/**")).permitAll()
|
||||
// Login OTP — same posture as PAN/GSTIN public verification: provider-credentialed,
|
||||
// no Fineract auth required.
|
||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/login/otp/generate")).permitAll()
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.function.Function;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsCryptoUtil;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsEnvelope;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.service.ImpsCbsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Receiver-side IMPS CBS endpoints — Fineract acting as the bank under
|
||||
* IMPS_BANK_API_SPEC_CBS_1.0 §6. Each endpoint:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Reads the {@link ImpsEnvelope} ({@code data}, {@code signature}).</li>
|
||||
* <li>Verifies the inbound JWT via {@link ImpsJwtUtil}.</li>
|
||||
* <li>Decrypts {@code data} via {@link ImpsCryptoUtil}.</li>
|
||||
* <li>Hands the typed {@link ImpsRequestData} to {@link ImpsCbsService}.</li>
|
||||
* <li>Encrypts the response, signs an outbound JWT, returns the envelope.</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Path("/v1/imps")
|
||||
@Component
|
||||
@Tag(name = "IMPS CBS (Bank Side)", description = "Receiver-side endpoints for IMPS_BANK_API_SPEC_CBS_1.0 §6.2.")
|
||||
@RequiredArgsConstructor
|
||||
public class ImpsCbsApiResource {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsCbsApiResource.class);
|
||||
|
||||
private final ImpsCryptoUtil cryptoUtil;
|
||||
private final ImpsJwtUtil jwtUtil;
|
||||
private final ImpsCbsService cbsService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@POST
|
||||
@Path("/ReqPay/v1/urn:txnid:{txnid}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "IMPS ReqPay (§6.2.1)")
|
||||
public Response reqPay(@PathParam("txnid") String txnid, ImpsEnvelope body) {
|
||||
return handle("ReqPay", txnid, body, cbsService::reqPay);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/ReqValAdd/v1/urn:txnid:{txnid}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "IMPS ReqValAdd (§6.2.3)")
|
||||
public Response reqValAdd(@PathParam("txnid") String txnid, ImpsEnvelope body) {
|
||||
return handle("ReqValAdd", txnid, body, cbsService::reqValAdd);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/ReqChkTxn/v1/urn:txnid:{txnid}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "IMPS ReqChkTxn (§6.2.2)")
|
||||
public Response reqChkTxn(@PathParam("txnid") String txnid, ImpsEnvelope body) {
|
||||
return handle("ReqChkTxn", txnid, body, cbsService::reqChkTxn);
|
||||
}
|
||||
|
||||
private Response handle(String api, String txnid, ImpsEnvelope body, Function<ImpsRequestData, ImpsResponseData> handler) {
|
||||
log.info("[ImpsCbs] Inbound {} txnid={}", api, txnid);
|
||||
try {
|
||||
if (!jwtUtil.verifyJwtSignature(body.getSignature())) {
|
||||
log.warn("[ImpsCbs] {} txnid={} — JWT verification FAILED", api, txnid);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(new ImpsEnvelope("invalid-signature", "")).build();
|
||||
}
|
||||
|
||||
String decrypted = cryptoUtil.decryptJson(body.getData());
|
||||
if (decrypted == null) {
|
||||
log.warn("[ImpsCbs] {} txnid={} — decrypt FAILED", api, txnid);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(new ImpsEnvelope("decrypt-failed", "")).build();
|
||||
}
|
||||
|
||||
ImpsRequestData request = objectMapper.readValue(decrypted, ImpsRequestData.class);
|
||||
ImpsResponseData response = handler.apply(request);
|
||||
String respJson = objectMapper.writeValueAsString(response);
|
||||
log.info("[ImpsCbs] {} txnid={} responding status={} responseCode={}", api, txnid, response.getStatus(),
|
||||
response.getResponseCode());
|
||||
|
||||
String encResp = cryptoUtil.encryptJson(respJson);
|
||||
String jwt = jwtUtil.signPayloadAsJwt(respJson);
|
||||
return Response.ok(new ImpsEnvelope(encResp, jwt)).build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[ImpsCbs] {} txnid={} unhandled error: {}", api, txnid, e.toString(), e);
|
||||
return Response.serverError().entity(new ImpsEnvelope("error: " + e.getMessage(), "")).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsCryptoUtil;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsEnvelope;
|
||||
import org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Outbound IMPS calls — Fineract acting as the bank initiating ReqPay / ReqValAdd /
|
||||
* ReqChkTxn against the LincPay switch.
|
||||
*
|
||||
* <p>Each call: build {@link ImpsRequestData} → encrypt with LincPay public key →
|
||||
* sign JWT with CBS private key → POST envelope → verify response JWT → decrypt
|
||||
* response body → parse to {@link ImpsResponseData}.
|
||||
*
|
||||
* <p>The crypto utilities are symmetric across inbound and outbound: encryptJson
|
||||
* uses the peer's public key (LincPay), decryptJson uses our private key (CBS).
|
||||
* No additional key configuration is needed beyond what's already set for the
|
||||
* inbound receiver.
|
||||
*/
|
||||
@Service
|
||||
public class ImpsSwitchClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsSwitchClient.class);
|
||||
private static final MediaType JSON = MediaType.get("application/json");
|
||||
|
||||
@Value("${fineract.imps.switch.base-url:}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${fineract.imps.switch.timeout-ms:10000}")
|
||||
private int timeoutMs;
|
||||
|
||||
private final ImpsCryptoUtil cryptoUtil;
|
||||
private final ImpsJwtUtil jwtUtil;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private OkHttpClient httpClient;
|
||||
|
||||
public ImpsSwitchClient(ImpsCryptoUtil cryptoUtil, ImpsJwtUtil jwtUtil) {
|
||||
this.cryptoUtil = cryptoUtil;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
private OkHttpClient client() {
|
||||
if (httpClient == null) {
|
||||
httpClient = new OkHttpClient.Builder().connectTimeout(Duration.ofMillis(timeoutMs))
|
||||
.readTimeout(timeoutMs, TimeUnit.MILLISECONDS).writeTimeout(timeoutMs, TimeUnit.MILLISECONDS).build();
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public ImpsResponseData reqPay(String txnId, ImpsRequestData payload) {
|
||||
return invoke("IMPS", "ReqPay", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData reqValAdd(String txnId, ImpsRequestData payload) {
|
||||
return invoke("IMPS", "ReqValAdd", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData reqChkTxn(String txnId, ImpsRequestData payload) {
|
||||
return invoke("IMPS", "ReqChkTxn", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData neftPay(String txnId, ImpsRequestData payload) {
|
||||
return invoke("NEFT", "ReqPay", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData neftChkTxn(String txnId, ImpsRequestData payload) {
|
||||
return invoke("NEFT", "ReqChkTxn", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData rtgsPay(String txnId, ImpsRequestData payload) {
|
||||
return invoke("RTGS", "ReqPay", txnId, payload);
|
||||
}
|
||||
|
||||
public ImpsResponseData rtgsChkTxn(String txnId, ImpsRequestData payload) {
|
||||
return invoke("RTGS", "ReqChkTxn", txnId, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort recall on the switch. Real-world semantics vary by channel:
|
||||
* IMPS uses a dispute / chargeback flow rather than a synchronous recall;
|
||||
* NEFT supports {@code ReqDirectRet} for return-on-no-credit; RTGS recall
|
||||
* is rare and usually manual. Callers should treat exceptions as soft
|
||||
* failures and proceed with local reversal.
|
||||
*/
|
||||
public ImpsResponseData recall(String channel, String txnId, ImpsRequestData payload) {
|
||||
return invoke(channel, "ReqRecall", txnId, payload);
|
||||
}
|
||||
|
||||
private ImpsResponseData invoke(String channel, String api, String txnId, ImpsRequestData payload) {
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
throw new IllegalStateException("fineract.imps.switch.base-url is not set");
|
||||
}
|
||||
try {
|
||||
String plainBody = objectMapper.writeValueAsString(payload);
|
||||
String encrypted = cryptoUtil.encryptJson(plainBody);
|
||||
String signature = jwtUtil.signPayloadAsJwt(plainBody);
|
||||
ImpsEnvelope env = new ImpsEnvelope(encrypted, signature);
|
||||
String envJson = objectMapper.writeValueAsString(env);
|
||||
|
||||
String url = baseUrl.replaceAll("/+$", "") + "/" + channel + "/" + api + "/v1/urn:txnid:" + txnId;
|
||||
log.info("[ImpsSwitchClient] {} {} txnId={} → POST {}", channel, api, txnId, url);
|
||||
|
||||
Request req = new Request.Builder().url(url).post(RequestBody.create(envJson, JSON)).build();
|
||||
try (Response resp = client().newCall(req).execute()) {
|
||||
String respBody = bodyAsString(resp.body());
|
||||
if (!resp.isSuccessful()) {
|
||||
log.warn("[ImpsSwitchClient] {} {} txnId={} HTTP {} body={}", channel, api, txnId, resp.code(), respBody);
|
||||
throw new IOException("Switch returned HTTP " + resp.code());
|
||||
}
|
||||
|
||||
ImpsEnvelope respEnv = objectMapper.readValue(respBody, ImpsEnvelope.class);
|
||||
if (!jwtUtil.verifyJwtSignature(respEnv.getSignature())) {
|
||||
throw new IllegalStateException("Response JWT signature verification failed");
|
||||
}
|
||||
String decrypted = cryptoUtil.decryptJson(respEnv.getData());
|
||||
if (decrypted == null) {
|
||||
throw new IllegalStateException("Response payload decrypt failed");
|
||||
}
|
||||
log.info("[ImpsSwitchClient] {} {} txnId={} ← decrypted response", channel, api, txnId);
|
||||
return objectMapper.readValue(decrypted, ImpsResponseData.class);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(channel + " " + api + " call failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String bodyAsString(ResponseBody body) throws IOException {
|
||||
return body == null ? "" : body.string();
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.crypto;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Encryption / decryption matching IMPS_BANK_API_SPEC_CBS_1.0 §4.3.
|
||||
*
|
||||
* <p>Wire format: {@code <base64(wrappedAESKey)>:<base64(iv)>:<base64(cipherText)>}.
|
||||
* AES-256-GCM with a 12-byte IV and a 128-bit tag; the AES key is wrapped via
|
||||
* RSA-OAEP-SHA256.
|
||||
*
|
||||
* <h3>Key directions</h3>
|
||||
* <ul>
|
||||
* <li><b>Inbound</b>: {@link #decryptJson(String)} — unwrap the AES key with
|
||||
* the CBS private key loaded from {@code fineract.imps.cbs.private-key-path}.</li>
|
||||
* <li><b>Outbound</b>: {@link #encryptJson(String)} — wrap the AES key with
|
||||
* the LincPay public key loaded from
|
||||
* {@code fineract.imps.lincpay.public-key-path}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Keys are loaded once at startup in {@link #init()}.
|
||||
*/
|
||||
@Component
|
||||
public class ImpsCryptoUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsCryptoUtil.class);
|
||||
|
||||
private static final String AES = "AES";
|
||||
private static final String AES_GCM = "AES/GCM/NoPadding";
|
||||
private static final String RSA = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final int AES_KEY_SIZE = 256;
|
||||
private static final int IV_SIZE = 12;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
@Value("${fineract.imps.cbs.private-key-path:}")
|
||||
private String cbsPrivateKeyPath;
|
||||
|
||||
@Value("${fineract.imps.cbs.private-key-password:}")
|
||||
private String cbsPrivateKeyPassword;
|
||||
|
||||
@Value("${fineract.imps.lincpay.public-key-path:}")
|
||||
private String lincpayPublicKeyPath;
|
||||
|
||||
private PrivateKey cbsPrivateKey;
|
||||
private PublicKey lincpayPublicKey;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
if (cbsPrivateKeyPath == null || cbsPrivateKeyPath.isBlank()) {
|
||||
log.info("[ImpsCryptoUtil] fineract.imps.cbs.private-key-path not set — CBS private key NOT loaded");
|
||||
} else {
|
||||
try {
|
||||
cbsPrivateKey = loadFirstPrivateKey(cbsPrivateKeyPath, cbsPrivateKeyPassword);
|
||||
log.info("[ImpsCryptoUtil] CBS private key loaded from {}", cbsPrivateKeyPath);
|
||||
} catch (Exception e) {
|
||||
log.error("[ImpsCryptoUtil] Failed to load CBS private key from {}: {}", cbsPrivateKeyPath, e.toString());
|
||||
}
|
||||
}
|
||||
if (lincpayPublicKeyPath == null || lincpayPublicKeyPath.isBlank()) {
|
||||
log.info("[ImpsCryptoUtil] fineract.imps.lincpay.public-key-path not set — LincPay public key NOT loaded");
|
||||
} else {
|
||||
try {
|
||||
lincpayPublicKey = loadPublicKey(lincpayPublicKeyPath);
|
||||
log.info("[ImpsCryptoUtil] LincPay public key loaded from {}", lincpayPublicKeyPath);
|
||||
} catch (Exception e) {
|
||||
log.error("[ImpsCryptoUtil] Failed to load LincPay public key from {}: {}", lincpayPublicKeyPath, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrypt inbound payload using the CBS private key. */
|
||||
public String decryptJson(String encryptedPayload) {
|
||||
if (cbsPrivateKey == null) {
|
||||
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
|
||||
}
|
||||
try {
|
||||
String[] parts = encryptedPayload.split(":");
|
||||
if (parts.length != 3) {
|
||||
log.error("Invalid encrypted payload format — expected 3 colon-separated parts");
|
||||
return null;
|
||||
}
|
||||
byte[] iv = Base64.getDecoder().decode(parts[1]);
|
||||
SecretKey aesKey = unwrapAESKey(parts[0], cbsPrivateKey);
|
||||
if (aesKey == null) {
|
||||
return null;
|
||||
}
|
||||
return decryptData(parts[2], aesKey, iv);
|
||||
} catch (Exception e) {
|
||||
log.error("Error decrypting inbound payload", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Encrypt outbound payload using the LincPay public key. */
|
||||
public String encryptJson(String json) {
|
||||
if (lincpayPublicKey == null) {
|
||||
throw new IllegalStateException("LincPay public key not loaded — check fineract.imps.lincpay.public-key-path");
|
||||
}
|
||||
try {
|
||||
SecretKey aesKey = generateAESKey();
|
||||
if (aesKey == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] iv = generateIV();
|
||||
String cipherText = encryptData(json, aesKey, iv);
|
||||
String wrappedKey = wrapAESKey(aesKey, lincpayPublicKey);
|
||||
if (cipherText == null || wrappedKey == null) {
|
||||
return null;
|
||||
}
|
||||
return wrappedKey + ":" + Base64.getEncoder().encodeToString(iv) + ":" + cipherText;
|
||||
} catch (Exception e) {
|
||||
log.error("Error encrypting outbound payload", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Exposed for {@link ImpsJwtUtil} — sign outbound JWT with the same CBS private key. */
|
||||
public PrivateKey getCbsPrivateKey() {
|
||||
return cbsPrivateKey;
|
||||
}
|
||||
|
||||
/** Exposed for {@link ImpsJwtUtil} — verify inbound JWT with the LincPay public key. */
|
||||
public PublicKey getLincpayPublicKey() {
|
||||
return lincpayPublicKey;
|
||||
}
|
||||
|
||||
private static SecretKey generateAESKey() throws Exception {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
|
||||
keyGen.init(AES_KEY_SIZE);
|
||||
return keyGen.generateKey();
|
||||
}
|
||||
|
||||
private static byte[] generateIV() {
|
||||
byte[] iv = new byte[IV_SIZE];
|
||||
SECURE_RANDOM.nextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
private static String wrapAESKey(SecretKey aesKey, PublicKey publicKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.WRAP_MODE, publicKey);
|
||||
return Base64.getEncoder().encodeToString(cipher.wrap(aesKey));
|
||||
} catch (Exception e) {
|
||||
log.error("Error wrapping AES key", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey unwrapAESKey(String wrappedBase64, PrivateKey privateKey) {
|
||||
try {
|
||||
byte[] wrapped = Base64.getDecoder().decode(wrappedBase64);
|
||||
Cipher cipher = Cipher.getInstance(RSA);
|
||||
cipher.init(Cipher.UNWRAP_MODE, privateKey);
|
||||
return (SecretKey) cipher.unwrap(wrapped, AES, Cipher.SECRET_KEY);
|
||||
} catch (Exception e) {
|
||||
log.error("Error unwrapping AES key", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String encryptData(String plain, SecretKey aesKey, byte[] iv) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
|
||||
byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(enc);
|
||||
} catch (Exception e) {
|
||||
log.error("Error encrypting data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String decryptData(String encBase64, SecretKey aesKey, byte[] iv) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
|
||||
byte[] dec = cipher.doFinal(Base64.getDecoder().decode(encBase64));
|
||||
return new String(dec, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("Error decrypting data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static PrivateKey loadFirstPrivateKey(String p12Path, String password) throws Exception {
|
||||
char[] pwd = password != null ? password.toCharArray() : null;
|
||||
try (FileInputStream fis = new FileInputStream(p12Path)) {
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(fis, pwd);
|
||||
Enumeration<String> aliases = ks.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
if (ks.isKeyEntry(alias)) {
|
||||
return (PrivateKey) ks.getKey(alias, pwd);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No private-key entry in keystore at " + p12Path);
|
||||
}
|
||||
}
|
||||
|
||||
private static PublicKey loadPublicKey(String crtPath) throws Exception {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
try (InputStream is = new BufferedInputStream(new FileInputStream(crtPath))) {
|
||||
Certificate cert = cf.generateCertificate(is);
|
||||
return cert.getPublicKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.crypto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Wire envelope shared with the LincPay IMPS gateway:
|
||||
* {@code { "data": "<encrypted JSON>", "signature": "<RS256 JWT>" } }.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ImpsEnvelope {
|
||||
|
||||
private String data;
|
||||
private String signature;
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.imps.crypto;
|
||||
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Date;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* JWT helpers for IMPS_BANK_API_SPEC_CBS_1.0 §4.2.
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Verify</b> inbound LincPay-signed JWT using the LincPay public key
|
||||
* loaded by {@link ImpsCryptoUtil}.</li>
|
||||
* <li><b>Sign</b> outbound JWT (RS256, 1h TTL, payload in {@code "data"}
|
||||
* claim) using the CBS private key loaded by {@link ImpsCryptoUtil}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>{@link #verifyJwtSignature(String)} returns {@code true} when the LincPay
|
||||
* public key is not configured — useful for smoke testing the encrypt/decrypt
|
||||
* path before both certs are wired up.
|
||||
*/
|
||||
@Component
|
||||
public class ImpsJwtUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsJwtUtil.class);
|
||||
|
||||
private static final long JWT_TTL_MILLIS = 3600_000L;
|
||||
|
||||
private final ImpsCryptoUtil cryptoUtil;
|
||||
|
||||
public ImpsJwtUtil(ImpsCryptoUtil cryptoUtil) {
|
||||
this.cryptoUtil = cryptoUtil;
|
||||
}
|
||||
|
||||
/** Sign a JSON payload — payload carried in the {@code "data"} claim. */
|
||||
public String signPayloadAsJwt(String jsonPayload) {
|
||||
PrivateKey privateKey = cryptoUtil.getCbsPrivateKey();
|
||||
if (privateKey == null) {
|
||||
log.warn("[ImpsJwtUtil] CBS private key not loaded — outbound JWT signing SKIPPED");
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
JWSSigner signer = new RSASSASigner(privateKey);
|
||||
JWTClaimsSet claims = new JWTClaimsSet.Builder().claim("data", jsonPayload).issueTime(new Date())
|
||||
.expirationTime(new Date(System.currentTimeMillis() + JWT_TTL_MILLIS)).build();
|
||||
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(), claims);
|
||||
signedJWT.sign(signer);
|
||||
return signedJWT.serialize();
|
||||
} catch (Exception e) {
|
||||
log.error("[ImpsJwtUtil] Error signing outbound JWT: {}", e.toString(), e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify an inbound JWT against the LincPay public key. */
|
||||
public boolean verifyJwtSignature(String jwtToken) {
|
||||
if (jwtToken == null || jwtToken.isBlank()) {
|
||||
log.warn("[ImpsJwtUtil] Inbound JWT is blank — verification skipped (signature missing)");
|
||||
return true;
|
||||
}
|
||||
PublicKey pub = cryptoUtil.getLincpayPublicKey();
|
||||
if (pub == null) {
|
||||
log.warn("[ImpsJwtUtil] LincPay public key not loaded — verification skipped (DEV MODE)");
|
||||
return true;
|
||||
}
|
||||
if (!(pub instanceof RSAPublicKey rsaPub)) {
|
||||
log.error("[ImpsJwtUtil] LincPay public key is not RSA (type={}) — verification FAILED", pub.getAlgorithm());
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
SignedJWT signedJWT = SignedJWT.parse(jwtToken);
|
||||
JWSVerifier verifier = new RSASSAVerifier(rsaPub);
|
||||
return signedJWT.verify(verifier);
|
||||
} catch (Exception e) {
|
||||
log.error("[ImpsJwtUtil] Verification error: {}", e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Account block under {@code payer.account} / {@code payee.account}. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AccountData {
|
||||
|
||||
private String accountNumber;
|
||||
private String ifsc;
|
||||
private String accountType;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Union of the decrypted inbound request fields across the three IMPS APIs
|
||||
* (ReqPay §6.2.1, ReqValAdd §6.2.3, ReqChkTxn §6.2.2). Fields not present in a
|
||||
* given API stay {@code null}.
|
||||
*
|
||||
* <p>Covers the fields the simulator extracted via {@code JsonNode} dotted paths.
|
||||
* Extend with additional spec fields (e.g. {@code remarks}, {@code merchantCode},
|
||||
* {@code signature}) as the IMPS_BANK_API_SPEC_CBS_1.0 §6 schemas are consulted.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ImpsRequestData {
|
||||
|
||||
private String txnId;
|
||||
private String orgTxnId;
|
||||
private String refId;
|
||||
private String utr;
|
||||
private String amount;
|
||||
private PartyData payer;
|
||||
private PartyData payee;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Union of CBS response fields across RespPay / RespValAdd / RespChkTxn per
|
||||
* IMPS_BANK_API_SPEC_CBS_1.0 §6.2.x. {@code @JsonInclude(NON_NULL)} so each API
|
||||
* only emits the fields its spec section requires.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ImpsResponseData {
|
||||
|
||||
private String txnId;
|
||||
private String orgTxnId;
|
||||
private String refId;
|
||||
private String timeStamp;
|
||||
private String utr;
|
||||
private String amount;
|
||||
private String status;
|
||||
private String responseCode;
|
||||
private String accountNumber;
|
||||
private String ifsc;
|
||||
private String maskName;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Payer / payee block on an IMPS request. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PartyData {
|
||||
|
||||
private String name;
|
||||
private String mobile;
|
||||
private AccountData account;
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* One row per inbound IMPS request received by Fineract acting as the CBS.
|
||||
* Records audit + idempotency state — a retried {@code txnId} should resolve
|
||||
* back to the same {@code status} / {@code responseCode} via {@code unique(txn_id, api)}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_imps_transaction")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ImpsTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
/** Which spec endpoint produced this row — REQ_PAY / REQ_VAL_ADD / REQ_CHK_TXN. */
|
||||
@Column(name = "api", nullable = false, length = 32)
|
||||
private String api;
|
||||
|
||||
@Column(name = "txn_id", nullable = false, length = 64)
|
||||
private String txnId;
|
||||
|
||||
@Column(name = "org_txn_id", length = 64)
|
||||
private String orgTxnId;
|
||||
|
||||
@Column(name = "ref_id", length = 64)
|
||||
private String refId;
|
||||
|
||||
@Column(name = "utr", length = 64)
|
||||
private String utr;
|
||||
|
||||
@Column(name = "amount", length = 32)
|
||||
private String amount;
|
||||
|
||||
@Column(name = "payee_account_number", length = 64)
|
||||
private String payeeAccountNumber;
|
||||
|
||||
@Column(name = "payee_ifsc", length = 32)
|
||||
private String payeeIfsc;
|
||||
|
||||
/** Resolved scenario — SUCCESS / DEEMED / FAILED. */
|
||||
@Column(name = "status", length = 16)
|
||||
private String status;
|
||||
|
||||
/** IMPS response code — 00 / 91 / XC / 8 / etc. */
|
||||
@Column(name = "response_code", length = 8)
|
||||
private String responseCode;
|
||||
|
||||
/** Decrypted request JSON, kept for audit / replay. */
|
||||
@Lob
|
||||
@Column(name = "request_json")
|
||||
private String requestJson;
|
||||
|
||||
/** Outbound response JSON before encryption, kept for audit. */
|
||||
@Lob
|
||||
@Column(name = "response_json")
|
||||
private String responseJson;
|
||||
|
||||
public static ImpsTransaction newRow(String api, String txnId) {
|
||||
ImpsTransaction t = new ImpsTransaction();
|
||||
t.api = api;
|
||||
t.txnId = txnId;
|
||||
return t;
|
||||
}
|
||||
}
|
||||
+27
@@ -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.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.imps.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ImpsTransactionRepository extends JpaRepository<ImpsTransaction, Long> {
|
||||
|
||||
Optional<ImpsTransaction> findFirstByApiAndTxnId(String api, String txnId);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
|
||||
/**
|
||||
* Receiver-side IMPS handler. Each method takes the decrypted request and
|
||||
* returns the unencrypted response body — the controller layer is responsible
|
||||
* for envelope encryption / signing around these calls.
|
||||
*/
|
||||
public interface ImpsCbsService {
|
||||
|
||||
ImpsResponseData reqPay(ImpsRequestData request);
|
||||
|
||||
ImpsResponseData reqValAdd(ImpsRequestData request);
|
||||
|
||||
ImpsResponseData reqChkTxn(ImpsRequestData request);
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.infrastructure.imps.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.infrastructure.imps.domain.ImpsTransaction;
|
||||
import org.apache.fineract.infrastructure.imps.domain.ImpsTransactionRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* STUB implementation — returns hard-coded SUCCESS for every request and
|
||||
* persists an audit row per call. Replace the {@link #buildResponse} body
|
||||
* with real Fineract account lookup / debit-credit logic when that scope is
|
||||
* agreed.
|
||||
*
|
||||
* <p>Idempotency: a retried {@code (api, txnId)} short-circuits to the
|
||||
* previously-recorded response instead of running the handler twice.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImpsCbsServiceImpl implements ImpsCbsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsCbsServiceImpl.class);
|
||||
private static final DateTimeFormatter ISO_OFFSET = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
|
||||
private final ImpsTransactionRepository repository;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ImpsResponseData reqPay(ImpsRequestData request) {
|
||||
return handle("REQ_PAY", request, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ImpsResponseData reqValAdd(ImpsRequestData request) {
|
||||
return handle("REQ_VAL_ADD", request, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ImpsResponseData reqChkTxn(ImpsRequestData request) {
|
||||
return handle("REQ_CHK_TXN", request, false);
|
||||
}
|
||||
|
||||
private ImpsResponseData handle(String api, ImpsRequestData request, boolean echoIfsc) {
|
||||
Optional<ImpsTransaction> prior = repository.findFirstByApiAndTxnId(api, request.getTxnId());
|
||||
if (prior.isPresent()) {
|
||||
log.info("[ImpsCbsService] {} txnId={} idempotent replay — returning prior response", api, request.getTxnId());
|
||||
return readResponse(prior.get());
|
||||
}
|
||||
|
||||
ImpsResponseData response = buildResponse(api, request, echoIfsc);
|
||||
|
||||
ImpsTransaction row = ImpsTransaction.newRow(api, request.getTxnId());
|
||||
row.setOrgTxnId(request.getOrgTxnId());
|
||||
row.setRefId(request.getRefId());
|
||||
row.setUtr(request.getUtr());
|
||||
row.setAmount(request.getAmount());
|
||||
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
|
||||
if (payeeAcc != null) {
|
||||
row.setPayeeAccountNumber(payeeAcc.getAccountNumber());
|
||||
row.setPayeeIfsc(payeeAcc.getIfsc());
|
||||
}
|
||||
row.setStatus(response.getStatus());
|
||||
row.setResponseCode(response.getResponseCode());
|
||||
row.setRequestJson(writeJson(request));
|
||||
row.setResponseJson(writeJson(response));
|
||||
repository.save(row);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/** STUB — replace with real account lookup + debit/credit. */
|
||||
private ImpsResponseData buildResponse(String api, ImpsRequestData request, boolean echoIfsc) {
|
||||
ImpsResponseData resp = new ImpsResponseData();
|
||||
resp.setTxnId(request.getTxnId());
|
||||
resp.setOrgTxnId(request.getOrgTxnId());
|
||||
resp.setRefId(request.getRefId());
|
||||
resp.setUtr(request.getUtr());
|
||||
resp.setAmount(request.getAmount());
|
||||
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET));
|
||||
resp.setStatus("SUCCESS");
|
||||
resp.setResponseCode("00");
|
||||
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
|
||||
if (payeeAcc != null) {
|
||||
resp.setAccountNumber(payeeAcc.getAccountNumber());
|
||||
if (echoIfsc) {
|
||||
resp.setIfsc(payeeAcc.getIfsc());
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
private String writeJson(Object o) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("[ImpsCbsService] failed to serialize {} for audit: {}", o.getClass().getSimpleName(), e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ImpsResponseData readResponse(ImpsTransaction row) {
|
||||
if (row.getResponseJson() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(row.getResponseJson(), ImpsResponseData.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("[ImpsCbsService] failed to deserialize stored response for txnId={}: {}", row.getTxnId(), e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferRequestData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyRequestData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.FundTransferReadPlatformService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.FundTransferWritePlatformService;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.BulkTransferRowResult;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.BulkTransferService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.ImpsDeemedReconciliationService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.NeftBatchProcessorService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Cashier-facing fund transfer endpoints. Flow:
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@code POST /v1/transfers/verify} — BANL, returns verificationToken</li>
|
||||
* <li>{@code POST /v1/transfers} — redeems token, executes (or queues for approval)</li>
|
||||
* <li>{@code GET /v1/transfers/{ref}} — status</li>
|
||||
* <li>{@code POST /v1/transfers/{ref}/approve} — manager approves a pending row</li>
|
||||
* <li>{@code POST /v1/transfers/{ref}/reject} — manager rejects a pending row</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Path("/v1/transfers")
|
||||
@Component
|
||||
@Tag(name = "Fund Transfers", description = "Cashier-initiated INTERNAL / IMPS / NEFT / RTGS transfers.")
|
||||
@RequiredArgsConstructor
|
||||
public class FundTransferApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final FundTransferWritePlatformService writeService;
|
||||
private final FundTransferReadPlatformService readService;
|
||||
private final ImpsDeemedReconciliationService reconciliationService;
|
||||
private final NeftBatchProcessorService neftBatchProcessor;
|
||||
private final BulkTransferService bulkTransferService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@POST
|
||||
@Path("/verify")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "BANL pre-step",
|
||||
description = "Validates the transfer, runs the beneficiary name lookup, and returns a verificationToken "
|
||||
+ "that must be presented at POST /v1/transfers. Token TTL is 300 seconds.")
|
||||
public VerifyResponseData verify(final String body) throws Exception {
|
||||
context.authenticatedUser().validateHasPermissionTo("CREATE_FUNDTRANSFER");
|
||||
VerifyRequestData request = objectMapper.readValue(body, VerifyRequestData.class);
|
||||
return writeService.verify(request);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Initiate transfer",
|
||||
description = "Redeems a verificationToken issued by /verify. Returns immediately with final status "
|
||||
+ "(AUTO path) or PENDING_APPROVAL (above maker-checker threshold).")
|
||||
public TransferResponseData create(final String body) throws Exception {
|
||||
context.authenticatedUser().validateHasPermissionTo("CREATE_FUNDTRANSFER");
|
||||
TransferRequestData request = objectMapper.readValue(body, TransferRequestData.class);
|
||||
return writeService.createFromToken(request.getVerificationToken(), request.getRemarks());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ref}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Get transfer status by ref")
|
||||
public TransferResponseData findByRef(@PathParam("ref") final String txnRef) {
|
||||
context.authenticatedUser().validateHasPermissionTo("READ_FUNDTRANSFER");
|
||||
return readService.findByRef(txnRef);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{ref}/approve")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Manager approves a pending transfer")
|
||||
public TransferResponseData approve(@PathParam("ref") final String txnRef) {
|
||||
context.authenticatedUser().validateHasPermissionTo("APPROVE_FUNDTRANSFER");
|
||||
return writeService.approve(txnRef);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{ref}/reject")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Manager rejects a pending transfer")
|
||||
public TransferResponseData reject(@PathParam("ref") final String txnRef, final String body) throws Exception {
|
||||
context.authenticatedUser().validateHasPermissionTo("REJECT_FUNDTRANSFER");
|
||||
String reason = null;
|
||||
if (body != null && !body.isBlank()) {
|
||||
var node = objectMapper.readTree(body);
|
||||
if (node.hasNonNull("reason")) {
|
||||
reason = node.get("reason").asText();
|
||||
}
|
||||
}
|
||||
return writeService.reject(txnRef, reason);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{ref}/recall")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Recall an OFF_US transfer",
|
||||
description = "Mark the original transfer REVERSED and reverse the suspense parking. "
|
||||
+ "Only valid on OFF_US transfers in SUCCESS / DEEMED / QUEUED state. "
|
||||
+ "Production may also need to notify the beneficiary bank/switch — that's a phase 4 enhancement.")
|
||||
public TransferResponseData recall(@PathParam("ref") final String txnRef, final String body) throws Exception {
|
||||
context.authenticatedUser().validateHasPermissionTo("RECALL_FUNDTRANSFER");
|
||||
String reason = null;
|
||||
if (body != null && !body.isBlank()) {
|
||||
var node = objectMapper.readTree(body);
|
||||
if (node.hasNonNull("reason")) {
|
||||
reason = node.get("reason").asText();
|
||||
}
|
||||
}
|
||||
return writeService.recall(txnRef, reason);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/reconcile-deemed")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Trigger DEEMED reconciliation (admin / cron)",
|
||||
description = "Polls the IMPS switch via ReqChkTxn for every DEEMED transfer in the "
|
||||
+ "[24h ago, 30s ago] window and resolves to SUCCESS/FAILED. Call from ops "
|
||||
+ "cron until phase 2 wires this into the Quartz scheduler.")
|
||||
public String reconcileDeemed() {
|
||||
context.authenticatedUser().validateHasPermissionTo("APPROVE_FUNDTRANSFER");
|
||||
int touched = reconciliationService.reconcileAll();
|
||||
return "{\"reconciled\":" + touched + "}";
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/bulk")
|
||||
@Consumes("text/csv")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Bulk upload transfers via CSV",
|
||||
description = "Accepts a CSV body with header row "
|
||||
+ "'channel,payerAccountId,payeeAccountNumber,payeeIfsc,payeeName,amount,remarks'. "
|
||||
+ "Each row is verified and created independently; one bad row does not roll back the others.")
|
||||
public List<BulkTransferRowResult> bulk(final String csvBody) {
|
||||
context.authenticatedUser().validateHasPermissionTo("CREATE_FUNDTRANSFER");
|
||||
return bulkTransferService.process(csvBody);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/process-neft-batch")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Trigger NEFT batch cycle (admin / cron)",
|
||||
description = "Picks up every QUEUED NEFT transfer that has cleared maker-checker and "
|
||||
+ "sends each to the switch's /NEFT/ReqPay endpoint. Call from ops cron on the "
|
||||
+ "NEFT half-hourly schedule until phase 2.5 wires this into Quartz.")
|
||||
public NeftBatchProcessorService.BatchResult processNeftBatch() {
|
||||
context.authenticatedUser().validateHasPermissionTo("APPROVE_FUNDTRANSFER");
|
||||
return neftBatchProcessor.runBatch();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** One row's outcome inside a bulk CSV upload. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class BulkTransferRowResult {
|
||||
|
||||
private int rowNumber;
|
||||
private String txnRef;
|
||||
private String status;
|
||||
private String approvalStatus;
|
||||
private String errorMessage;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Per-channel, per-customer effective limits — output of
|
||||
* {@code TransferLimitResolver.resolve(...)}. Null fields mean unbounded.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ResolvedLimits {
|
||||
|
||||
private BigDecimal perTxnMin;
|
||||
private BigDecimal perTxnMax;
|
||||
private BigDecimal perDayMax;
|
||||
private BigDecimal makerCheckerAbove;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* POST /v1/transfers body — debit step. The verificationToken locks all
|
||||
* transfer params; only remarks can be set here.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TransferRequestData {
|
||||
|
||||
private String verificationToken;
|
||||
private String remarks;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Response for POST /v1/transfers + GET /v1/transfers/{ref}. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class TransferResponseData {
|
||||
|
||||
private String txnRef;
|
||||
private String channel;
|
||||
private String direction;
|
||||
private String status;
|
||||
private String approvalStatus;
|
||||
private String responseCode;
|
||||
private String responseMessage;
|
||||
private String payeeAccountNumber;
|
||||
private String payeeIfsc;
|
||||
private String payeeMaskName;
|
||||
private BigDecimal amount;
|
||||
private String utr;
|
||||
private String switchTxnId;
|
||||
private LocalDateTime initiatedAt;
|
||||
private LocalDateTime settledAt;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** POST /v1/transfers/verify body — BANL step. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class VerifyRequestData {
|
||||
|
||||
private String channel;
|
||||
private Long payerAccountId;
|
||||
private String payeeAccountNumber;
|
||||
private String payeeIfsc;
|
||||
private String payeeName;
|
||||
private BigDecimal amount;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Response from /v1/transfers/verify — masked name + token to present at debit. */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class VerifyResponseData {
|
||||
|
||||
private String verificationToken;
|
||||
private long ttlSeconds;
|
||||
private String direction;
|
||||
private String payeeMaskName;
|
||||
private boolean makerCheckerRequired;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
public enum ApprovalStatus {
|
||||
AUTO,
|
||||
PENDING_APPROVAL,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/** Row in our bank's IFSC list — presence here means a payee.ifsc resolves ON-US. */
|
||||
@Entity
|
||||
@Table(name = "m_bank_ifsc")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class BankIfsc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "ifsc_code", nullable = false, length = 11, unique = true)
|
||||
private String ifscCode;
|
||||
|
||||
@Column(name = "branch_code", length = 16)
|
||||
private String branchCode;
|
||||
|
||||
@Column(name = "branch_name")
|
||||
private String branchName;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface BankIfscRepository extends JpaRepository<BankIfsc, Long> {
|
||||
|
||||
Optional<BankIfsc> findByIfscCodeIgnoreCaseAndActiveTrue(String ifscCode);
|
||||
|
||||
boolean existsByIfscCodeIgnoreCaseAndActiveTrue(String ifscCode);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Per-customer override of per_txn_max / per_day_max for a channel. Null on
|
||||
* either column inherits the global default from {@link TransferLimitConfig}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_customer_transfer_limit")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class CustomerTransferLimit extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "client_id", nullable = false)
|
||||
private Long clientId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 16)
|
||||
private TransferChannel channel;
|
||||
|
||||
@Column(name = "per_txn_max", precision = 19, scale = 4)
|
||||
private BigDecimal perTxnMax;
|
||||
|
||||
@Column(name = "per_day_max", precision = 19, scale = 4)
|
||||
private BigDecimal perDayMax;
|
||||
|
||||
@Column(name = "effective_from", nullable = false)
|
||||
private LocalDate effectiveFrom;
|
||||
|
||||
@Column(name = "effective_to")
|
||||
private LocalDate effectiveTo;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface CustomerTransferLimitRepository extends JpaRepository<CustomerTransferLimit, Long> {
|
||||
|
||||
@Query("""
|
||||
SELECT l FROM CustomerTransferLimit l
|
||||
WHERE l.clientId = :clientId
|
||||
AND l.channel = :channel
|
||||
AND l.effectiveFrom <= :asOf
|
||||
AND (l.effectiveTo IS NULL OR l.effectiveTo >= :asOf)
|
||||
ORDER BY l.effectiveFrom DESC
|
||||
""")
|
||||
Optional<CustomerTransferLimit> findEffective(@Param("clientId") Long clientId,
|
||||
@Param("channel") TransferChannel channel, @Param("asOf") LocalDate asOf);
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/** One row per outward fund transfer (INTERNAL / IMPS / NEFT / RTGS). */
|
||||
@Entity
|
||||
@Table(name = "m_fund_transfer")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class FundTransfer extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "txn_ref", nullable = false, length = 64, unique = true)
|
||||
private String txnRef;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 16)
|
||||
private TransferChannel channel;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "direction", nullable = false, length = 8)
|
||||
private TransferDirection direction;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 24)
|
||||
private FundTransferStatus status;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "approval_status", nullable = false, length = 16)
|
||||
private ApprovalStatus approvalStatus = ApprovalStatus.AUTO;
|
||||
|
||||
@Column(name = "response_code", length = 8)
|
||||
private String responseCode;
|
||||
|
||||
@Column(name = "response_message")
|
||||
private String responseMessage;
|
||||
|
||||
@Column(name = "cashier_user_id", nullable = false)
|
||||
private Long cashierUserId;
|
||||
|
||||
@Column(name = "client_id", nullable = false)
|
||||
private Long clientId;
|
||||
|
||||
@Column(name = "payer_account_id", nullable = false)
|
||||
private Long payerAccountId;
|
||||
|
||||
@Column(name = "payer_account_number", nullable = false, length = 64)
|
||||
private String payerAccountNumber;
|
||||
|
||||
@Column(name = "payee_account_number", nullable = false, length = 64)
|
||||
private String payeeAccountNumber;
|
||||
|
||||
@Column(name = "payee_ifsc", length = 11)
|
||||
private String payeeIfsc;
|
||||
|
||||
@Column(name = "payee_name")
|
||||
private String payeeName;
|
||||
|
||||
@Column(name = "payee_mask_name")
|
||||
private String payeeMaskName;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 19, scale = 4)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "remarks")
|
||||
private String remarks;
|
||||
|
||||
@Column(name = "internal_txn_id")
|
||||
private Long internalTxnId;
|
||||
|
||||
@Column(name = "switch_txn_id", length = 64)
|
||||
private String switchTxnId;
|
||||
|
||||
@Column(name = "utr", length = 64)
|
||||
private String utr;
|
||||
|
||||
@Column(name = "command_source_id")
|
||||
private Long commandSourceId;
|
||||
|
||||
@Column(name = "initiated_at", nullable = false)
|
||||
private LocalDateTime initiatedAt;
|
||||
|
||||
@Column(name = "settled_at")
|
||||
private LocalDateTime settledAt;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Append-only audit row per state transition on a {@link FundTransfer}. */
|
||||
@Entity
|
||||
@Table(name = "m_fund_transfer_event")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class FundTransferEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "fund_transfer_id", nullable = false)
|
||||
private Long fundTransferId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "from_status", length = 24)
|
||||
private FundTransferStatus fromStatus;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "to_status", nullable = false, length = 24)
|
||||
private FundTransferStatus toStatus;
|
||||
|
||||
@Column(name = "event", nullable = false, length = 48)
|
||||
private String event;
|
||||
|
||||
@Lob
|
||||
@Column(name = "payload_json")
|
||||
private String payloadJson;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
public static FundTransferEvent of(Long fundTransferId, FundTransferStatus from, FundTransferStatus to,
|
||||
String event, String payloadJson, Long createdBy) {
|
||||
FundTransferEvent e = new FundTransferEvent();
|
||||
e.fundTransferId = fundTransferId;
|
||||
e.fromStatus = from;
|
||||
e.toStatus = to;
|
||||
e.event = event;
|
||||
e.payloadJson = payloadJson;
|
||||
e.createdAt = LocalDateTime.now();
|
||||
e.createdBy = createdBy;
|
||||
return e;
|
||||
}
|
||||
}
|
||||
+27
@@ -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.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface FundTransferEventRepository extends JpaRepository<FundTransferEvent, Long> {
|
||||
|
||||
List<FundTransferEvent> findByFundTransferIdOrderByCreatedAtAsc(Long fundTransferId);
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface FundTransferRepository extends JpaRepository<FundTransfer, Long> {
|
||||
|
||||
Optional<FundTransfer> findByTxnRef(String txnRef);
|
||||
|
||||
/**
|
||||
* Sum of completed/in-flight transfer amounts for a payer on a channel
|
||||
* since the given timestamp. Used to enforce per_day_max.
|
||||
* Excludes FAILED and REJECTED rows.
|
||||
*/
|
||||
@Query("""
|
||||
SELECT COALESCE(SUM(t.amount), 0) FROM FundTransfer t
|
||||
WHERE t.payerAccountId = :payerAccountId
|
||||
AND t.channel = :channel
|
||||
AND t.initiatedAt >= :since
|
||||
AND t.status NOT IN (org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus.FAILED,
|
||||
org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus.REVERSED)
|
||||
AND t.approvalStatus <> org.apache.fineract.portfolio.fundtransfer.domain.ApprovalStatus.REJECTED
|
||||
""")
|
||||
BigDecimal sumActiveOnChannelSince(@Param("payerAccountId") Long payerAccountId,
|
||||
@Param("channel") TransferChannel channel, @Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* DEEMED transfers eligible for reconciliation — initiated between the floor (24h ago)
|
||||
* and ceiling (30s ago) so we don't poll a transfer that just landed or one that's
|
||||
* already aged out.
|
||||
*/
|
||||
@Query("""
|
||||
SELECT t FROM FundTransfer t
|
||||
WHERE t.status = org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus.DEEMED
|
||||
AND t.initiatedAt BETWEEN :floor AND :ceiling
|
||||
ORDER BY t.initiatedAt ASC
|
||||
""")
|
||||
List<FundTransfer> findDeemedNeedingReconciliation(@Param("floor") LocalDateTime floor,
|
||||
@Param("ceiling") LocalDateTime ceiling);
|
||||
|
||||
/**
|
||||
* QUEUED NEFT transfers that have cleared maker-checker and are ready for the next
|
||||
* batch cycle. Excludes rows still PENDING_APPROVAL.
|
||||
*/
|
||||
@Query("""
|
||||
SELECT t FROM FundTransfer t
|
||||
WHERE t.status = org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus.QUEUED
|
||||
AND t.channel = org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel.NEFT
|
||||
AND t.approvalStatus IN (org.apache.fineract.portfolio.fundtransfer.domain.ApprovalStatus.AUTO,
|
||||
org.apache.fineract.portfolio.fundtransfer.domain.ApprovalStatus.APPROVED)
|
||||
ORDER BY t.initiatedAt ASC
|
||||
""")
|
||||
List<FundTransfer> findQueuedNeftReadyForBatch();
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
public enum FundTransferStatus {
|
||||
INITIATED,
|
||||
VALIDATED,
|
||||
VERIFIED,
|
||||
/** NEFT-only: row created and awaiting the next batch cycle. */
|
||||
QUEUED,
|
||||
IN_PROGRESS,
|
||||
SUCCESS,
|
||||
DEEMED,
|
||||
FAILED,
|
||||
REVERSED
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
public enum TransferChannel {
|
||||
INTERNAL,
|
||||
IMPS,
|
||||
NEFT,
|
||||
RTGS;
|
||||
|
||||
public static TransferChannel from(String s) {
|
||||
if (s == null) {
|
||||
throw new IllegalArgumentException("channel is required");
|
||||
}
|
||||
return TransferChannel.valueOf(s.trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
public enum TransferDirection {
|
||||
ON_US,
|
||||
OFF_US
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/** Global per-channel transfer limits + maker-checker threshold. */
|
||||
@Entity
|
||||
@Table(name = "m_transfer_limit_config")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class TransferLimitConfig extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 16)
|
||||
private TransferChannel channel;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "direction", length = 8)
|
||||
private TransferDirection direction;
|
||||
|
||||
@Column(name = "per_txn_min", precision = 19, scale = 4)
|
||||
private BigDecimal perTxnMin;
|
||||
|
||||
@Column(name = "per_txn_max", precision = 19, scale = 4)
|
||||
private BigDecimal perTxnMax;
|
||||
|
||||
@Column(name = "per_day_max", precision = 19, scale = 4)
|
||||
private BigDecimal perDayMax;
|
||||
|
||||
@Column(name = "maker_checker_above", precision = 19, scale = 4)
|
||||
private BigDecimal makerCheckerAbove;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
}
|
||||
+27
@@ -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.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TransferLimitConfigRepository extends JpaRepository<TransferLimitConfig, Long> {
|
||||
|
||||
Optional<TransferLimitConfig> findByChannelAndActiveTrue(TransferChannel channel);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Short-lived token issued at /v1/transfers/verify and redeemed at
|
||||
* /v1/transfers. Locks the params so they can't change between confirm and
|
||||
* debit, and doubles as the idempotency key.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_verification_token")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class VerificationToken {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "token", nullable = false, length = 64, unique = true)
|
||||
private String token;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 16)
|
||||
private TransferChannel channel;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "direction", nullable = false, length = 8)
|
||||
private TransferDirection direction;
|
||||
|
||||
@Column(name = "cashier_user_id", nullable = false)
|
||||
private Long cashierUserId;
|
||||
|
||||
@Column(name = "client_id", nullable = false)
|
||||
private Long clientId;
|
||||
|
||||
@Column(name = "payer_account_id", nullable = false)
|
||||
private Long payerAccountId;
|
||||
|
||||
@Column(name = "payee_account_number", nullable = false, length = 64)
|
||||
private String payeeAccountNumber;
|
||||
|
||||
@Column(name = "payee_ifsc", length = 11)
|
||||
private String payeeIfsc;
|
||||
|
||||
@Column(name = "payee_mask_name")
|
||||
private String payeeMaskName;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 19, scale = 4)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private VerificationTokenStatus status = VerificationTokenStatus.ISSUED;
|
||||
|
||||
@Column(name = "issued_at", nullable = false)
|
||||
private LocalDateTime issuedAt;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
@Column(name = "redeemed_at")
|
||||
private LocalDateTime redeemedAt;
|
||||
|
||||
@Column(name = "redeemed_fund_transfer_id")
|
||||
private Long redeemedFundTransferId;
|
||||
|
||||
public boolean isExpired() {
|
||||
return LocalDateTime.now().isAfter(expiresAt);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface VerificationTokenRepository extends JpaRepository<VerificationToken, Long> {
|
||||
|
||||
Optional<VerificationToken> findByToken(String token);
|
||||
|
||||
/**
|
||||
* Deletes verification tokens whose lifecycle has already terminated and that are
|
||||
* older than the cutoff. Spares ISSUED tokens regardless of age — those are still
|
||||
* potentially redeemable until they tick over to EXPIRED on next load.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("""
|
||||
DELETE FROM VerificationToken t
|
||||
WHERE t.status <> org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenStatus.ISSUED
|
||||
AND t.issuedAt < :cutoff
|
||||
""")
|
||||
int deleteTerminalOlderThan(@Param("cutoff") LocalDateTime cutoff);
|
||||
|
||||
/**
|
||||
* Flips ISSUED → EXPIRED for tokens whose expiry has passed. Called on the same
|
||||
* cleanup cycle so the next prune sweep can remove them.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE VerificationToken t
|
||||
SET t.status = org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenStatus.EXPIRED
|
||||
WHERE t.status = org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenStatus.ISSUED
|
||||
AND t.expiresAt < :now
|
||||
""")
|
||||
int markExpired(@Param("now") LocalDateTime now);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.domain;
|
||||
|
||||
public enum VerificationTokenStatus {
|
||||
ISSUED,
|
||||
REDEEMED,
|
||||
EXPIRED
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.ImpsDeemedReconciliationService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class ImpsDeemedReconcileJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private ImpsDeemedReconciliationService service;
|
||||
|
||||
@Bean
|
||||
public ImpsDeemedReconcileTasklet impsDeemedReconcileTasklet() {
|
||||
return new ImpsDeemedReconcileTasklet(service);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step impsDeemedReconcileStep() {
|
||||
return new StepBuilder(JobName.IMPS_DEEMED_RECONCILE.name(), jobRepository)
|
||||
.tasklet(impsDeemedReconcileTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job impsDeemedReconcileJob() {
|
||||
return new JobBuilder(JobName.IMPS_DEEMED_RECONCILE.name(), jobRepository).start(impsDeemedReconcileStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+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.portfolio.fundtransfer.job;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.ImpsDeemedReconciliationService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ImpsDeemedReconcileTasklet implements Tasklet {
|
||||
|
||||
private final ImpsDeemedReconciliationService service;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
int touched = service.reconcileAll();
|
||||
log.info("[Job:IMPS_DEEMED_RECONCILE] reconciled {} transfer(s)", touched);
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.NeftBatchProcessorService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class NeftBatchProcessJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private NeftBatchProcessorService service;
|
||||
|
||||
@Bean
|
||||
public NeftBatchProcessTasklet neftBatchProcessTasklet() {
|
||||
return new NeftBatchProcessTasklet(service);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step neftBatchProcessStep() {
|
||||
return new StepBuilder(JobName.NEFT_BATCH_PROCESS.name(), jobRepository)
|
||||
.tasklet(neftBatchProcessTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job neftBatchProcessJob() {
|
||||
return new JobBuilder(JobName.NEFT_BATCH_PROCESS.name(), jobRepository).start(neftBatchProcessStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.job;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.NeftBatchProcessorService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class NeftBatchProcessTasklet implements Tasklet {
|
||||
|
||||
private final NeftBatchProcessorService service;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
NeftBatchProcessorService.BatchResult r = service.runBatch();
|
||||
log.info("[Job:NEFT_BATCH_PROCESS] picked={} success={} deemed={} failed={}",
|
||||
r.picked(), r.success(), r.deemed(), r.failed());
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.VerificationTokenCleanupService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class VerificationTokenCleanupJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private VerificationTokenCleanupService service;
|
||||
|
||||
@Bean
|
||||
public VerificationTokenCleanupTasklet verificationTokenCleanupTasklet() {
|
||||
return new VerificationTokenCleanupTasklet(service);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step verificationTokenCleanupStep() {
|
||||
return new StepBuilder(JobName.VERIFICATION_TOKEN_CLEANUP.name(), jobRepository)
|
||||
.tasklet(verificationTokenCleanupTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job verificationTokenCleanupJob() {
|
||||
return new JobBuilder(JobName.VERIFICATION_TOKEN_CLEANUP.name(), jobRepository).start(verificationTokenCleanupStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+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.portfolio.fundtransfer.job;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.VerificationTokenCleanupService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class VerificationTokenCleanupTasklet implements Tasklet {
|
||||
|
||||
private final VerificationTokenCleanupService service;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
VerificationTokenCleanupService.CleanupResult r = service.cleanup();
|
||||
log.info("[Job:VERIFICATION_TOKEN_CLEANUP] expired={} deleted={}", r.expired(), r.deleted());
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.BankIfscRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Decides ON_US vs OFF_US by checking the payee IFSC against m_bank_ifsc. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BankIfscLookupService {
|
||||
|
||||
private final BankIfscRepository repository;
|
||||
|
||||
public boolean isOurIfsc(String ifscCode) {
|
||||
if (ifscCode == null || ifscCode.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return repository.existsByIfscCodeIgnoreCaseAndActiveTrue(ifscCode.trim());
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.BulkTransferRowResult;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyRequestData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyResponseData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Processes a CSV upload of transfers. Each row goes through the standard
|
||||
* verify + create flow, isolated in its own transaction so one bad row doesn't
|
||||
* roll back the rest.
|
||||
*
|
||||
* <p>Expected CSV header: {@code channel,payerAccountId,payeeAccountNumber,
|
||||
* payeeIfsc,payeeName,amount,remarks}
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BulkTransferService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BulkTransferService.class);
|
||||
|
||||
private final FundTransferWritePlatformService writeService;
|
||||
|
||||
/** Hard cap on rows per upload — keeps a JAX-RS worker from being tied up for minutes. */
|
||||
@Value("${fineract.transfer.bulk.max-rows:1000}")
|
||||
private int maxRows;
|
||||
|
||||
public List<BulkTransferRowResult> process(String csvBody) {
|
||||
List<BulkTransferRowResult> results = new ArrayList<>();
|
||||
CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).setTrim(true).build();
|
||||
try (CSVParser parser = new CSVParser(new StringReader(csvBody), format)) {
|
||||
int rowNumber = 0;
|
||||
for (CSVRecord rec : parser) {
|
||||
rowNumber++;
|
||||
if (rowNumber > maxRows) {
|
||||
throw new IllegalArgumentException("CSV exceeds bulk row limit of " + maxRows
|
||||
+ " — split the upload or raise fineract.transfer.bulk.max-rows");
|
||||
}
|
||||
BulkTransferRowResult row = new BulkTransferRowResult();
|
||||
row.setRowNumber(rowNumber);
|
||||
try {
|
||||
VerifyRequestData verifyReq = parseRow(rec);
|
||||
VerifyResponseData verifyResp = writeService.verify(verifyReq);
|
||||
String remarks = column(rec, "remarks");
|
||||
TransferResponseData txnResp = writeService.createFromToken(verifyResp.getVerificationToken(), remarks);
|
||||
row.setTxnRef(txnResp.getTxnRef());
|
||||
row.setStatus(txnResp.getStatus());
|
||||
row.setApprovalStatus(txnResp.getApprovalStatus());
|
||||
} catch (RuntimeException ex) {
|
||||
row.setStatus("ERROR");
|
||||
row.setErrorMessage(ex.getMessage());
|
||||
log.warn("[BulkTransfer] row {} failed: {}", rowNumber, ex.toString());
|
||||
}
|
||||
results.add(row);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to parse CSV: " + e.getMessage(), e);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static VerifyRequestData parseRow(CSVRecord rec) {
|
||||
VerifyRequestData req = new VerifyRequestData();
|
||||
req.setChannel(column(rec, "channel"));
|
||||
String payerId = column(rec, "payerAccountId");
|
||||
if (payerId == null) {
|
||||
throw new IllegalArgumentException("payerAccountId is required");
|
||||
}
|
||||
req.setPayerAccountId(Long.valueOf(payerId));
|
||||
req.setPayeeAccountNumber(column(rec, "payeeAccountNumber"));
|
||||
req.setPayeeIfsc(column(rec, "payeeIfsc"));
|
||||
req.setPayeeName(column(rec, "payeeName"));
|
||||
String amount = column(rec, "amount");
|
||||
if (amount == null) {
|
||||
throw new IllegalArgumentException("amount is required");
|
||||
}
|
||||
req.setAmount(new BigDecimal(amount));
|
||||
return req;
|
||||
}
|
||||
|
||||
private static String column(CSVRecord rec, String name) {
|
||||
if (!rec.isMapped(name)) {
|
||||
return null;
|
||||
}
|
||||
String v = rec.get(name);
|
||||
return v == null || v.isBlank() ? null : v.trim();
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.service;
|
||||
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
|
||||
public interface FundTransferReadPlatformService {
|
||||
|
||||
TransferResponseData findByRef(String txnRef);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FundTransferReadPlatformServiceImpl implements FundTransferReadPlatformService {
|
||||
|
||||
private final FundTransferRepository repository;
|
||||
|
||||
@Override
|
||||
public TransferResponseData findByRef(String txnRef) {
|
||||
FundTransfer t = repository.findByTxnRef(txnRef)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown txn_ref: " + txnRef));
|
||||
return toData(t);
|
||||
}
|
||||
|
||||
static TransferResponseData toData(FundTransfer t) {
|
||||
return new TransferResponseData(t.getTxnRef(), t.getChannel().name(), t.getDirection().name(), t.getStatus().name(),
|
||||
t.getApprovalStatus().name(), t.getResponseCode(), t.getResponseMessage(), t.getPayeeAccountNumber(),
|
||||
t.getPayeeIfsc(), t.getPayeeMaskName(), t.getAmount(), t.getUtr(), t.getSwitchTxnId(), t.getInitiatedAt(),
|
||||
t.getSettledAt());
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.fundtransfer.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferDirection;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.ImpsOutwardStrategy;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.InternalTransferStrategy;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.NeftOutwardStrategy;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.RtgsOutwardStrategy;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.TransferStrategy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Selects the right {@link TransferStrategy} for a transfer. ON_US always goes
|
||||
* internal regardless of channel; OFF_US dispatches by channel.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FundTransferRouter {
|
||||
|
||||
private final InternalTransferStrategy internal;
|
||||
private final ImpsOutwardStrategy impsOutward;
|
||||
private final NeftOutwardStrategy neftOutward;
|
||||
private final RtgsOutwardStrategy rtgsOutward;
|
||||
|
||||
public TransferStrategy strategyFor(FundTransfer transfer) {
|
||||
if (transfer.getDirection() == TransferDirection.ON_US) {
|
||||
return internal;
|
||||
}
|
||||
TransferChannel channel = transfer.getChannel();
|
||||
return switch (channel) {
|
||||
case IMPS -> impsOutward;
|
||||
case NEFT -> neftOutward;
|
||||
case RTGS -> rtgsOutward;
|
||||
case INTERNAL -> internal;
|
||||
};
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyRequestData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyResponseData;
|
||||
|
||||
public interface FundTransferWritePlatformService {
|
||||
|
||||
VerifyResponseData verify(VerifyRequestData request);
|
||||
|
||||
TransferResponseData createFromToken(String token, String remarks);
|
||||
|
||||
TransferResponseData approve(String txnRef);
|
||||
|
||||
TransferResponseData reject(String txnRef, String reason);
|
||||
|
||||
TransferResponseData recall(String txnRef, String reason);
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.imps.client.ImpsSwitchClient;
|
||||
import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.ResolvedLimits;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.TransferResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyRequestData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.VerifyResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.ApprovalStatus;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEvent;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEventRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferDirection;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.VerificationToken;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.strategy.TransferStrategy;
|
||||
import org.apache.fineract.portfolio.sanctions.data.SanctionsScreeningResult;
|
||||
import org.apache.fineract.portfolio.sanctions.service.SanctionsScreeningService;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FundTransferWritePlatformServiceImpl implements FundTransferWritePlatformService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FundTransferWritePlatformServiceImpl.class);
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final FundTransferRepository fundTransferRepository;
|
||||
private final FundTransferEventRepository eventRepository;
|
||||
private final SavingsAccountRepositoryWrapper savingsRepository;
|
||||
private final BankIfscLookupService ifscLookup;
|
||||
private final PayeeAccountLookupService payeeLookup;
|
||||
private final TransferLimitResolver limitResolver;
|
||||
private final VerificationTokenService tokenService;
|
||||
private final FundTransferRouter router;
|
||||
private final ClientReadPlatformService clientReadPlatformService;
|
||||
private final ImpsSwitchClient impsSwitchClient;
|
||||
private final SanctionsScreeningService sanctionsScreeningService;
|
||||
private final TransferChannelWindowValidator windowValidator;
|
||||
private final SuspenseGlService suspenseGl;
|
||||
|
||||
// ─── /verify ──────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public VerifyResponseData verify(VerifyRequestData request) {
|
||||
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("amount must be > 0");
|
||||
}
|
||||
TransferChannel channel = TransferChannel.from(request.getChannel());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
windowValidator.requireOpen(channel, LocalTime.from(now), now.getDayOfWeek());
|
||||
|
||||
AppUser cashier = context.authenticatedUser();
|
||||
SavingsAccount payer = savingsRepository.findOneWithNotFoundDetection(request.getPayerAccountId());
|
||||
Long clientId = payer.clientId();
|
||||
if (clientId == null) {
|
||||
throw new IllegalArgumentException("payer account is not owned by a client");
|
||||
}
|
||||
|
||||
ResolvedLimits limits = limitResolver.resolve(clientId, channel);
|
||||
validateAmountAgainstLimits(request.getAmount(), limits);
|
||||
validateDailyLimit(payer.getId(), channel, request.getAmount(), limits);
|
||||
|
||||
TransferDirection direction = resolveDirection(channel, request.getPayeeIfsc());
|
||||
if (direction == TransferDirection.ON_US) {
|
||||
payeeLookup.findByAccountNumber(request.getPayeeAccountNumber())
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"INVALID_PAYEE — IFSC is ours but account " + request.getPayeeAccountNumber() + " not found"));
|
||||
} else {
|
||||
screenOffUsPayee(request.getPayeeName(), clientId);
|
||||
}
|
||||
|
||||
String maskName = resolvePayeeMaskName(direction, request.getPayeeAccountNumber(), request.getPayeeIfsc(),
|
||||
request.getAmount());
|
||||
|
||||
VerificationToken token = tokenService.issue(channel, direction, cashier.getId(), clientId, payer.getId(),
|
||||
request.getPayeeAccountNumber(), request.getPayeeIfsc(), maskName, request.getAmount());
|
||||
|
||||
boolean makerCheckerRequired = isMakerCheckerRequired(request.getAmount(), limits);
|
||||
|
||||
log.info("[FundTransfer] verify OK channel={} direction={} amount={} makerChecker={} token={}",
|
||||
channel, direction, request.getAmount(), makerCheckerRequired, token.getToken());
|
||||
|
||||
return new VerifyResponseData(token.getToken(), VerificationTokenService.TTL_SECONDS, direction.name(), maskName,
|
||||
makerCheckerRequired);
|
||||
}
|
||||
|
||||
// ─── POST /v1/transfers ───────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TransferResponseData createFromToken(String tokenString, String remarks) {
|
||||
if (tokenString == null || tokenString.isBlank()) {
|
||||
throw new IllegalArgumentException("verificationToken is required");
|
||||
}
|
||||
VerificationToken token = tokenService.loadActive(tokenString);
|
||||
AppUser cashier = context.authenticatedUser();
|
||||
if (!token.getCashierUserId().equals(cashier.getId())) {
|
||||
throw new IllegalStateException("Verification token was issued to a different cashier");
|
||||
}
|
||||
|
||||
ResolvedLimits limits = limitResolver.resolve(token.getClientId(), token.getChannel());
|
||||
boolean needsApproval = isMakerCheckerRequired(token.getAmount(), limits);
|
||||
|
||||
FundTransfer transfer = new FundTransfer();
|
||||
transfer.setTxnRef(generateRef());
|
||||
transfer.setChannel(token.getChannel());
|
||||
transfer.setDirection(token.getDirection());
|
||||
transfer.setStatus(FundTransferStatus.VERIFIED);
|
||||
transfer.setApprovalStatus(needsApproval ? ApprovalStatus.PENDING_APPROVAL : ApprovalStatus.AUTO);
|
||||
transfer.setCashierUserId(cashier.getId());
|
||||
transfer.setClientId(token.getClientId());
|
||||
|
||||
SavingsAccount payer = savingsRepository.findOneWithNotFoundDetection(token.getPayerAccountId());
|
||||
transfer.setPayerAccountId(payer.getId());
|
||||
transfer.setPayerAccountNumber(payer.getAccountNumber());
|
||||
transfer.setPayeeAccountNumber(token.getPayeeAccountNumber());
|
||||
transfer.setPayeeIfsc(token.getPayeeIfsc());
|
||||
transfer.setPayeeMaskName(token.getPayeeMaskName());
|
||||
transfer.setAmount(token.getAmount());
|
||||
transfer.setRemarks(remarks);
|
||||
transfer.setInitiatedAt(LocalDateTime.now());
|
||||
|
||||
if (!needsApproval) {
|
||||
executeStrategy(transfer);
|
||||
}
|
||||
FundTransfer saved = fundTransferRepository.save(transfer);
|
||||
tokenService.markRedeemed(token, saved.getId());
|
||||
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), null, saved.getStatus(),
|
||||
needsApproval ? "INITIATED_PENDING_APPROVAL" : "INITIATED_AUTO_EXECUTE", null, cashier.getId()));
|
||||
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
|
||||
// ─── POST /v1/transfers/{ref}/approve ─────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TransferResponseData approve(String txnRef) {
|
||||
FundTransfer t = loadByRef(txnRef);
|
||||
if (t.getApprovalStatus() != ApprovalStatus.PENDING_APPROVAL) {
|
||||
throw new IllegalStateException("Transfer is not pending approval");
|
||||
}
|
||||
AppUser approver = context.authenticatedUser();
|
||||
if (approver.getId().equals(t.getCashierUserId())) {
|
||||
throw new IllegalStateException("Maker cannot approve own transfer");
|
||||
}
|
||||
FundTransferStatus prev = t.getStatus();
|
||||
t.setApprovalStatus(ApprovalStatus.APPROVED);
|
||||
executeStrategy(t);
|
||||
FundTransfer saved = fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), prev, saved.getStatus(), "APPROVED", null, approver.getId()));
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
|
||||
// ─── POST /v1/transfers/{ref}/reject ──────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TransferResponseData reject(String txnRef, String reason) {
|
||||
FundTransfer t = loadByRef(txnRef);
|
||||
if (t.getApprovalStatus() != ApprovalStatus.PENDING_APPROVAL) {
|
||||
throw new IllegalStateException("Transfer is not pending approval");
|
||||
}
|
||||
AppUser approver = context.authenticatedUser();
|
||||
if (approver.getId().equals(t.getCashierUserId())) {
|
||||
throw new IllegalStateException("Maker cannot reject own transfer");
|
||||
}
|
||||
FundTransferStatus prev = t.getStatus();
|
||||
t.setApprovalStatus(ApprovalStatus.REJECTED);
|
||||
t.setStatus(FundTransferStatus.FAILED);
|
||||
t.setResponseCode("REJ");
|
||||
t.setResponseMessage(reason);
|
||||
t.setSettledAt(LocalDateTime.now());
|
||||
FundTransfer saved = fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), prev, saved.getStatus(), "REJECTED", reason, approver.getId()));
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
|
||||
// ─── POST /v1/transfers/{ref}/recall ─────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TransferResponseData recall(String txnRef, String reason) {
|
||||
FundTransfer t = loadByRef(txnRef);
|
||||
if (t.getDirection() == TransferDirection.ON_US) {
|
||||
throw new IllegalStateException("Recall only applies to OFF_US transfers");
|
||||
}
|
||||
FundTransferStatus prev = t.getStatus();
|
||||
if (prev != FundTransferStatus.SUCCESS && prev != FundTransferStatus.DEEMED && prev != FundTransferStatus.QUEUED) {
|
||||
throw new IllegalStateException("Cannot recall a transfer in state " + prev);
|
||||
}
|
||||
AppUser actor = context.authenticatedUser();
|
||||
String switchOutcome = notifySwitchOfRecall(t);
|
||||
suspenseGl.reverseFromSuspense(t);
|
||||
t.setStatus(FundTransferStatus.REVERSED);
|
||||
t.setResponseCode("RCL");
|
||||
t.setResponseMessage(reason == null ? "Recall initiated by ops" : reason);
|
||||
t.setSettledAt(LocalDateTime.now());
|
||||
FundTransfer saved = fundTransferRepository.save(t);
|
||||
String payload = "reason=" + reason + ", switch=" + switchOutcome;
|
||||
eventRepository.save(FundTransferEvent.of(saved.getId(), prev, saved.getStatus(), "RECALLED", payload, actor.getId()));
|
||||
log.info("[FundTransfer] txnRef={} recalled from state {} by user {} (switch outcome: {})",
|
||||
txnRef, prev, actor.getId(), switchOutcome);
|
||||
return FundTransferReadPlatformServiceImpl.toData(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort recall notification to the switch. Real-world recall protocols vary
|
||||
* by channel (IMPS = dispute flow; NEFT = ReqDirectRet; RTGS = manual). We send a
|
||||
* uniform {@code ReqRecall} envelope and swallow any error so the local reversal
|
||||
* still proceeds.
|
||||
*/
|
||||
private String notifySwitchOfRecall(FundTransfer t) {
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
String recallId = "RCL" + UUID.randomUUID().toString().replace("-", "").substring(0, 14).toUpperCase();
|
||||
req.setTxnId(recallId);
|
||||
req.setOrgTxnId(t.getSwitchTxnId() == null ? t.getTxnRef() : t.getSwitchTxnId());
|
||||
req.setRefId(t.getTxnRef());
|
||||
req.setUtr(t.getUtr());
|
||||
req.setAmount(t.getAmount() == null ? null : t.getAmount().toPlainString());
|
||||
try {
|
||||
ImpsResponseData resp = impsSwitchClient.recall(t.getChannel().name(), recallId, req);
|
||||
String code = resp == null ? null : resp.getResponseCode();
|
||||
return "NOTIFIED code=" + code;
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[FundTransfer] txnRef={} switch-side recall failed (proceeding with local reversal): {}",
|
||||
t.getTxnRef(), ex.toString());
|
||||
return "FAILED " + ex.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── internals ────────────────────────────────────────────────────────
|
||||
|
||||
private void executeStrategy(FundTransfer transfer) {
|
||||
transfer.setStatus(FundTransferStatus.IN_PROGRESS);
|
||||
|
||||
Long suspenseLegId = null;
|
||||
if (transfer.getDirection() == TransferDirection.OFF_US) {
|
||||
suspenseLegId = suspenseGl.parkInSuspense(transfer);
|
||||
if (suspenseLegId != null) {
|
||||
transfer.setInternalTxnId(suspenseLegId);
|
||||
}
|
||||
}
|
||||
|
||||
TransferStrategy strategy = router.strategyFor(transfer);
|
||||
try {
|
||||
strategy.execute(transfer);
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("[FundTransfer] strategy failed for txnRef={}: {}", transfer.getTxnRef(), ex.toString(), ex);
|
||||
transfer.setStatus(FundTransferStatus.FAILED);
|
||||
transfer.setResponseCode("ERR");
|
||||
transfer.setResponseMessage(ex.getMessage());
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
if (transfer.getDirection() == TransferDirection.OFF_US && suspenseLegId != null
|
||||
&& transfer.getStatus() == FundTransferStatus.FAILED) {
|
||||
suspenseGl.reverseFromSuspense(transfer);
|
||||
}
|
||||
}
|
||||
|
||||
private FundTransfer loadByRef(String txnRef) {
|
||||
return fundTransferRepository.findByTxnRef(txnRef)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown txn_ref: " + txnRef));
|
||||
}
|
||||
|
||||
private String resolvePayeeMaskName(TransferDirection direction, String payeeAccountNumber, String payeeIfsc,
|
||||
BigDecimal amount) {
|
||||
if (direction == TransferDirection.ON_US) {
|
||||
return payeeLookup.findByAccountNumber(payeeAccountNumber).map(SavingsAccount::clientId)
|
||||
.map(this::maskClientName).orElse(null);
|
||||
}
|
||||
return callSwitchForBanl(payeeAccountNumber, payeeIfsc, amount);
|
||||
}
|
||||
|
||||
private void screenOffUsPayee(String payeeName, Long initiatorClientId) {
|
||||
if (payeeName == null || payeeName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
SanctionsScreeningResult result = sanctionsScreeningService.screenName(payeeName, "FUND_TRANSFER",
|
||||
initiatorClientId, null);
|
||||
if (result.isMatchFound()) {
|
||||
log.warn("[FundTransfer] payee '{}' FLAGGED by sanctions screening — {} match(es), screeningLogId={}",
|
||||
payeeName, result.getMatchCount(), result.getScreeningLogId());
|
||||
throw new IllegalStateException("SCREENING_FAILED — payee matches sanctions list (review screeningLog id "
|
||||
+ result.getScreeningLogId() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private String callSwitchForBanl(String payeeAccountNumber, String payeeIfsc, BigDecimal amount) {
|
||||
String banlTxnId = "BANL" + UUID.randomUUID().toString().replace("-", "").substring(0, 14).toUpperCase();
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
req.setTxnId(banlTxnId);
|
||||
req.setRefId(banlTxnId);
|
||||
req.setAmount(amount == null ? null : amount.toPlainString());
|
||||
PartyData payee = new PartyData();
|
||||
AccountData payeeAcc = new AccountData();
|
||||
payeeAcc.setAccountNumber(payeeAccountNumber);
|
||||
payeeAcc.setIfsc(payeeIfsc);
|
||||
payee.setAccount(payeeAcc);
|
||||
req.setPayee(payee);
|
||||
try {
|
||||
ImpsResponseData resp = impsSwitchClient.reqValAdd(banlTxnId, req);
|
||||
return resp == null ? null : resp.getMaskName();
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[FundTransfer] OFF_US BANL call failed: {} — proceeding without maskName", ex.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String maskClientName(Long clientId) {
|
||||
try {
|
||||
var client = clientReadPlatformService.retrieveOne(clientId);
|
||||
return maskName(client == null ? null : client.getDisplayName());
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[FundTransfer] client {} display-name lookup failed: {}", clientId, ex.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks a display name to the IMPS RespValAdd convention: first letter of each
|
||||
* word kept, rest replaced with up to 5 asterisks. "John Doe" → "J*** D**".
|
||||
*/
|
||||
static String maskName(String displayName) {
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = displayName.trim().split("\\s+");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(' ');
|
||||
}
|
||||
String p = parts[i];
|
||||
if (p.length() <= 1) {
|
||||
sb.append(Character.toUpperCase(p.charAt(0)));
|
||||
} else {
|
||||
sb.append(Character.toUpperCase(p.charAt(0)));
|
||||
int stars = Math.min(p.length() - 1, 5);
|
||||
for (int j = 0; j < stars; j++) {
|
||||
sb.append('*');
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private TransferDirection resolveDirection(TransferChannel channel, String payeeIfsc) {
|
||||
if (channel == TransferChannel.INTERNAL) {
|
||||
return TransferDirection.ON_US;
|
||||
}
|
||||
if (payeeIfsc == null || payeeIfsc.isBlank()) {
|
||||
throw new IllegalArgumentException("payeeIfsc is required for channel " + channel);
|
||||
}
|
||||
return ifscLookup.isOurIfsc(payeeIfsc) ? TransferDirection.ON_US : TransferDirection.OFF_US;
|
||||
}
|
||||
|
||||
private void validateAmountAgainstLimits(BigDecimal amount, ResolvedLimits limits) {
|
||||
if (limits.getPerTxnMin() != null && amount.compareTo(limits.getPerTxnMin()) < 0) {
|
||||
throw new IllegalArgumentException("amount below per-txn minimum " + limits.getPerTxnMin());
|
||||
}
|
||||
if (limits.getPerTxnMax() != null && amount.compareTo(limits.getPerTxnMax()) > 0) {
|
||||
throw new IllegalArgumentException("amount exceeds per-txn maximum " + limits.getPerTxnMax());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDailyLimit(Long payerAccountId, TransferChannel channel, BigDecimal amount, ResolvedLimits limits) {
|
||||
if (limits.getPerDayMax() == null) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
BigDecimal already = fundTransferRepository.sumActiveOnChannelSince(payerAccountId, channel, startOfDay);
|
||||
if (already.add(amount).compareTo(limits.getPerDayMax()) > 0) {
|
||||
throw new IllegalStateException("daily limit exhausted for channel " + channel + " — already " + already
|
||||
+ " of " + limits.getPerDayMax());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMakerCheckerRequired(BigDecimal amount, ResolvedLimits limits) {
|
||||
if (limits.getMakerCheckerAbove() == null) {
|
||||
return true;
|
||||
}
|
||||
return amount.compareTo(limits.getMakerCheckerAbove()) >= 0;
|
||||
}
|
||||
|
||||
private static String generateRef() {
|
||||
return "FT" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.imps.client.ImpsSwitchClient;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEvent;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEventRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Polls the IMPS switch for DEEMED transfers (responseCode=91) to resolve them
|
||||
* to SUCCESS / FAILED. Reconciliation window: initiated between 24h ago and 30s
|
||||
* ago — too young = noisy, too old = ops responsibility.
|
||||
*
|
||||
* <p>Phase 1.5 wiring: callable directly (manual trigger) or via an admin REST
|
||||
* endpoint. Phase 2 should register this with Fineract's Quartz scheduler
|
||||
* ({@code JobName} enum + a step/tasklet) for a proper 30-second cron.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImpsDeemedReconciliationService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsDeemedReconciliationService.class);
|
||||
|
||||
/** Minimum age — give the switch ~30s before chasing it. */
|
||||
static final long MIN_AGE_SECONDS = 30;
|
||||
/** Maximum age — past this, ops owns the row. */
|
||||
static final long MAX_AGE_HOURS = 24;
|
||||
|
||||
private final FundTransferRepository fundTransferRepository;
|
||||
private final FundTransferEventRepository eventRepository;
|
||||
private final ImpsSwitchClient switchClient;
|
||||
private final SuspenseGlService suspenseGl;
|
||||
|
||||
/** Picks all DEEMED rows in the window and tries to resolve each. Returns count touched. */
|
||||
public int reconcileAll() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime floor = now.minusHours(MAX_AGE_HOURS);
|
||||
LocalDateTime ceiling = now.minusSeconds(MIN_AGE_SECONDS);
|
||||
List<FundTransfer> deemed = fundTransferRepository.findDeemedNeedingReconciliation(floor, ceiling);
|
||||
log.info("[ImpsDeemedReconciliation] {} DEEMED transfer(s) in window {} .. {}", deemed.size(), floor, ceiling);
|
||||
int touched = 0;
|
||||
for (FundTransfer t : deemed) {
|
||||
try {
|
||||
if (reconcileOne(t)) {
|
||||
touched++;
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[ImpsDeemedReconciliation] txnRef={} reconciliation error: {}", t.getTxnRef(), ex.toString());
|
||||
}
|
||||
}
|
||||
return touched;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean reconcileOne(FundTransfer transfer) {
|
||||
String chkTxnId = "CHK" + UUID.randomUUID().toString().replace("-", "").substring(0, 14).toUpperCase();
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
req.setTxnId(chkTxnId);
|
||||
req.setOrgTxnId(transfer.getSwitchTxnId() == null ? transfer.getTxnRef() : transfer.getSwitchTxnId());
|
||||
req.setRefId(transfer.getTxnRef());
|
||||
|
||||
ImpsResponseData resp = switchClient.reqChkTxn(chkTxnId, req);
|
||||
String code = resp.getResponseCode() == null ? "" : resp.getResponseCode().trim();
|
||||
FundTransferStatus newStatus = switch (code) {
|
||||
case "00" -> FundTransferStatus.SUCCESS;
|
||||
case "91" -> FundTransferStatus.DEEMED;
|
||||
default -> FundTransferStatus.FAILED;
|
||||
};
|
||||
|
||||
if (newStatus == FundTransferStatus.DEEMED) {
|
||||
log.info("[ImpsDeemedReconciliation] txnRef={} still DEEMED (code={})", transfer.getTxnRef(), code);
|
||||
return false;
|
||||
}
|
||||
FundTransferStatus prev = transfer.getStatus();
|
||||
transfer.setStatus(newStatus);
|
||||
transfer.setResponseCode(code.isEmpty() ? null : code);
|
||||
transfer.setResponseMessage(resp.getStatus());
|
||||
if (resp.getUtr() != null) {
|
||||
transfer.setUtr(resp.getUtr());
|
||||
}
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
if (newStatus == FundTransferStatus.FAILED) {
|
||||
suspenseGl.reverseFromSuspense(transfer);
|
||||
}
|
||||
fundTransferRepository.save(transfer);
|
||||
eventRepository.save(FundTransferEvent.of(transfer.getId(), prev, newStatus, "RECONCILED",
|
||||
"code=" + code + " status=" + resp.getStatus(), null));
|
||||
log.info("[ImpsDeemedReconciliation] txnRef={} resolved {} → {}", transfer.getTxnRef(), prev, newStatus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.imps.client.ImpsSwitchClient;
|
||||
import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEvent;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferEventRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Processes a NEFT batch cycle: picks all rows in QUEUED, sends each to the
|
||||
* switch's {@code /NEFT/ReqPay} endpoint, and writes the final status. Driven
|
||||
* by an admin endpoint today; phase 2.5 should wire this to Quartz with a
|
||||
* 30-minute cron matching the NEFT settlement schedule.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class NeftBatchProcessorService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NeftBatchProcessorService.class);
|
||||
|
||||
private final FundTransferRepository fundTransferRepository;
|
||||
private final FundTransferEventRepository eventRepository;
|
||||
private final ImpsSwitchClient switchClient;
|
||||
private final SuspenseGlService suspenseGl;
|
||||
|
||||
@Value("${fineract.imps.our-default-ifsc:BANK0000001}")
|
||||
private String ourDefaultIfsc;
|
||||
|
||||
public BatchResult runBatch() {
|
||||
List<FundTransfer> queued = fundTransferRepository.findQueuedNeftReadyForBatch();
|
||||
log.info("[NeftBatchProcessor] picking up {} queued NEFT transfer(s)", queued.size());
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
int deemed = 0;
|
||||
for (FundTransfer t : queued) {
|
||||
try {
|
||||
FundTransferStatus terminal = processOne(t);
|
||||
if (terminal == FundTransferStatus.SUCCESS) {
|
||||
success++;
|
||||
} else if (terminal == FundTransferStatus.DEEMED) {
|
||||
deemed++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[NeftBatchProcessor] txnRef={} unhandled error: {}", t.getTxnRef(), ex.toString());
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
log.info("[NeftBatchProcessor] cycle done — success={} deemed={} failed={}", success, deemed, failed);
|
||||
return new BatchResult(queued.size(), success, deemed, failed);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FundTransferStatus processOne(FundTransfer t) {
|
||||
ImpsRequestData payload = buildPayload(t);
|
||||
FundTransferStatus prev = t.getStatus();
|
||||
try {
|
||||
ImpsResponseData resp = switchClient.neftPay(t.getTxnRef(), payload);
|
||||
String code = resp.getResponseCode() == null ? "" : resp.getResponseCode().trim();
|
||||
FundTransferStatus mapped = switch (code) {
|
||||
case "00" -> FundTransferStatus.SUCCESS;
|
||||
case "91" -> FundTransferStatus.DEEMED;
|
||||
default -> FundTransferStatus.FAILED;
|
||||
};
|
||||
t.setStatus(mapped);
|
||||
t.setResponseCode(code.isEmpty() ? null : code);
|
||||
t.setResponseMessage(resp.getStatus());
|
||||
t.setSwitchTxnId(resp.getTxnId() == null ? t.getTxnRef() : resp.getTxnId());
|
||||
t.setUtr(resp.getUtr());
|
||||
if (mapped != FundTransferStatus.DEEMED) {
|
||||
t.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
if (mapped == FundTransferStatus.FAILED) {
|
||||
suspenseGl.reverseFromSuspense(t);
|
||||
}
|
||||
fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(t.getId(), prev, mapped, "NEFT_BATCH_PROCESSED",
|
||||
"code=" + code, null));
|
||||
log.info("[NeftBatchProcessor] txnRef={} {} → {} utr={}", t.getTxnRef(), prev, mapped, t.getUtr());
|
||||
return mapped;
|
||||
} catch (RuntimeException ex) {
|
||||
t.setStatus(FundTransferStatus.FAILED);
|
||||
t.setResponseCode("ERR");
|
||||
t.setResponseMessage(ex.getMessage());
|
||||
t.setSettledAt(LocalDateTime.now());
|
||||
suspenseGl.reverseFromSuspense(t);
|
||||
fundTransferRepository.save(t);
|
||||
eventRepository.save(FundTransferEvent.of(t.getId(), prev, FundTransferStatus.FAILED,
|
||||
"NEFT_BATCH_ERROR", ex.toString(), null));
|
||||
log.warn("[NeftBatchProcessor] txnRef={} switch call failed: {}", t.getTxnRef(), ex.toString());
|
||||
return FundTransferStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
private ImpsRequestData buildPayload(FundTransfer t) {
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
req.setTxnId(t.getTxnRef());
|
||||
req.setRefId(t.getTxnRef());
|
||||
req.setAmount(t.getAmount() == null ? null : t.getAmount().toPlainString());
|
||||
PartyData payer = new PartyData();
|
||||
AccountData payerAcc = new AccountData();
|
||||
payerAcc.setAccountNumber(t.getPayerAccountNumber());
|
||||
payerAcc.setIfsc(ourDefaultIfsc);
|
||||
payer.setAccount(payerAcc);
|
||||
req.setPayer(payer);
|
||||
PartyData payee = new PartyData();
|
||||
payee.setName(t.getPayeeName());
|
||||
AccountData payeeAcc = new AccountData();
|
||||
payeeAcc.setAccountNumber(t.getPayeeAccountNumber());
|
||||
payeeAcc.setIfsc(t.getPayeeIfsc());
|
||||
payee.setAccount(payeeAcc);
|
||||
req.setPayee(payee);
|
||||
return req;
|
||||
}
|
||||
|
||||
public record BatchResult(int picked, int success, int deemed, int failed) {}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Resolves a payee account number to a {@link SavingsAccount} for the on-us path. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PayeeAccountLookupService {
|
||||
|
||||
private final SavingsAccountRepositoryWrapper savingsRepository;
|
||||
|
||||
public Optional<SavingsAccount> findByAccountNumber(String accountNumber) {
|
||||
if (accountNumber == null || accountNumber.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(savingsRepository.findNonClosedAccountByAccountNumber(accountNumber.trim()));
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Moves money between the customer's savings account and the bank's "in transit"
|
||||
* suspense savings account around OFF_US transfers. The suspense account is
|
||||
* seeded by Liquibase 0311 with {@code external_id = 'FUNDTRANSFER_SUSPENSE'} —
|
||||
* resolved at runtime, so the auto-generated id never has to be threaded
|
||||
* through application.properties.
|
||||
*
|
||||
* <p>Override via {@code fineract.transfer.suspense-account-external-id} if a
|
||||
* deployment uses a different external id (legacy installs, multi-tenant
|
||||
* splits, etc.).
|
||||
*
|
||||
* <h3>Flow</h3>
|
||||
* <ul>
|
||||
* <li>Initiate OFF_US → debit customer, credit suspense</li>
|
||||
* <li>Switch SUCCESS → leave it; end-of-day reconciliation clears suspense</li>
|
||||
* <li>Switch FAILED → reverse: debit suspense, credit customer</li>
|
||||
* <li>DEEMED → FAILED → reverse (via {@code ImpsDeemedReconciliationService})</li>
|
||||
* <li>NEFT batch FAILED → reverse (via {@code NeftBatchProcessorService})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When the suspense account can't be resolved, both methods are no-ops with a
|
||||
* warning log — keeps dev environments without the GL set up still bootable.
|
||||
*/
|
||||
@Service
|
||||
public class SuspenseGlService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SuspenseGlService.class);
|
||||
|
||||
private final AccountTransfersWritePlatformService transfersService;
|
||||
private final SavingsAccountRepository savingsAccountRepository;
|
||||
|
||||
@Value("${fineract.transfer.suspense-account-external-id:FUNDTRANSFER_SUSPENSE}")
|
||||
private String suspenseExternalId;
|
||||
|
||||
/** Resolved on first lookup and cached for the JVM lifetime — the suspense account doesn't change. */
|
||||
private volatile Long resolvedSuspenseAccountId;
|
||||
|
||||
public SuspenseGlService(AccountTransfersWritePlatformService transfersService,
|
||||
SavingsAccountRepository savingsAccountRepository) {
|
||||
this.transfersService = transfersService;
|
||||
this.savingsAccountRepository = savingsAccountRepository;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void warnIfUnresolved() {
|
||||
Long id = tryResolve();
|
||||
if (id == null) {
|
||||
log.warn("[SuspenseGl] suspense external_id '{}' did not resolve — OFF_US debits will be no-ops "
|
||||
+ "until the suspense GL row is created (see Liquibase 0311)", suspenseExternalId);
|
||||
} else {
|
||||
log.info("[SuspenseGl] resolved suspense external_id '{}' → savings account id {}", suspenseExternalId, id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Debit customer, credit suspense. Returns the transfer transaction id, or null when GL not configured. */
|
||||
public Long parkInSuspense(FundTransfer transfer) {
|
||||
Long suspenseId = tryResolve();
|
||||
if (suspenseId == null) {
|
||||
log.warn("[SuspenseGl] suspense GL unresolved — skipping debit-to-suspense for txnRef={}", transfer.getTxnRef());
|
||||
return null;
|
||||
}
|
||||
return move(transfer.getPayerAccountId(), suspenseId, transfer, "OFF_US park: " + transfer.getTxnRef());
|
||||
}
|
||||
|
||||
/** Reverse a prior park — debit suspense, credit customer. */
|
||||
public Long reverseFromSuspense(FundTransfer transfer) {
|
||||
Long suspenseId = tryResolve();
|
||||
if (suspenseId == null) {
|
||||
log.warn("[SuspenseGl] suspense GL unresolved — skipping reversal for txnRef={}", transfer.getTxnRef());
|
||||
return null;
|
||||
}
|
||||
return move(suspenseId, transfer.getPayerAccountId(), transfer, "OFF_US reverse: " + transfer.getTxnRef());
|
||||
}
|
||||
|
||||
private Long tryResolve() {
|
||||
Long cached = resolvedSuspenseAccountId;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
if (suspenseExternalId == null || suspenseExternalId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
SavingsAccount account = savingsAccountRepository.findByExternalId(new ExternalId(suspenseExternalId));
|
||||
if (account == null) {
|
||||
return null;
|
||||
}
|
||||
resolvedSuspenseAccountId = account.getId();
|
||||
return resolvedSuspenseAccountId;
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("[SuspenseGl] lookup by external_id '{}' failed: {}", suspenseExternalId, ex.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long move(Long fromAccountId, Long toAccountId, FundTransfer t, String description) {
|
||||
AccountTransferDTO dto = new AccountTransferDTO(LocalDate.now(), t.getAmount(), PortfolioAccountType.SAVINGS,
|
||||
PortfolioAccountType.SAVINGS, fromAccountId, toAccountId, description, null, null, null, null, null, null, null,
|
||||
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, null, null,
|
||||
Boolean.TRUE, Boolean.FALSE);
|
||||
Long txnId = transfersService.transferFunds(dto);
|
||||
log.info("[SuspenseGl] txnRef={} moved {} units {} → {} (internalTxnId={})",
|
||||
t.getTxnRef(), t.getAmount(), fromAccountId, toAccountId, txnId);
|
||||
return txnId;
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalTime;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Optional channel-operating-window guard. RBI made NEFT and RTGS 24×7 in 2019
|
||||
* and 2020 respectively, so the defaults are open; but banks often still enforce
|
||||
* business-hour windows internally for ops reasons. Property format:
|
||||
*
|
||||
* <pre>fineract.transfer.<channel>.window-hours=09-16</pre>
|
||||
*
|
||||
* Empty / unset = always open. Days-of-week format:
|
||||
*
|
||||
* <pre>fineract.transfer.<channel>.window-days=MON,TUE,WED,THU,FRI,SAT</pre>
|
||||
*
|
||||
* Empty / unset = all days.
|
||||
*/
|
||||
@Component
|
||||
public class TransferChannelWindowValidator {
|
||||
|
||||
@Value("${fineract.transfer.imps.window-hours:}")
|
||||
private String impsHours;
|
||||
@Value("${fineract.transfer.imps.window-days:}")
|
||||
private String impsDays;
|
||||
|
||||
@Value("${fineract.transfer.neft.window-hours:}")
|
||||
private String neftHours;
|
||||
@Value("${fineract.transfer.neft.window-days:}")
|
||||
private String neftDays;
|
||||
|
||||
@Value("${fineract.transfer.rtgs.window-hours:}")
|
||||
private String rtgsHours;
|
||||
@Value("${fineract.transfer.rtgs.window-days:}")
|
||||
private String rtgsDays;
|
||||
|
||||
public void requireOpen(TransferChannel channel, LocalTime nowTime, DayOfWeek nowDay) {
|
||||
String hours;
|
||||
String days;
|
||||
switch (channel) {
|
||||
case IMPS -> { hours = impsHours; days = impsDays; }
|
||||
case NEFT -> { hours = neftHours; days = neftDays; }
|
||||
case RTGS -> { hours = rtgsHours; days = rtgsDays; }
|
||||
default -> { return; }
|
||||
}
|
||||
if (!isHourInWindow(nowTime, hours)) {
|
||||
throw new IllegalStateException("OUTSIDE_" + channel + "_WINDOW — current hour outside configured window " + hours);
|
||||
}
|
||||
if (!isDayInWindow(nowDay, days)) {
|
||||
throw new IllegalStateException("OUTSIDE_" + channel + "_WINDOW — current day not in configured days " + days);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHourInWindow(LocalTime now, String spec) {
|
||||
if (spec == null || spec.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
String[] parts = spec.split("-");
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalStateException("Invalid window-hours format: " + spec + " (expected HH-HH)");
|
||||
}
|
||||
int start = Integer.parseInt(parts[0].trim());
|
||||
int end = Integer.parseInt(parts[1].trim());
|
||||
int hour = now.getHour();
|
||||
return hour >= start && hour < end;
|
||||
}
|
||||
|
||||
private static boolean isDayInWindow(DayOfWeek now, String spec) {
|
||||
if (spec == null || spec.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
for (String tok : spec.split(",")) {
|
||||
if (DayOfWeek.valueOf(tok.trim().toUpperCase()) == now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.data.ResolvedLimits;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.CustomerTransferLimit;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.CustomerTransferLimitRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferLimitConfig;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferLimitConfigRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Resolves effective per-customer-per-channel limits by overlaying any
|
||||
* {@link CustomerTransferLimit} row on top of the global {@link TransferLimitConfig}
|
||||
* defaults. Null fields on the customer override inherit the global value.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TransferLimitResolver {
|
||||
|
||||
private final TransferLimitConfigRepository globalRepository;
|
||||
private final CustomerTransferLimitRepository customerRepository;
|
||||
|
||||
public ResolvedLimits resolve(Long clientId, TransferChannel channel) {
|
||||
TransferLimitConfig global = globalRepository.findByChannelAndActiveTrue(channel)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"No active m_transfer_limit_config for channel " + channel + " — seed missing?"));
|
||||
|
||||
Optional<CustomerTransferLimit> override = customerRepository.findEffective(clientId, channel, LocalDate.now());
|
||||
|
||||
BigDecimal perTxnMax = override.map(CustomerTransferLimit::getPerTxnMax).orElse(null);
|
||||
if (perTxnMax == null) {
|
||||
perTxnMax = global.getPerTxnMax();
|
||||
}
|
||||
BigDecimal perDayMax = override.map(CustomerTransferLimit::getPerDayMax).orElse(null);
|
||||
if (perDayMax == null) {
|
||||
perDayMax = global.getPerDayMax();
|
||||
}
|
||||
|
||||
return new ResolvedLimits(global.getPerTxnMin(), perTxnMax, perDayMax, global.getMakerCheckerAbove());
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Two-phase cleanup of {@code m_verification_token}:
|
||||
* <ol>
|
||||
* <li>Flip ISSUED tokens past their {@code expires_at} to EXPIRED.</li>
|
||||
* <li>Delete REDEEMED + EXPIRED rows older than the retention cutoff.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Default retention: 24 hours. ISSUED rows are never deleted directly; they
|
||||
* have to pass through EXPIRED first, so a clock skew never silently drops a
|
||||
* token that's still potentially redeemable.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VerificationTokenCleanupService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VerificationTokenCleanupService.class);
|
||||
|
||||
private final VerificationTokenRepository repository;
|
||||
|
||||
@Value("${fineract.transfer.verification-token.retention-hours:24}")
|
||||
private long retentionHours;
|
||||
|
||||
@Transactional
|
||||
public CleanupResult cleanup() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int expired = repository.markExpired(now);
|
||||
int deleted = repository.deleteTerminalOlderThan(now.minusHours(retentionHours));
|
||||
log.info("[VerificationTokenCleanup] flipped {} → EXPIRED, deleted {} terminal token(s) older than {}h",
|
||||
expired, deleted, retentionHours);
|
||||
return new CleanupResult(expired, deleted);
|
||||
}
|
||||
|
||||
public record CleanupResult(int expired, int deleted) {}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferChannel;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.TransferDirection;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.VerificationToken;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenRepository;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.VerificationTokenStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Issues + redeems verification tokens (the BANL → debit bridge). */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VerificationTokenService {
|
||||
|
||||
/** Five minutes is long enough for a cashier interaction, short enough that stale tokens don't pile up. */
|
||||
public static final long TTL_SECONDS = 300L;
|
||||
|
||||
private final VerificationTokenRepository repository;
|
||||
|
||||
@Transactional
|
||||
public VerificationToken issue(TransferChannel channel, TransferDirection direction, Long cashierUserId, Long clientId,
|
||||
Long payerAccountId, String payeeAccountNumber, String payeeIfsc, String payeeMaskName, BigDecimal amount) {
|
||||
|
||||
VerificationToken t = new VerificationToken();
|
||||
t.setToken(UUID.randomUUID().toString());
|
||||
t.setChannel(channel);
|
||||
t.setDirection(direction);
|
||||
t.setCashierUserId(cashierUserId);
|
||||
t.setClientId(clientId);
|
||||
t.setPayerAccountId(payerAccountId);
|
||||
t.setPayeeAccountNumber(payeeAccountNumber);
|
||||
t.setPayeeIfsc(payeeIfsc);
|
||||
t.setPayeeMaskName(payeeMaskName);
|
||||
t.setAmount(amount);
|
||||
t.setStatus(VerificationTokenStatus.ISSUED);
|
||||
t.setIssuedAt(LocalDateTime.now());
|
||||
t.setExpiresAt(t.getIssuedAt().plusSeconds(TTL_SECONDS));
|
||||
return repository.save(t);
|
||||
}
|
||||
|
||||
public VerificationToken loadActive(String token) {
|
||||
VerificationToken t = repository.findByToken(token)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown verification token"));
|
||||
if (t.getStatus() == VerificationTokenStatus.REDEEMED) {
|
||||
throw new IllegalStateException("Token already redeemed — txn ref already issued");
|
||||
}
|
||||
if (t.isExpired()) {
|
||||
t.setStatus(VerificationTokenStatus.EXPIRED);
|
||||
repository.save(t);
|
||||
throw new IllegalStateException("Verification token expired — re-run /verify");
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markRedeemed(VerificationToken t, Long fundTransferId) {
|
||||
t.setStatus(VerificationTokenStatus.REDEEMED);
|
||||
t.setRedeemedAt(LocalDateTime.now());
|
||||
t.setRedeemedFundTransferId(fundTransferId);
|
||||
repository.save(t);
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service.strategy;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.imps.client.ImpsSwitchClient;
|
||||
import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* IMPS outward via LincPay's ReqPay endpoint. Maps the IMPS response code to
|
||||
* the {@link FundTransferStatus} lifecycle: 00 → SUCCESS, 91 → DEEMED (pending
|
||||
* reconciliation), anything else → FAILED.
|
||||
*
|
||||
* <p>Note: payer IFSC is read from {@code fineract.imps.our-default-ifsc} for
|
||||
* phase 1.5. Production should derive it from the payer account's branch office.
|
||||
*/
|
||||
@Component
|
||||
public class ImpsOutwardStrategy implements TransferStrategy {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImpsOutwardStrategy.class);
|
||||
|
||||
private final ImpsSwitchClient switchClient;
|
||||
|
||||
@Value("${fineract.imps.our-default-ifsc:BANK0000001}")
|
||||
private String ourDefaultIfsc;
|
||||
|
||||
public ImpsOutwardStrategy(ImpsSwitchClient switchClient) {
|
||||
this.switchClient = switchClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(FundTransfer transfer) {
|
||||
ImpsRequestData payload = buildPayload(transfer);
|
||||
try {
|
||||
ImpsResponseData resp = switchClient.reqPay(transfer.getTxnRef(), payload);
|
||||
applyResponse(transfer, resp);
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("[ImpsOutwardStrategy] txnRef={} switch call failed: {}", transfer.getTxnRef(), ex.toString());
|
||||
transfer.setStatus(FundTransferStatus.FAILED);
|
||||
transfer.setResponseCode("ERR");
|
||||
transfer.setResponseMessage(ex.getMessage());
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
private ImpsRequestData buildPayload(FundTransfer t) {
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
req.setTxnId(t.getTxnRef());
|
||||
req.setRefId(t.getTxnRef());
|
||||
req.setAmount(t.getAmount() == null ? null : t.getAmount().toPlainString());
|
||||
|
||||
PartyData payer = new PartyData();
|
||||
AccountData payerAcc = new AccountData();
|
||||
payerAcc.setAccountNumber(t.getPayerAccountNumber());
|
||||
payerAcc.setIfsc(ourDefaultIfsc);
|
||||
payer.setAccount(payerAcc);
|
||||
req.setPayer(payer);
|
||||
|
||||
PartyData payee = new PartyData();
|
||||
payee.setName(t.getPayeeName());
|
||||
AccountData payeeAcc = new AccountData();
|
||||
payeeAcc.setAccountNumber(t.getPayeeAccountNumber());
|
||||
payeeAcc.setIfsc(t.getPayeeIfsc());
|
||||
payee.setAccount(payeeAcc);
|
||||
req.setPayee(payee);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private void applyResponse(FundTransfer transfer, ImpsResponseData resp) {
|
||||
String code = resp.getResponseCode() == null ? "" : resp.getResponseCode().trim();
|
||||
transfer.setResponseCode(code.isEmpty() ? null : code);
|
||||
transfer.setResponseMessage(resp.getStatus());
|
||||
transfer.setSwitchTxnId(resp.getTxnId() == null ? transfer.getTxnRef() : resp.getTxnId());
|
||||
transfer.setUtr(resp.getUtr());
|
||||
|
||||
FundTransferStatus mapped = switch (code) {
|
||||
case "00" -> FundTransferStatus.SUCCESS;
|
||||
case "91" -> FundTransferStatus.DEEMED;
|
||||
default -> FundTransferStatus.FAILED;
|
||||
};
|
||||
transfer.setStatus(mapped);
|
||||
if (mapped != FundTransferStatus.DEEMED) {
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
log.info("[ImpsOutwardStrategy] txnRef={} responseCode={} → status={} utr={}",
|
||||
transfer.getTxnRef(), code, mapped, transfer.getUtr());
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service.strategy;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.apache.fineract.portfolio.fundtransfer.service.PayeeAccountLookupService;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* On-us settlement — debits payer + credits payee atomically via Fineract's
|
||||
* existing {@link AccountTransfersWritePlatformService#transferFunds}. Runs
|
||||
* inside the caller's JPA transaction so balance check, debit, credit, and
|
||||
* the FundTransfer row commit (or roll back) together.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InternalTransferStrategy implements TransferStrategy {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InternalTransferStrategy.class);
|
||||
|
||||
private final AccountTransfersWritePlatformService transfersService;
|
||||
private final PayeeAccountLookupService payeeLookup;
|
||||
|
||||
@Override
|
||||
public void execute(FundTransfer transfer) {
|
||||
SavingsAccount payee = payeeLookup.findByAccountNumber(transfer.getPayeeAccountNumber()).orElseThrow(
|
||||
() -> new IllegalStateException("Payee account vanished between verify and debit: " + transfer.getPayeeAccountNumber()));
|
||||
|
||||
AccountTransferDTO dto = new AccountTransferDTO(LocalDate.now(), transfer.getAmount(), PortfolioAccountType.SAVINGS,
|
||||
PortfolioAccountType.SAVINGS, transfer.getPayerAccountId(), payee.getId(),
|
||||
buildDescription(transfer), null, null, null, null, null, null, null,
|
||||
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, null, null,
|
||||
Boolean.TRUE, Boolean.FALSE);
|
||||
|
||||
Long transactionId = transfersService.transferFunds(dto);
|
||||
log.info("[InternalTransferStrategy] txnRef={} debited {} → credited {} ({} units) — internalTxnId={}",
|
||||
transfer.getTxnRef(), transfer.getPayerAccountId(), payee.getId(), transfer.getAmount(), transactionId);
|
||||
|
||||
transfer.setInternalTxnId(transactionId);
|
||||
transfer.setStatus(FundTransferStatus.SUCCESS);
|
||||
transfer.setResponseCode("00");
|
||||
transfer.setResponseMessage("Internal transfer settled");
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private static String buildDescription(FundTransfer t) {
|
||||
String r = t.getRemarks();
|
||||
return r == null || r.isBlank() ? "Fund transfer " + t.getTxnRef() : (t.getTxnRef() + " — " + r);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service.strategy;
|
||||
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* NEFT outward — enqueue only. NEFT in India is batched (currently half-hourly,
|
||||
* 24×7 since Dec 2019). The actual switch call happens in
|
||||
* {@code NeftBatchProcessorService}, which an ops cron / Quartz job triggers
|
||||
* once per cycle.
|
||||
*
|
||||
* <p>Status transition here: VERIFIED → QUEUED. Final status (SUCCESS / FAILED
|
||||
* / DEEMED) is written by the batch processor.
|
||||
*/
|
||||
@Component
|
||||
public class NeftOutwardStrategy implements TransferStrategy {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NeftOutwardStrategy.class);
|
||||
|
||||
@Override
|
||||
public void execute(FundTransfer transfer) {
|
||||
transfer.setStatus(FundTransferStatus.QUEUED);
|
||||
transfer.setResponseCode("QUEUED");
|
||||
transfer.setResponseMessage("Awaiting next NEFT batch cycle");
|
||||
log.info("[NeftOutwardStrategy] txnRef={} enqueued for next NEFT batch", transfer.getTxnRef());
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service.strategy;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.imps.client.ImpsSwitchClient;
|
||||
import org.apache.fineract.infrastructure.imps.data.AccountData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
|
||||
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
|
||||
import org.apache.fineract.infrastructure.imps.data.PartyData;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransferStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* RTGS outward — realtime, same shape as IMPS but routed to {@code /RTGS/ReqPay}.
|
||||
* RTGS in India: min ₹2 lakh (enforced by the limit config), 24×7 since Dec 2020.
|
||||
* Maker-checker is mandatory per the seeded config.
|
||||
*/
|
||||
@Component
|
||||
public class RtgsOutwardStrategy implements TransferStrategy {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RtgsOutwardStrategy.class);
|
||||
|
||||
private final ImpsSwitchClient switchClient;
|
||||
|
||||
@Value("${fineract.imps.our-default-ifsc:BANK0000001}")
|
||||
private String ourDefaultIfsc;
|
||||
|
||||
public RtgsOutwardStrategy(ImpsSwitchClient switchClient) {
|
||||
this.switchClient = switchClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(FundTransfer transfer) {
|
||||
ImpsRequestData payload = buildPayload(transfer);
|
||||
try {
|
||||
ImpsResponseData resp = switchClient.rtgsPay(transfer.getTxnRef(), payload);
|
||||
applyResponse(transfer, resp);
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("[RtgsOutwardStrategy] txnRef={} switch call failed: {}", transfer.getTxnRef(), ex.toString());
|
||||
transfer.setStatus(FundTransferStatus.FAILED);
|
||||
transfer.setResponseCode("ERR");
|
||||
transfer.setResponseMessage(ex.getMessage());
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
private ImpsRequestData buildPayload(FundTransfer t) {
|
||||
ImpsRequestData req = new ImpsRequestData();
|
||||
req.setTxnId(t.getTxnRef());
|
||||
req.setRefId(t.getTxnRef());
|
||||
req.setAmount(t.getAmount() == null ? null : t.getAmount().toPlainString());
|
||||
|
||||
PartyData payer = new PartyData();
|
||||
AccountData payerAcc = new AccountData();
|
||||
payerAcc.setAccountNumber(t.getPayerAccountNumber());
|
||||
payerAcc.setIfsc(ourDefaultIfsc);
|
||||
payer.setAccount(payerAcc);
|
||||
req.setPayer(payer);
|
||||
|
||||
PartyData payee = new PartyData();
|
||||
payee.setName(t.getPayeeName());
|
||||
AccountData payeeAcc = new AccountData();
|
||||
payeeAcc.setAccountNumber(t.getPayeeAccountNumber());
|
||||
payeeAcc.setIfsc(t.getPayeeIfsc());
|
||||
payee.setAccount(payeeAcc);
|
||||
req.setPayee(payee);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private void applyResponse(FundTransfer transfer, ImpsResponseData resp) {
|
||||
String code = resp.getResponseCode() == null ? "" : resp.getResponseCode().trim();
|
||||
transfer.setResponseCode(code.isEmpty() ? null : code);
|
||||
transfer.setResponseMessage(resp.getStatus());
|
||||
transfer.setSwitchTxnId(resp.getTxnId() == null ? transfer.getTxnRef() : resp.getTxnId());
|
||||
transfer.setUtr(resp.getUtr());
|
||||
|
||||
FundTransferStatus mapped = switch (code) {
|
||||
case "00" -> FundTransferStatus.SUCCESS;
|
||||
case "91" -> FundTransferStatus.DEEMED;
|
||||
default -> FundTransferStatus.FAILED;
|
||||
};
|
||||
transfer.setStatus(mapped);
|
||||
if (mapped != FundTransferStatus.DEEMED) {
|
||||
transfer.setSettledAt(LocalDateTime.now());
|
||||
}
|
||||
log.info("[RtgsOutwardStrategy] txnRef={} responseCode={} → status={} utr={}",
|
||||
transfer.getTxnRef(), code, mapped, transfer.getUtr());
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.portfolio.fundtransfer.service.strategy;
|
||||
|
||||
import org.apache.fineract.portfolio.fundtransfer.domain.FundTransfer;
|
||||
|
||||
/**
|
||||
* Executes a single, validated, approved transfer. Implementations must update the
|
||||
* {@link FundTransfer} row in place — final status, response code, switch_txn_id,
|
||||
* utr, settled_at — and persist via the caller's transaction.
|
||||
*/
|
||||
public interface TransferStrategy {
|
||||
|
||||
void execute(FundTransfer transfer);
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogPage;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogSearchCriteria;
|
||||
import org.apache.fineract.useradministration.mfa.log.service.MfaLogReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Admin viewer for MFA audit, scoped to two tabs the UI renders side by side:
|
||||
*
|
||||
* <ul>
|
||||
* <li>GET /v1/mfa/logs?tab=enrollment — device mapping / first-time verification events
|
||||
* (SMS / EMAIL / TOTP / DSC). Sourced from {@code m_user_mfa_factor} with related
|
||||
* enrolment-OTP / DSC-challenge context joined in.</li>
|
||||
* <li>GET /v1/mfa/logs?tab=login — login 2FA challenges. Sourced from {@code m_otp_log}
|
||||
* (purpose LIKE LOGIN_2FA*) excluding enrolment OTPs, UNION'd with {@code m_dsc_challenge}
|
||||
* rows of purpose LOGIN_2FA.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both tabs share filters: {@code factorType}, {@code status}, {@code userId},
|
||||
* {@code username}, {@code search}, {@code dateFrom}, {@code dateTo}, plus {@code page} /
|
||||
* {@code pageSize} for paging.
|
||||
*
|
||||
* <p>Permission: {@code READ_OTP_LOG} — same audit audience as the OTP log viewer.
|
||||
*/
|
||||
@Path("/v1/mfa/logs")
|
||||
@Component
|
||||
@Tag(name = "MFA Logs (Admin)", description = "Two-tab MFA audit: device mapping + login 2FA events")
|
||||
@RequiredArgsConstructor
|
||||
public class MfaLogApiResource {
|
||||
|
||||
private static final String RESOURCE = "OTP_LOG";
|
||||
private static final int DEFAULT_PAGE_SIZE = 25;
|
||||
private static final int MAX_PAGE_SIZE = 500;
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final MfaLogReadPlatformService readService;
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Paged MFA audit. Pass tab=enrollment for device-mapping events or "
|
||||
+ "tab=login for 2FA login events. Filters apply to both tabs.")
|
||||
public MfaLogPage list(
|
||||
@QueryParam("tab") @DefaultValue("enrollment") final String tab,
|
||||
@QueryParam("page") @DefaultValue("0") final int page,
|
||||
@QueryParam("pageSize") @DefaultValue("25") final int pageSize,
|
||||
@QueryParam("factorType") final String factorType,
|
||||
@QueryParam("status") final String status,
|
||||
@QueryParam("userId") final Long userId,
|
||||
@QueryParam("username") final String username,
|
||||
@QueryParam("search") final String search,
|
||||
@QueryParam("dateFrom") final String dateFrom,
|
||||
@QueryParam("dateTo") final String dateTo) {
|
||||
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE);
|
||||
|
||||
final int sanitizedPageSize = (pageSize <= 0 || pageSize > MAX_PAGE_SIZE) ? DEFAULT_PAGE_SIZE : pageSize;
|
||||
final int sanitizedPage = Math.max(0, page);
|
||||
|
||||
final MfaLogSearchCriteria criteria = MfaLogSearchCriteria.builder()
|
||||
.userId(userId)
|
||||
.username(blankToNull(username))
|
||||
.factorType(blankToNull(factorType))
|
||||
.status(blankToNull(status))
|
||||
.search(blankToNull(search))
|
||||
.fromDate(parseFromDate(dateFrom))
|
||||
.toDate(parseToDate(dateTo))
|
||||
.offset(sanitizedPage * sanitizedPageSize)
|
||||
.limit(sanitizedPageSize)
|
||||
.build();
|
||||
|
||||
return "login".equalsIgnoreCase(tab)
|
||||
? readService.retrieveLoginLogs(criteria)
|
||||
: readService.retrieveEnrollmentLogs(criteria);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static String blankToNull(final String s) {
|
||||
return s == null || s.isBlank() ? null : s;
|
||||
}
|
||||
|
||||
private static LocalDateTime parseFromDate(final String s) {
|
||||
if (s == null || s.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s).atStartOfDay();
|
||||
} catch (final Exception ignored) {
|
||||
try {
|
||||
return LocalDateTime.parse(s);
|
||||
} catch (final Exception ignored2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDateTime parseToDate(final String s) {
|
||||
if (s == null || s.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s).atTime(23, 59, 59, 999_000_000);
|
||||
} catch (final Exception ignored) {
|
||||
try {
|
||||
return LocalDateTime.parse(s);
|
||||
} catch (final Exception ignored2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* One row for either tab of the MFA audit grid. Shape is intentionally unified so the UI can
|
||||
* render both tabs from the same renderer / column set — the {@code tab} field tells it which
|
||||
* source the row came from in case the column ordering needs to differ.
|
||||
*
|
||||
* <p>Tab values:
|
||||
* <ul>
|
||||
* <li>{@code ENROLLMENT} — device-mapping event (first-time SMS / EMAIL / TOTP / DSC pairing).
|
||||
* Sourced from {@code m_user_mfa_factor} with related context joined from
|
||||
* {@code m_otp_log} (SMS/EMAIL) and {@code m_dsc_challenge} (DSC).</li>
|
||||
* <li>{@code LOGIN} — a 2FA challenge during sign-in. Sourced from {@code m_otp_log} rows
|
||||
* with purpose {@code LOGIN_2FA*} (excluding the OTPs that were used for enrolment) and
|
||||
* {@code m_dsc_challenge} rows with purpose {@code LOGIN_2FA}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class MfaLogData {
|
||||
|
||||
/** ENROLLMENT or LOGIN — which tab this row belongs to. */
|
||||
private final String tab;
|
||||
|
||||
/** Primary key of the underlying audit row. NOT globally unique across sources — composite with {@code source}. */
|
||||
private final Long id;
|
||||
|
||||
/** Source table: USER_MFA_FACTOR / OTP_LOG / DSC_CHALLENGE. */
|
||||
private final String source;
|
||||
|
||||
private final Long userId;
|
||||
private final String username;
|
||||
/** Composed full name from m_appuser (firstname + lastname). */
|
||||
private final String userFullName;
|
||||
|
||||
/** Normalised factor type — SMS / EMAIL / TOTP / DSC. */
|
||||
private final String factorType;
|
||||
|
||||
/** Masked recipient — "+91 XXXXXX2076" for SMS, "adXXXX@gmail.com" for email, common name for DSC, "Google Authenticator" for TOTP. */
|
||||
private final String destination;
|
||||
|
||||
/**
|
||||
* Status — for ENROLLMENT: ACTIVE / DISABLED / PENDING_VERIFICATION (factor row's verified+disabled state).
|
||||
* For LOGIN: PENDING / SENT / VERIFIED / FAILED / EXPIRED / LOCKED (carried verbatim from m_otp_log or m_dsc_challenge.status).
|
||||
*/
|
||||
private final String status;
|
||||
|
||||
/** When the event occurred (enrolment verified_on, or login OTP/challenge created_at). */
|
||||
private final LocalDateTime eventOn;
|
||||
|
||||
/** Verified timestamp when applicable (enrolment verified_on, or login verified_at). */
|
||||
private final OffsetDateTime verifiedOn;
|
||||
|
||||
/** Number of attempts (login OTP/challenge) — null for enrolment rows. */
|
||||
private final Integer attempts;
|
||||
|
||||
private final String ipAddress;
|
||||
private final String userAgent;
|
||||
|
||||
/** Human-readable summary — error_message on failure, synthesised text otherwise. */
|
||||
private final String message;
|
||||
|
||||
/** Provider — LINCPAY / EMAIL / TOTP / DSC. */
|
||||
private final String provider;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.data;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/** Paged result envelope — matches the shape used by {@code OtpLogPage}. */
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class MfaLogPage {
|
||||
|
||||
private final Long totalFilteredRecords;
|
||||
private final List<MfaLogData> pageItems;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Filter knobs for the MFA log search. All fields optional except {@code limit}/{@code offset}
|
||||
* (paging is always applied). {@code tab} drives which underlying query is used — service-level
|
||||
* concern, not exposed to the caller as part of the filter set.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class MfaLogSearchCriteria {
|
||||
|
||||
private final Long userId;
|
||||
private final String username;
|
||||
/** SMS / EMAIL / TOTP / DSC — null means all factor types. */
|
||||
private final String factorType;
|
||||
/** Status filter (ACTIVE/DISABLED for enrolment, VERIFIED/FAILED/EXPIRED for login). */
|
||||
private final String status;
|
||||
/** LIKE-match across username, destination (mobile/email). */
|
||||
private final String search;
|
||||
private final LocalDateTime fromDate;
|
||||
private final LocalDateTime toDate;
|
||||
private final int offset;
|
||||
private final int limit;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.service;
|
||||
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogPage;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogSearchCriteria;
|
||||
|
||||
public interface MfaLogReadPlatformService {
|
||||
|
||||
/** Tab 1 — device-mapping / first-time verification events. */
|
||||
MfaLogPage retrieveEnrollmentLogs(MfaLogSearchCriteria criteria);
|
||||
|
||||
/** Tab 2 — login 2FA challenges (SMS / EMAIL / TOTP / DSC). */
|
||||
MfaLogPage retrieveLoginLogs(MfaLogSearchCriteria criteria);
|
||||
}
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 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.useradministration.mfa.log.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogData;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogPage;
|
||||
import org.apache.fineract.useradministration.mfa.log.data.MfaLogSearchCriteria;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Tab 1 (enrolment) is sourced from {@code m_user_mfa_factor} — the canonical "which devices is
|
||||
* this user paired with" table — with related context joined in from {@code m_otp_log} so SMS /
|
||||
* EMAIL enrolments carry the IP / provider / OTP reference that proved possession.
|
||||
*
|
||||
* <p>Tab 2 (login) is a UNION ALL of {@code m_otp_log} (filtered to login OTPs, excluding the
|
||||
* OTP rows whose ids are referenced by a factor — those were the one-time enrolment OTPs) and
|
||||
* {@code m_dsc_challenge} (purpose=LOGIN_2FA). Filters and paging are applied to the merged set.
|
||||
*
|
||||
* <p>TOTP login attempts also write a row into {@code m_otp_log} with provider=TOTP from
|
||||
* {@code MfaFactorBackedTwoFactorServiceImpl}, so they show up in Tab 2 alongside SMS / EMAIL /
|
||||
* DSC.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class MfaLogReadPlatformServiceImpl implements MfaLogReadPlatformService {
|
||||
|
||||
private static final String TAB_ENROLLMENT = "ENROLLMENT";
|
||||
private static final String TAB_LOGIN = "LOGIN";
|
||||
|
||||
private static final String SOURCE_FACTOR = "USER_MFA_FACTOR";
|
||||
private static final String SOURCE_OTP = "OTP_LOG";
|
||||
private static final String SOURCE_DSC = "DSC_CHALLENGE";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
// ── Tab 1: ENROLLMENT ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public MfaLogPage retrieveEnrollmentLogs(final MfaLogSearchCriteria criteria) {
|
||||
final List<Object> params = new ArrayList<>();
|
||||
// verified=true → only completed device-mapping events. Unverified rows are pending TOTP
|
||||
// QR setup that hasn't been confirmed with a code yet; we surface those separately or
|
||||
// simply hide them from the audit grid.
|
||||
final StringBuilder where = new StringBuilder(" WHERE uf.verified = true ");
|
||||
|
||||
if (criteria.getUserId() != null) {
|
||||
where.append(" AND uf.appuser_id = ? ");
|
||||
params.add(criteria.getUserId());
|
||||
}
|
||||
if (notBlank(criteria.getUsername())) {
|
||||
where.append(" AND u.username = ? ");
|
||||
params.add(criteria.getUsername());
|
||||
}
|
||||
if (notBlank(criteria.getFactorType())) {
|
||||
where.append(" AND uf.factor_type = ? ");
|
||||
params.add(criteria.getFactorType().toUpperCase());
|
||||
}
|
||||
if (notBlank(criteria.getStatus())) {
|
||||
// ENROLLMENT-side status synthesised from disabled_on. Translate the input.
|
||||
final String s = criteria.getStatus().toUpperCase();
|
||||
if ("ACTIVE".equals(s)) {
|
||||
where.append(" AND uf.disabled_on IS NULL ");
|
||||
} else if ("DISABLED".equals(s)) {
|
||||
where.append(" AND uf.disabled_on IS NOT NULL ");
|
||||
}
|
||||
}
|
||||
if (notBlank(criteria.getSearch())) {
|
||||
where.append(" AND (u.username LIKE ? OR uf.destination LIKE ?) ");
|
||||
final String like = "%" + criteria.getSearch() + "%";
|
||||
params.add(like);
|
||||
params.add(like);
|
||||
}
|
||||
if (criteria.getFromDate() != null) {
|
||||
where.append(" AND COALESCE(uf.verified_on, uf.created_on) >= ? ");
|
||||
params.add(Timestamp.valueOf(criteria.getFromDate()));
|
||||
}
|
||||
if (criteria.getToDate() != null) {
|
||||
where.append(" AND COALESCE(uf.verified_on, uf.created_on) <= ? ");
|
||||
params.add(Timestamp.valueOf(criteria.getToDate()));
|
||||
}
|
||||
|
||||
final String baseFrom = " FROM m_user_mfa_factor uf "
|
||||
+ " LEFT JOIN m_appuser u ON u.id = uf.appuser_id "
|
||||
+ " LEFT JOIN m_otp_log ol ON ol.id = uf.otp_log_id ";
|
||||
|
||||
final String countSql = "SELECT COUNT(*) " + baseFrom + where;
|
||||
final Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray()); // NOSONAR
|
||||
if (total == null || total == 0L) {
|
||||
return new MfaLogPage(0L, Collections.emptyList());
|
||||
}
|
||||
|
||||
final String dataSql = "SELECT "
|
||||
+ " uf.id AS row_id, uf.factor_type, uf.destination, uf.verified, uf.verified_on, "
|
||||
+ " uf.disabled_on, uf.created_on, "
|
||||
+ " u.id AS user_id, u.username, u.firstname, u.lastname, "
|
||||
+ " ol.provider AS otp_provider, ol.ip_address AS otp_ip, ol.user_agent AS otp_ua, "
|
||||
+ " ol.otp_reference, ol.error_message AS otp_err, ol.attempts AS otp_attempts "
|
||||
+ baseFrom + where
|
||||
+ " ORDER BY COALESCE(uf.verified_on, uf.created_on) DESC "
|
||||
+ " LIMIT ? OFFSET ? ";
|
||||
final List<Object> dataParams = new ArrayList<>(params);
|
||||
dataParams.add(criteria.getLimit() <= 0 ? 25 : criteria.getLimit());
|
||||
dataParams.add(Math.max(0, criteria.getOffset()));
|
||||
|
||||
final List<MfaLogData> rows = jdbcTemplate.query(dataSql, enrollmentRowMapper(), dataParams.toArray()); // NOSONAR
|
||||
return new MfaLogPage(total, rows);
|
||||
}
|
||||
|
||||
private RowMapper<MfaLogData> enrollmentRowMapper() {
|
||||
return (final ResultSet rs, final int rowNum) -> {
|
||||
final String factorType = upper(rs.getString("factor_type"));
|
||||
final String storedDestination = rs.getString("destination");
|
||||
final String destination = maskDestination(factorType, storedDestination);
|
||||
final boolean disabled = rs.getTimestamp("disabled_on") != null;
|
||||
final String status = disabled ? "DISABLED" : "ACTIVE";
|
||||
final String provider = providerForEnrolment(factorType, rs.getString("otp_provider"));
|
||||
final String message = composeEnrolmentMessage(factorType, rs.getString("otp_reference"), rs.getString("otp_err"));
|
||||
|
||||
return MfaLogData.builder()
|
||||
.tab(TAB_ENROLLMENT)
|
||||
.id(rs.getLong("row_id"))
|
||||
.source(SOURCE_FACTOR)
|
||||
.userId(getNullableLong(rs, "user_id"))
|
||||
.username(rs.getString("username"))
|
||||
.userFullName(composeFullName(rs.getString("firstname"), rs.getString("lastname")))
|
||||
.factorType(factorType)
|
||||
.destination(destination)
|
||||
.status(status)
|
||||
.eventOn(toLocalDateTime(rs.getTimestamp("verified_on") != null ? rs.getTimestamp("verified_on") : rs.getTimestamp("created_on")))
|
||||
.verifiedOn(toOffsetDateTime(rs.getTimestamp("verified_on")))
|
||||
.attempts(getNullableInt(rs, "otp_attempts"))
|
||||
.ipAddress(rs.getString("otp_ip"))
|
||||
.userAgent(rs.getString("otp_ua"))
|
||||
.message(message)
|
||||
.provider(provider)
|
||||
.build();
|
||||
};
|
||||
}
|
||||
|
||||
// ── Tab 2: LOGIN ─────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public MfaLogPage retrieveLoginLogs(final MfaLogSearchCriteria criteria) {
|
||||
// UNION ALL of OTP login rows (excluding the enrolment OTPs referenced by a factor) and
|
||||
// DSC login challenges. Both halves project to the same column shape so the outer SELECT
|
||||
// can paginate, sort, and apply filters uniformly.
|
||||
final String unionInner = "("
|
||||
// OTP half — login OTPs only (LOGIN_2FA / LOGIN_2FA_EMAIL) and TOTP login rows.
|
||||
// NOT IN excludes the OTP id that was used to enrol an SMS/EMAIL factor.
|
||||
+ " SELECT "
|
||||
+ " o.id AS row_id, 'OTP_LOG' AS source, "
|
||||
+ " CASE WHEN o.provider = 'LINCPAY' THEN 'SMS' "
|
||||
+ " WHEN o.provider = 'EMAIL' THEN 'EMAIL' "
|
||||
+ " WHEN o.provider = 'TOTP' THEN 'TOTP' "
|
||||
+ " ELSE o.provider END AS factor_type, "
|
||||
+ " CASE WHEN o.provider = 'LINCPAY' THEN o.mobile_number "
|
||||
+ " WHEN o.provider = 'EMAIL' THEN o.email_address "
|
||||
+ " WHEN o.provider = 'TOTP' THEN 'Google Authenticator' "
|
||||
+ " ELSE COALESCE(o.mobile_number, o.email_address) END AS destination, "
|
||||
+ " o.user_id, o.username, "
|
||||
+ " u.firstname, u.lastname, "
|
||||
+ " o.status, o.attempts, "
|
||||
+ " o.created_at AS event_on, o.verified_at AS verified_on, "
|
||||
+ " o.ip_address, o.user_agent, o.error_message, "
|
||||
+ " o.otp_reference, o.provider "
|
||||
+ " FROM m_otp_log o "
|
||||
+ " LEFT JOIN m_appuser u ON u.id = o.user_id "
|
||||
+ " WHERE o.purpose LIKE 'LOGIN_2FA%' "
|
||||
+ " AND o.id NOT IN (SELECT otp_log_id FROM m_user_mfa_factor WHERE otp_log_id IS NOT NULL) "
|
||||
|
||||
+ " UNION ALL "
|
||||
|
||||
// DSC half — login challenges only.
|
||||
+ " SELECT "
|
||||
+ " c.id AS row_id, 'DSC_CHALLENGE' AS source, "
|
||||
+ " 'DSC' AS factor_type, "
|
||||
+ " c.cert_serial_returned AS destination, "
|
||||
+ " c.appuser_id AS user_id, u.username, "
|
||||
+ " u.firstname, u.lastname, "
|
||||
+ " c.status, c.attempts, "
|
||||
+ " c.created_at AS event_on, c.verified_at AS verified_on, "
|
||||
+ " c.ip_address, c.user_agent, c.error_message, "
|
||||
+ " NULL AS otp_reference, 'DSC' AS provider "
|
||||
+ " FROM m_dsc_challenge c "
|
||||
+ " LEFT JOIN m_appuser u ON u.id = c.appuser_id "
|
||||
+ " WHERE c.purpose = 'LOGIN_2FA' "
|
||||
+ ") merged ";
|
||||
|
||||
final List<Object> params = new ArrayList<>();
|
||||
final StringBuilder where = new StringBuilder(" WHERE 1=1 ");
|
||||
if (criteria.getUserId() != null) {
|
||||
where.append(" AND merged.user_id = ? ");
|
||||
params.add(criteria.getUserId());
|
||||
}
|
||||
if (notBlank(criteria.getUsername())) {
|
||||
where.append(" AND merged.username = ? ");
|
||||
params.add(criteria.getUsername());
|
||||
}
|
||||
if (notBlank(criteria.getFactorType())) {
|
||||
where.append(" AND merged.factor_type = ? ");
|
||||
params.add(criteria.getFactorType().toUpperCase());
|
||||
}
|
||||
if (notBlank(criteria.getStatus())) {
|
||||
where.append(" AND merged.status = ? ");
|
||||
params.add(criteria.getStatus().toUpperCase());
|
||||
}
|
||||
if (notBlank(criteria.getSearch())) {
|
||||
where.append(" AND (merged.username LIKE ? OR merged.destination LIKE ? OR merged.otp_reference LIKE ?) ");
|
||||
final String like = "%" + criteria.getSearch() + "%";
|
||||
params.add(like);
|
||||
params.add(like);
|
||||
params.add(like);
|
||||
}
|
||||
if (criteria.getFromDate() != null) {
|
||||
where.append(" AND merged.event_on >= ? ");
|
||||
params.add(Timestamp.valueOf(criteria.getFromDate()));
|
||||
}
|
||||
if (criteria.getToDate() != null) {
|
||||
where.append(" AND merged.event_on <= ? ");
|
||||
params.add(Timestamp.valueOf(criteria.getToDate()));
|
||||
}
|
||||
|
||||
final String countSql = "SELECT COUNT(*) FROM " + unionInner + where;
|
||||
final Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray()); // NOSONAR
|
||||
if (total == null || total == 0L) {
|
||||
return new MfaLogPage(0L, Collections.emptyList());
|
||||
}
|
||||
|
||||
final String dataSql = "SELECT * FROM " + unionInner + where
|
||||
+ " ORDER BY merged.event_on DESC, merged.row_id DESC "
|
||||
+ " LIMIT ? OFFSET ? ";
|
||||
final List<Object> dataParams = new ArrayList<>(params);
|
||||
dataParams.add(criteria.getLimit() <= 0 ? 25 : criteria.getLimit());
|
||||
dataParams.add(Math.max(0, criteria.getOffset()));
|
||||
|
||||
final List<MfaLogData> rows = jdbcTemplate.query(dataSql, loginRowMapper(), dataParams.toArray()); // NOSONAR
|
||||
return new MfaLogPage(total, rows);
|
||||
}
|
||||
|
||||
private RowMapper<MfaLogData> loginRowMapper() {
|
||||
return (final ResultSet rs, final int rowNum) -> {
|
||||
final String factorType = upper(rs.getString("factor_type"));
|
||||
final String storedDestination = rs.getString("destination");
|
||||
// Mask the destination for the login grid the same way the existing OTP log does.
|
||||
final String destination = maskDestination(factorType, storedDestination);
|
||||
final String source = rs.getString("source");
|
||||
final String status = upper(rs.getString("status"));
|
||||
final String message = composeLoginMessage(status, rs.getString("error_message"), rs.getString("otp_reference"));
|
||||
|
||||
return MfaLogData.builder()
|
||||
.tab(TAB_LOGIN)
|
||||
.id(rs.getLong("row_id"))
|
||||
.source(source)
|
||||
.userId(getNullableLong(rs, "user_id"))
|
||||
.username(rs.getString("username"))
|
||||
.userFullName(composeFullName(rs.getString("firstname"), rs.getString("lastname")))
|
||||
.factorType(factorType)
|
||||
.destination(destination)
|
||||
.status(status)
|
||||
.eventOn(toLocalDateTime(rs.getTimestamp("event_on")))
|
||||
.verifiedOn(toOffsetDateTime(rs.getTimestamp("verified_on")))
|
||||
.attempts(getNullableInt(rs, "attempts"))
|
||||
.ipAddress(rs.getString("ip_address"))
|
||||
.userAgent(rs.getString("user_agent"))
|
||||
.message(message)
|
||||
.provider(rs.getString("provider"))
|
||||
.build();
|
||||
};
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static String composeEnrolmentMessage(final String factorType, final String otpRef, final String otpErr) {
|
||||
if (notBlank(otpErr)) {
|
||||
return otpErr;
|
||||
}
|
||||
if ("SMS".equalsIgnoreCase(factorType) || "EMAIL".equalsIgnoreCase(factorType)) {
|
||||
if (notBlank(otpRef)) {
|
||||
return factorType.toUpperCase() + " OTP verified (req#" + otpRef + ")";
|
||||
}
|
||||
return factorType.toUpperCase() + " OTP verified";
|
||||
}
|
||||
if ("TOTP".equalsIgnoreCase(factorType)) {
|
||||
return "Google Authenticator device paired";
|
||||
}
|
||||
if ("DSC".equalsIgnoreCase(factorType)) {
|
||||
return "Digital Signature Certificate mapped";
|
||||
}
|
||||
return factorType + " factor enrolled";
|
||||
}
|
||||
|
||||
private static String composeLoginMessage(final String status, final String errorMessage, final String otpReference) {
|
||||
if (notBlank(errorMessage)) {
|
||||
return errorMessage;
|
||||
}
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
switch (status) {
|
||||
case "VERIFIED":
|
||||
return "2FA verified";
|
||||
case "SENT":
|
||||
return notBlank(otpReference) ? "OTP delivered (req#" + otpReference + ")" : "OTP delivered";
|
||||
case "EXPIRED":
|
||||
return "Challenge expired";
|
||||
case "FAILED":
|
||||
return "Verification failed";
|
||||
case "PENDING":
|
||||
return "Pending verification";
|
||||
case "LOCKED":
|
||||
return "Locked — too many attempts";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
private static String providerForEnrolment(final String factorType, final String otpProvider) {
|
||||
if ("DSC".equalsIgnoreCase(factorType)) {
|
||||
return "DSC";
|
||||
}
|
||||
if ("TOTP".equalsIgnoreCase(factorType)) {
|
||||
return "TOTP";
|
||||
}
|
||||
return otpProvider;
|
||||
}
|
||||
|
||||
/** Mask SMS / email destinations; pass DSC / TOTP labels through unchanged. */
|
||||
private static String maskDestination(final String factorType, final String stored) {
|
||||
if (stored == null || stored.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
if ("SMS".equalsIgnoreCase(factorType)) {
|
||||
return maskMobile(stored);
|
||||
}
|
||||
if ("EMAIL".equalsIgnoreCase(factorType)) {
|
||||
return maskEmail(stored);
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
private static String maskMobile(final String mobile) {
|
||||
if (mobile == null || mobile.length() <= 4) {
|
||||
return mobile;
|
||||
}
|
||||
String prefix = "";
|
||||
String digits = mobile;
|
||||
if (mobile.startsWith("+") && mobile.length() > 3) {
|
||||
prefix = mobile.substring(0, 3) + " ";
|
||||
digits = mobile.substring(3);
|
||||
}
|
||||
if (digits.length() <= 4) {
|
||||
return prefix + digits;
|
||||
}
|
||||
final String last4 = digits.substring(digits.length() - 4);
|
||||
final int hidden = digits.length() - 4;
|
||||
return prefix + "X".repeat(hidden) + last4;
|
||||
}
|
||||
|
||||
private static String maskEmail(final String email) {
|
||||
if (email == null) {
|
||||
return null;
|
||||
}
|
||||
final int at = email.indexOf('@');
|
||||
if (at < 0) {
|
||||
return email;
|
||||
}
|
||||
final String local = email.substring(0, at);
|
||||
final String domain = email.substring(at);
|
||||
if (local.length() <= 2) {
|
||||
return local + "XXXX" + domain;
|
||||
}
|
||||
return local.substring(0, 2) + "XXXX" + domain;
|
||||
}
|
||||
|
||||
private static String composeFullName(final String firstname, final String lastname) {
|
||||
if (firstname == null && lastname == null) {
|
||||
return null;
|
||||
}
|
||||
if (firstname == null) {
|
||||
return lastname;
|
||||
}
|
||||
if (lastname == null) {
|
||||
return firstname;
|
||||
}
|
||||
return (firstname + " " + lastname).trim();
|
||||
}
|
||||
|
||||
private static String upper(final String s) {
|
||||
return s == null ? null : s.toUpperCase();
|
||||
}
|
||||
|
||||
private static boolean notBlank(final String s) {
|
||||
return s != null && !s.isBlank();
|
||||
}
|
||||
|
||||
private static Long getNullableLong(final ResultSet rs, final String col) throws SQLException {
|
||||
final long v = rs.getLong(col);
|
||||
return rs.wasNull() ? null : v;
|
||||
}
|
||||
|
||||
private static Integer getNullableInt(final ResultSet rs, final String col) throws SQLException {
|
||||
final int v = rs.getInt(col);
|
||||
return rs.wasNull() ? null : v;
|
||||
}
|
||||
|
||||
private static LocalDateTime toLocalDateTime(final Timestamp ts) {
|
||||
return ts == null ? null : ts.toLocalDateTime();
|
||||
}
|
||||
|
||||
private static OffsetDateTime toOffsetDateTime(final Timestamp ts) {
|
||||
return ts == null ? null : ts.toLocalDateTime().atOffset(ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
@@ -327,4 +327,16 @@
|
||||
<include file="parts/0018_cash_reconciliation.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0019_cash_reversal_columns.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0020_cash_permissions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0300_create_m_imps_transaction.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0301_create_m_bank_ifsc.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0302_create_m_transfer_limit_config.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0303_create_m_customer_transfer_limit.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0304_create_m_fund_transfer.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0305_create_m_fund_transfer_event.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0306_create_m_verification_token.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0307_seed_fund_transfer_permissions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0308_grant_fund_transfer_permissions_to_roles.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0309_phase3_jobs_and_recall_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0310_fund_transfer_roles_and_token_cleanup_job.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0311_create_fund_transfer_suspense_account.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
One row per inbound IMPS request received by Fineract acting as the CBS
|
||||
under IMPS_BANK_API_SPEC_CBS_1.0 §6. Used for audit + idempotency: a
|
||||
retried (api, txn_id) replays the prior recorded response.
|
||||
-->
|
||||
<changeSet id="0300_create_m_imps_transaction" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_imps_transaction"/></not></preConditions>
|
||||
<createTable tableName="m_imps_transaction">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
|
||||
<!-- REQ_PAY / REQ_VAL_ADD / REQ_CHK_TXN -->
|
||||
<column name="api" type="VARCHAR(32)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="txn_id" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="org_txn_id" type="VARCHAR(64)"/>
|
||||
<column name="ref_id" type="VARCHAR(64)"/>
|
||||
<column name="utr" type="VARCHAR(64)"/>
|
||||
<column name="amount" type="VARCHAR(32)"/>
|
||||
|
||||
<column name="payee_account_number" type="VARCHAR(64)"/>
|
||||
<column name="payee_ifsc" type="VARCHAR(32)"/>
|
||||
|
||||
<!-- SUCCESS / DEEMED / FAILED -->
|
||||
<column name="status" type="VARCHAR(16)"/>
|
||||
<!-- 00 / 91 / XC / 8 / etc. -->
|
||||
<column name="response_code" type="VARCHAR(8)"/>
|
||||
|
||||
<column name="request_json" type="CLOB"/>
|
||||
<column name="response_json" type="CLOB"/>
|
||||
|
||||
<!-- Standard Fineract audit columns (AbstractAuditableWithUTCDateTimeCustom). -->
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="version" type="BIGINT" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<!-- Idempotency: a retried (api, txn_id) resolves to the prior row. -->
|
||||
<addUniqueConstraint tableName="m_imps_transaction" columnNames="api, txn_id"
|
||||
constraintName="uq_imps_txn_api_txnid"/>
|
||||
|
||||
<!-- Common audit queries: latest activity per payee account. -->
|
||||
<createIndex tableName="m_imps_transaction" indexName="idx_imps_txn_payee_account">
|
||||
<column name="payee_account_number"/>
|
||||
</createIndex>
|
||||
|
||||
<!-- Reconciliation by UTR. -->
|
||||
<createIndex tableName="m_imps_transaction" indexName="idx_imps_txn_utr">
|
||||
<column name="utr"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
OUR bank's IFSC codes. Used to classify a transfer as ON_US (payee.ifsc matches
|
||||
a row here) vs OFF_US (route via switch). The real RBI/sponsor-bank list is
|
||||
bulk-loaded at deployment time via tenant data-import — the placeholder row
|
||||
below just keeps a non-empty set for dev.
|
||||
-->
|
||||
<changeSet id="0301_create_m_bank_ifsc" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_bank_ifsc"/></not></preConditions>
|
||||
<createTable tableName="m_bank_ifsc">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="ifsc_code" type="VARCHAR(11)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_bank_ifsc_code"/>
|
||||
</column>
|
||||
<column name="branch_code" type="VARCHAR(16)"/>
|
||||
<column name="branch_name" type="VARCHAR(255)"/>
|
||||
<column name="is_active" type="boolean" defaultValueBoolean="true">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="version" type="BIGINT" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0301_seed_placeholder_bank_ifsc" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_bank_ifsc</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_bank_ifsc">
|
||||
<column name="ifsc_code" value="BANK0000001"/>
|
||||
<column name="branch_code" value="MAIN"/>
|
||||
<column name="branch_name" value="Head Office (placeholder — replace with sponsor-bank IFSC list before go-live)"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="created_by" valueNumeric="1"/>
|
||||
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
<column name="last_modified_by" valueNumeric="1"/>
|
||||
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Global per-channel transfer limits + maker-checker thresholds. Per-customer
|
||||
overrides live in m_customer_transfer_limit. Resolution = customer override
|
||||
first, fall back to this table.
|
||||
|
||||
maker_checker_above semantics:
|
||||
NULL or 0 → every transfer needs maker-checker
|
||||
huge val → never (auto-execute)
|
||||
else → maker-checker required when amount >= threshold
|
||||
-->
|
||||
<changeSet id="0302_create_m_transfer_limit_config" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_transfer_limit_config"/></not></preConditions>
|
||||
<createTable tableName="m_transfer_limit_config">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="channel" type="VARCHAR(16)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="direction" type="VARCHAR(8)"/>
|
||||
<column name="per_txn_min" type="DECIMAL(19,4)" defaultValueNumeric="1.00"/>
|
||||
<column name="per_txn_max" type="DECIMAL(19,4)"/>
|
||||
<column name="per_day_max" type="DECIMAL(19,4)"/>
|
||||
<column name="maker_checker_above" type="DECIMAL(19,4)"/>
|
||||
<column name="is_active" type="boolean" defaultValueBoolean="true">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="version" type="BIGINT" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addUniqueConstraint tableName="m_transfer_limit_config" columnNames="channel"
|
||||
constraintName="uq_transfer_limit_config_channel"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0302_seed_transfer_limit_config_defaults" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_transfer_limit_config</sqlCheck>
|
||||
</preConditions>
|
||||
<!-- INTERNAL (on-us): no upper cap, maker-checker at ₹50,000 -->
|
||||
<insert tableName="m_transfer_limit_config">
|
||||
<column name="channel" value="INTERNAL"/>
|
||||
<column name="per_txn_min" valueNumeric="1.00"/>
|
||||
<column name="maker_checker_above" valueNumeric="50000.00"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="created_by" valueNumeric="1"/>
|
||||
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
<column name="last_modified_by" valueNumeric="1"/>
|
||||
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
<!-- IMPS: per RBI, ₹5 lakh per-txn cap; maker-checker at ₹1 lakh -->
|
||||
<insert tableName="m_transfer_limit_config">
|
||||
<column name="channel" value="IMPS"/>
|
||||
<column name="per_txn_min" valueNumeric="1.00"/>
|
||||
<column name="per_txn_max" valueNumeric="500000.00"/>
|
||||
<column name="per_day_max" valueNumeric="1000000.00"/>
|
||||
<column name="maker_checker_above" valueNumeric="100000.00"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="created_by" valueNumeric="1"/>
|
||||
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
<column name="last_modified_by" valueNumeric="1"/>
|
||||
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
<!-- NEFT: no cap, maker-checker at ₹2 lakh -->
|
||||
<insert tableName="m_transfer_limit_config">
|
||||
<column name="channel" value="NEFT"/>
|
||||
<column name="per_txn_min" valueNumeric="1.00"/>
|
||||
<column name="maker_checker_above" valueNumeric="200000.00"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="created_by" valueNumeric="1"/>
|
||||
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
<column name="last_modified_by" valueNumeric="1"/>
|
||||
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
<!-- RTGS: min ₹2 lakh, maker-checker always -->
|
||||
<insert tableName="m_transfer_limit_config">
|
||||
<column name="channel" value="RTGS"/>
|
||||
<column name="per_txn_min" valueNumeric="200000.00"/>
|
||||
<column name="maker_checker_above" valueNumeric="0.00"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="created_by" valueNumeric="1"/>
|
||||
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
<column name="last_modified_by" valueNumeric="1"/>
|
||||
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Per-customer override of (per_txn_max, per_day_max) for a given channel.
|
||||
NULL on either column inherits the global default from m_transfer_limit_config.
|
||||
Effective dating lets us raise/lower a customer's limit at KYC re-review without
|
||||
deleting history.
|
||||
-->
|
||||
<changeSet id="0303_create_m_customer_transfer_limit" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_customer_transfer_limit"/></not></preConditions>
|
||||
<createTable tableName="m_customer_transfer_limit">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="client_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="channel" type="VARCHAR(16)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="per_txn_max" type="DECIMAL(19,4)"/>
|
||||
<column name="per_day_max" type="DECIMAL(19,4)"/>
|
||||
<column name="effective_from" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="effective_to" type="DATE"/>
|
||||
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="version" type="BIGINT" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseTableName="m_customer_transfer_limit" baseColumnNames="client_id"
|
||||
constraintName="fk_cust_transfer_limit_client"
|
||||
referencedTableName="m_client" referencedColumnNames="id"/>
|
||||
|
||||
<addUniqueConstraint tableName="m_customer_transfer_limit"
|
||||
columnNames="client_id, channel, effective_from"
|
||||
constraintName="uq_cust_transfer_limit_scope"/>
|
||||
|
||||
<createIndex tableName="m_customer_transfer_limit" indexName="idx_cust_transfer_limit_lookup">
|
||||
<column name="client_id"/>
|
||||
<column name="channel"/>
|
||||
<column name="effective_from"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
One row per customer-initiated outward fund transfer (INTERNAL/IMPS/NEFT/RTGS).
|
||||
Distinct from m_imps_transaction which tracks INBOUND IMPS calls Fineract
|
||||
receives as the bank under §6.2.x.
|
||||
|
||||
Lifecycle:
|
||||
INITIATED → VALIDATED → VERIFIED → IN_PROGRESS → SUCCESS / DEEMED / FAILED / REVERSED
|
||||
Plus an approval axis: AUTO / PENDING_APPROVAL / APPROVED / REJECTED
|
||||
-->
|
||||
<changeSet id="0304_create_m_fund_transfer" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_fund_transfer"/></not></preConditions>
|
||||
<createTable tableName="m_fund_transfer">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="txn_ref" type="VARCHAR(64)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_fund_transfer_ref"/>
|
||||
</column>
|
||||
<column name="channel" type="VARCHAR(16)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="direction" type="VARCHAR(8)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(24)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="approval_status" type="VARCHAR(16)" defaultValue="AUTO">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="response_code" type="VARCHAR(8)"/>
|
||||
<column name="response_message" type="VARCHAR(255)"/>
|
||||
|
||||
<!-- Cashier (initiator) + customer (owner of payer account) -->
|
||||
<column name="cashier_user_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="client_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<!-- Payer side -->
|
||||
<column name="payer_account_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payer_account_number" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<!-- Payee side -->
|
||||
<column name="payee_account_number" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payee_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="payee_name" type="VARCHAR(255)"/>
|
||||
<column name="payee_mask_name" type="VARCHAR(255)"/>
|
||||
|
||||
<column name="amount" type="DECIMAL(19,4)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="remarks" type="VARCHAR(255)"/>
|
||||
|
||||
<!-- External handles -->
|
||||
<column name="internal_txn_id" type="BIGINT"/>
|
||||
<column name="switch_txn_id" type="VARCHAR(64)"/>
|
||||
<column name="utr" type="VARCHAR(64)"/>
|
||||
|
||||
<!-- Maker-checker linkage -->
|
||||
<column name="command_source_id" type="BIGINT"/>
|
||||
|
||||
<column name="initiated_at" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="settled_at" type="DATETIME"/>
|
||||
|
||||
<column name="created_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="last_modified_on_utc" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="version" type="BIGINT" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="cashier_user_id"
|
||||
constraintName="fk_fund_transfer_cashier"
|
||||
referencedTableName="m_appuser" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="client_id"
|
||||
constraintName="fk_fund_transfer_client"
|
||||
referencedTableName="m_client" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="payer_account_id"
|
||||
constraintName="fk_fund_transfer_payer_acc"
|
||||
referencedTableName="m_savings_account" referencedColumnNames="id"/>
|
||||
|
||||
<!-- Cashier's pipeline + daily-limit summation -->
|
||||
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_payer_initiated">
|
||||
<column name="payer_account_id"/>
|
||||
<column name="initiated_at"/>
|
||||
</createIndex>
|
||||
|
||||
<!-- Operations queues -->
|
||||
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_channel_status">
|
||||
<column name="channel"/>
|
||||
<column name="status"/>
|
||||
</createIndex>
|
||||
|
||||
<!-- Reconciliation by switch handles -->
|
||||
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_utr">
|
||||
<column name="utr"/>
|
||||
</createIndex>
|
||||
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_switch_txn">
|
||||
<column name="switch_txn_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Append-only audit log of every state change on a m_fund_transfer row. Mandatory
|
||||
for fraud investigations and RBI reconciliation queries.
|
||||
-->
|
||||
<changeSet id="0305_create_m_fund_transfer_event" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_fund_transfer_event"/></not></preConditions>
|
||||
<createTable tableName="m_fund_transfer_event">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="fund_transfer_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="from_status" type="VARCHAR(24)"/>
|
||||
<column name="to_status" type="VARCHAR(24)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="event" type="VARCHAR(48)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payload_json" type="CLOB"/>
|
||||
<column name="created_at" type="DATETIME" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseTableName="m_fund_transfer_event" baseColumnNames="fund_transfer_id"
|
||||
constraintName="fk_fund_transfer_event_parent"
|
||||
referencedTableName="m_fund_transfer" referencedColumnNames="id"/>
|
||||
|
||||
<createIndex tableName="m_fund_transfer_event" indexName="idx_fund_transfer_event_parent_created">
|
||||
<column name="fund_transfer_id"/>
|
||||
<column name="created_at"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Short-lived token issued at /v1/transfers/verify (BANL step) and redeemed at
|
||||
/v1/transfers (actual debit). Carries the locked transfer params so the
|
||||
customer can't change account number / amount between confirming the masked
|
||||
name and the debit landing. Also doubles as the idempotency key — a token
|
||||
already in REDEEMED state returns the cached transfer ref.
|
||||
-->
|
||||
<changeSet id="0306_create_m_verification_token" author="fineract">
|
||||
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_verification_token"/></not></preConditions>
|
||||
<createTable tableName="m_verification_token">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="token" type="VARCHAR(64)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_verification_token"/>
|
||||
</column>
|
||||
|
||||
<!-- Locked params (cashier cannot vary these between verify and debit) -->
|
||||
<column name="channel" type="VARCHAR(16)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="direction" type="VARCHAR(8)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="cashier_user_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="client_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payer_account_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payee_account_number" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payee_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="payee_mask_name" type="VARCHAR(255)"/>
|
||||
<column name="amount" type="DECIMAL(19,4)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<!-- Lifecycle -->
|
||||
<column name="status" type="VARCHAR(16)" defaultValue="ISSUED">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="issued_at" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="expires_at" type="DATETIME">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="redeemed_at" type="DATETIME"/>
|
||||
<column name="redeemed_fund_transfer_id" type="BIGINT"/>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseTableName="m_verification_token" baseColumnNames="cashier_user_id"
|
||||
constraintName="fk_verification_token_cashier"
|
||||
referencedTableName="m_appuser" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_verification_token" baseColumnNames="client_id"
|
||||
constraintName="fk_verification_token_client"
|
||||
referencedTableName="m_client" referencedColumnNames="id"/>
|
||||
|
||||
<createIndex tableName="m_verification_token" indexName="idx_verification_token_expires">
|
||||
<column name="status"/>
|
||||
<column name="expires_at"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Permissions for fund transfer (FUNDTRANSFER entity) and its sibling admin
|
||||
entities (BANKIFSC, CUSTOMERTRANSFERLIMIT). can_maker_checker=true on the
|
||||
sensitive ones drives Fineract's command-pattern approval queue.
|
||||
-->
|
||||
<changeSet id="0307_seed_fund_transfer_permissions" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
|
||||
</preConditions>
|
||||
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings"/>
|
||||
<column name="code" value="CREATE_FUNDTRANSFER"/>
|
||||
<column name="entity_name" value="FUNDTRANSFER"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings"/>
|
||||
<column name="code" value="APPROVE_FUNDTRANSFER"/>
|
||||
<column name="entity_name" value="FUNDTRANSFER"/>
|
||||
<column name="action_name" value="APPROVE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings"/>
|
||||
<column name="code" value="REJECT_FUNDTRANSFER"/>
|
||||
<column name="entity_name" value="FUNDTRANSFER"/>
|
||||
<column name="action_name" value="REJECT"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings"/>
|
||||
<column name="code" value="READ_FUNDTRANSFER"/>
|
||||
<column name="entity_name" value="FUNDTRANSFER"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="CREATE_BANKIFSC"/>
|
||||
<column name="entity_name" value="BANKIFSC"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="UPDATE_BANKIFSC"/>
|
||||
<column name="entity_name" value="BANKIFSC"/>
|
||||
<column name="action_name" value="UPDATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="READ_BANKIFSC"/>
|
||||
<column name="entity_name" value="BANKIFSC"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="CREATE_CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="UPDATE_CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="action_name" value="UPDATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="true"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="configuration"/>
|
||||
<column name="code" value="READ_CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Bootstrap permission grants for the fund-transfer module.
|
||||
|
||||
Two role buckets:
|
||||
- 'Super user' gets EVERYTHING (initiate + read + approve + reject + admin tables).
|
||||
- Every role attached to 'bankadmin' gets the cashier subset (initiate + read).
|
||||
Matches the pattern used in 0291. Bank-side roles (Bank Cashier, Branch Manager)
|
||||
can be created in Manage Roles UI later; this migration unblocks the immediate
|
||||
"log in as bankadmin and use the new endpoints" path.
|
||||
|
||||
Idempotent via NOT EXISTS. No-op on installs missing the target users/roles.
|
||||
-->
|
||||
<changeSet id="0308_grant_fund_transfer_to_superuser" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Super user'
|
||||
AND p.code IN (
|
||||
'CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER',
|
||||
'APPROVE_FUNDTRANSFER', 'REJECT_FUNDTRANSFER',
|
||||
'CREATE_BANKIFSC', 'UPDATE_BANKIFSC', 'READ_BANKIFSC',
|
||||
'CREATE_CUSTOMERTRANSFERLIMIT', 'UPDATE_CUSTOMERTRANSFERLIMIT', 'READ_CUSTOMERTRANSFERLIMIT'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0308_grant_fund_transfer_to_bankadmin_roles" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_appuser WHERE username = 'bankadmin'</sqlCheck>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT ur.role_id, p.id
|
||||
FROM m_appuser u
|
||||
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||
CROSS JOIN m_permission p
|
||||
WHERE u.username = 'bankadmin'
|
||||
AND p.code IN ('CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER', 'READ_BANKIFSC', 'READ_CUSTOMERTRANSFERLIMIT')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||
)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- Phase 3 wiring: two Quartz jobs + RECALL_FUNDTRANSFER permission. -->
|
||||
|
||||
<changeSet author="fineract" id="0309_imps_deemed_reconcile_job">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'IMPS DEEMED Reconciliation'</sqlCheck>
|
||||
</preConditions>
|
||||
<!--
|
||||
Polls the IMPS switch (ReqChkTxn) for every DEEMED transfer in the [24h ago, 30s ago]
|
||||
window. Cron: every 30 seconds. Adjust via Manage Jobs in the UI if too aggressive.
|
||||
-->
|
||||
<insert tableName="job">
|
||||
<column name="name" value="IMPS DEEMED Reconciliation"/>
|
||||
<column name="short_name" value="IMPS_REC"/>
|
||||
<column name="display_name" value="IMPS DEEMED Reconciliation"/>
|
||||
<column name="cron_expression" value="0/30 * * * * ?"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="IMPS DEEMED ReconciliationDetail1 _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="false"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="0"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0309_neft_batch_process_job">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'NEFT Batch Process'</sqlCheck>
|
||||
</preConditions>
|
||||
<!--
|
||||
Processes QUEUED NEFT rows through the switch. Cron: top and bottom of every hour,
|
||||
matching the NEFT half-hourly settlement schedule.
|
||||
-->
|
||||
<insert tableName="job">
|
||||
<column name="name" value="NEFT Batch Process"/>
|
||||
<column name="short_name" value="NEFT_BAT"/>
|
||||
<column name="display_name" value="NEFT Batch Process"/>
|
||||
<column name="cron_expression" value="0 0,30 * * * ?"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="NEFT Batch ProcessDetail1 _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="false"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="0"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0309_seed_recall_fund_transfer_permission" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'RECALL_FUNDTRANSFER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings"/>
|
||||
<column name="code" value="RECALL_FUNDTRANSFER"/>
|
||||
<column name="entity_name" value="FUNDTRANSFER"/>
|
||||
<column name="action_name" value="RECALL"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0309_grant_recall_to_superuser" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'RECALL_FUNDTRANSFER'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Super user'
|
||||
AND p.code = 'RECALL_FUNDTRANSFER'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Creates two purpose-built roles for the fund-transfer module so admins don't have to
|
||||
wire permissions by hand in Manage Roles UI. Existing 0308 grants to Super user +
|
||||
bankadmin stay; these add the canonical pair.
|
||||
|
||||
Bank Cashier: initiates transfers, reads bank IFSC list, reads customer limit view
|
||||
Branch Manager: everything the cashier can do, plus approve / reject / recall, plus
|
||||
create / update / read per-customer limit overrides
|
||||
-->
|
||||
|
||||
<changeSet id="0310_create_bank_cashier_role" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_role WHERE name = 'Bank Cashier'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_role">
|
||||
<column name="name" value="Bank Cashier"/>
|
||||
<column name="description" value="Teller-side fund-transfer role. Initiate IMPS / NEFT / RTGS / INTERNAL transfers via /v1/transfers. No approval / recall rights."/>
|
||||
<column name="is_disabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0310_grant_bank_cashier_permissions" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Bank Cashier'</sqlCheck>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Bank Cashier'
|
||||
AND p.code IN ('CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER', 'READ_BANKIFSC', 'READ_CUSTOMERTRANSFERLIMIT')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0310_create_branch_manager_role" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_role WHERE name = 'Branch Manager'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_role">
|
||||
<column name="name" value="Branch Manager"/>
|
||||
<column name="description" value="Approves / rejects / recalls fund transfers above maker-checker threshold. Can also create and update per-customer limit overrides. Includes all Bank Cashier rights."/>
|
||||
<column name="is_disabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0310_grant_branch_manager_permissions" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Branch Manager'</sqlCheck>
|
||||
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FUNDTRANSFER'</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Branch Manager'
|
||||
AND p.code IN (
|
||||
'CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER',
|
||||
'APPROVE_FUNDTRANSFER', 'REJECT_FUNDTRANSFER', 'RECALL_FUNDTRANSFER',
|
||||
'READ_BANKIFSC',
|
||||
'CREATE_CUSTOMERTRANSFERLIMIT', 'UPDATE_CUSTOMERTRANSFERLIMIT', 'READ_CUSTOMERTRANSFERLIMIT'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<!--
|
||||
Register the verification-token cleanup Quartz job. Inactive by default; ops should
|
||||
flip is_active to true once they're happy with the retention window
|
||||
(fineract.transfer.verification-token.retention-hours, default 24h).
|
||||
Cron: every 15 minutes — light query, no concurrency concerns.
|
||||
-->
|
||||
<changeSet author="fineract" id="0310_verification_token_cleanup_job">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'Verification Token Cleanup'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="job">
|
||||
<column name="name" value="Verification Token Cleanup"/>
|
||||
<column name="short_name" value="VTOK_CLN"/>
|
||||
<column name="display_name" value="Verification Token Cleanup"/>
|
||||
<column name="cron_expression" value="0 0/15 * * * ?"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="Verification Token CleanupDetail1 _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="false"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="0"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user