Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
This commit is contained in:
@@ -1,975 +0,0 @@
|
||||
> 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
|
||||
@@ -1,18 +0,0 @@
|
||||
> 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 - d7821c7824067a96d31c4c031ea538fc5e4107fc
|
||||
ref configuration: COMMIT - pattern: null
|
||||
version: ${describe.tag.version.major}.${describe.tag.version.minor.next}.0-SNAPSHOT
|
||||
describeTagPattern: .*(\d+\.\d+\.\d+).*
|
||||
describeTagFirstParent: false
|
||||
|
||||
project version: 0.1.0-SNAPSHOT
|
||||
Binary file not shown.
+1
-1
@@ -75,7 +75,7 @@ import org.springframework.stereotype.Component;
|
||||
@Path("/v1/journalentries")
|
||||
@Component
|
||||
@Tag(name = "Journal Entries", description = """
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"\s
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"
|
||||
|
||||
A journal entry directly changes the account balances on the general ledger""")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.email.service;
|
||||
import java.time.Year;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
@@ -129,7 +130,7 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
+ escape(supportEmail) + "</a>"
|
||||
+ " </p>"
|
||||
+ " <p style=\"margin:0;font-size:12px;color:#94a3b8;\">"
|
||||
+ " © " + Year.now().getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " © " + Year.now(DateUtils.getDateTimeZoneOfTenant()).getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " </p>"
|
||||
+ " </td></tr>"
|
||||
+ " </table>"
|
||||
|
||||
+1
@@ -109,6 +109,7 @@ public class ImpsCryptoUtil {
|
||||
}
|
||||
|
||||
/** Decrypt inbound payload using the CBS private key. */
|
||||
@SuppressWarnings("StringSplitter")
|
||||
public String decryptJson(String encryptedPayload) {
|
||||
if (cbsPrivateKey == null) {
|
||||
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* STUB implementation — returns hard-coded SUCCESS for every request and
|
||||
@@ -108,7 +109,7 @@ public class ImpsCbsServiceImpl implements ImpsCbsService {
|
||||
resp.setRefId(request.getRefId());
|
||||
resp.setUtr(request.getUtr());
|
||||
resp.setAmount(request.getAmount());
|
||||
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET));
|
||||
resp.setTimeStamp(OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(ISO_OFFSET));
|
||||
resp.setStatus("SUCCESS");
|
||||
resp.setResponseCode("00");
|
||||
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
|
||||
|
||||
+2
-2
@@ -28,7 +28,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
@@ -42,6 +41,7 @@ import org.apache.fineract.infrastructure.openproject.data.OpenProjectProjectDat
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectStatusSummaryData;
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -164,7 +164,7 @@ public class OpenProjectService {
|
||||
}
|
||||
|
||||
// Set required custom field: "Last Follow-up Date" (customField8)
|
||||
body.addProperty("customField8", LocalDate.now().toString());
|
||||
body.addProperty("customField8", LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
|
||||
if (typeName != null && !typeName.isBlank()) {
|
||||
JsonObject links = new JsonObject();
|
||||
|
||||
+2
-1
@@ -47,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -127,7 +128,7 @@ public class RbiFeedFetchService {
|
||||
NodeList items = doc.getElementsByTagName("item");
|
||||
log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category);
|
||||
List<RbiFeedItem> newItems = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
for (int i = 0; i < items.getLength(); i++) {
|
||||
Element item = (Element) items.item(i);
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class EmailLoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class LoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+5
-16
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLog;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLogRepository;
|
||||
import org.apache.fineract.infrastructure.security.otp.util.OtpDestinationMasker;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||
import org.apache.fineract.useradministration.mfa.domain.MfaFactorType;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
@@ -127,17 +129,17 @@ public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
|
||||
// Render the branded template — same look and feel for OTP, welcome, password-reset, alerts.
|
||||
final String html = emailTemplateService.renderOtp(recipientName, otp, validityMinutes);
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now().format(SUBJECT_TIMESTAMP) + ")";
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(SUBJECT_TIMESTAMP) + ")";
|
||||
|
||||
final boolean sent = emailSenderService.sendHtml(email, subject, html);
|
||||
if (sent) {
|
||||
log.setStatus(STATUS_SENT);
|
||||
final OtpLog saved = otpLogRepository.saveAndFlush(log);
|
||||
LOG.info("Email login OTP sent. otpId={}, user={}, email={}", saved.getId(), username, mask(email));
|
||||
LOG.info("Email login OTP sent. otpId={}, user={}, email={}", saved.getId(), username, OtpDestinationMasker.maskEmail(email));
|
||||
return OtpGenerateResponse.builder()
|
||||
.success(true)
|
||||
.otpId(saved.getId())
|
||||
.maskedMobile(mask(email)) // reusing the field; UI just shows where it was sent
|
||||
.maskedMobile(OtpDestinationMasker.maskEmail(email)) // reusing the field; UI just shows where it was sent
|
||||
.expiresInSeconds(ttlSeconds)
|
||||
.message("OTP sent to your registered email")
|
||||
.build();
|
||||
@@ -240,19 +242,6 @@ public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
}
|
||||
}
|
||||
|
||||
private String mask(final String email) {
|
||||
if (email == null || !email.contains("@")) {
|
||||
return "****";
|
||||
}
|
||||
final int at = email.indexOf('@');
|
||||
final String local = email.substring(0, at);
|
||||
final String domain = email.substring(at);
|
||||
if (local.length() <= 2) {
|
||||
return "*" + local.substring(local.length() - 1) + domain;
|
||||
}
|
||||
return local.charAt(0) + "***" + local.charAt(local.length() - 1) + domain;
|
||||
}
|
||||
|
||||
private String displayName(final AppUser user) {
|
||||
if (user.getFirstname() != null && !user.getFirstname().isBlank()) {
|
||||
return user.getFirstname();
|
||||
|
||||
+4
-10
@@ -31,6 +31,7 @@ import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLog;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLogRepository;
|
||||
import org.apache.fineract.infrastructure.security.otp.util.OtpDestinationMasker;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||
import org.apache.fineract.useradministration.mfa.domain.MfaFactorType;
|
||||
@@ -156,12 +157,12 @@ public class LoginOtpServiceImpl implements LoginOtpService {
|
||||
log.setOtpReference(extractField(response, "reference_id", "refNumber", "ref_number",
|
||||
"transactionId", "txnId", "requestId"));
|
||||
final OtpLog saved = otpLogRepository.saveAndFlush(log);
|
||||
LOG.info("LincPay login OTP sent. otpId={}, mobile={}", saved.getId(), mask(mobileNumber));
|
||||
LOG.info("LincPay login OTP sent. otpId={}, mobile={}", saved.getId(), OtpDestinationMasker.maskMobile(mobileNumber));
|
||||
return OtpGenerateResponse.builder()
|
||||
.success(true)
|
||||
.otpId(saved.getId())
|
||||
.otpReference(saved.getOtpReference())
|
||||
.maskedMobile(mask(mobileNumber))
|
||||
.maskedMobile(OtpDestinationMasker.maskMobile(mobileNumber))
|
||||
.expiresInSeconds(ttlSeconds)
|
||||
.message("OTP sent")
|
||||
.build();
|
||||
@@ -239,7 +240,7 @@ public class LoginOtpServiceImpl implements LoginOtpService {
|
||||
row.setVerifiedAt(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
autoPromoteFactor(row);
|
||||
LOG.info("LincPay login OTP verified. otpId={}, mobile={}", row.getId(), mask(row.getMobileNumber()));
|
||||
LOG.info("LincPay login OTP verified. otpId={}, mobile={}", row.getId(), OtpDestinationMasker.maskMobile(row.getMobileNumber()));
|
||||
return OtpVerifyResponse.builder().verified(true).message("OTP verified").build();
|
||||
}
|
||||
|
||||
@@ -364,13 +365,6 @@ public class LoginOtpServiceImpl implements LoginOtpService {
|
||||
}
|
||||
}
|
||||
|
||||
private String mask(final String mobile) {
|
||||
if (mobile == null || mobile.length() < 4) {
|
||||
return "****";
|
||||
}
|
||||
return "******" + mobile.substring(mobile.length() - 4);
|
||||
}
|
||||
|
||||
private String truncate(final String s, final int max) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
|
||||
-2
@@ -20,7 +20,6 @@ package org.apache.fineract.organisation.teller.service;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
@@ -44,7 +43,6 @@ import org.apache.fineract.organisation.teller.domain.CashierTransactionReposito
|
||||
import org.apache.fineract.organisation.teller.domain.CashierTxnType;
|
||||
import org.apache.fineract.organisation.teller.domain.Teller;
|
||||
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
|
||||
import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
|
||||
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
|
||||
|
||||
+7
-4
@@ -60,6 +60,8 @@ public interface ShareAccountApiConstants {
|
||||
|
||||
String externalid_paramname = "externalId";
|
||||
|
||||
String resolutionnumber_paramname = "resolutionNumber";
|
||||
|
||||
String currency_paramname = "currencyCode";
|
||||
|
||||
String digitsafterdecimal_paramname = "digitsAfterDecimal";
|
||||
@@ -102,9 +104,10 @@ public interface ShareAccountApiConstants {
|
||||
|
||||
Set<String> supportedParameters = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(locale_paramname, dateformat_paramname,
|
||||
id_paramname, clientid_paramname, productid_paramname, submitteddate_paramname, approveddate_paramname, externalid_paramname,
|
||||
currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname, requestedshares_paramname,
|
||||
savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname, minimumactiveperiod_paramname,
|
||||
minimumactiveperiodfrequencytype_paramname, allowdividendcalculationforinactiveclients_paramname, charges_paramname,
|
||||
applicationdate_param, purchaseddate_paramname, numberofshares_paramname, purchasedprice_paramname)));
|
||||
resolutionnumber_paramname, currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname,
|
||||
requestedshares_paramname, savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname,
|
||||
minimumactiveperiod_paramname, minimumactiveperiodfrequencytype_paramname,
|
||||
allowdividendcalculationforinactiveclients_paramname, charges_paramname, applicationdate_param, purchaseddate_paramname,
|
||||
numberofshares_paramname, purchasedprice_paramname)));
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ public class AccountRequest implements Serializable {
|
||||
private Long numberOfShares;
|
||||
private String allowDividendCalculationForInactiveClients;
|
||||
private String externalId;
|
||||
private String resolutionNumber;
|
||||
private String minimumActivePeriodFrequencyType;
|
||||
private Long savingsAccountId;
|
||||
private String locale;
|
||||
|
||||
+38
@@ -40,6 +40,7 @@ import org.apache.fineract.portfolio.client.domain.ClientAddress;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientAddressRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientAddressRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.service.ClientChangeNotificationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -53,6 +54,7 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
|
||||
private final AddressRepository addressRepository;
|
||||
private final ClientAddressRepositoryWrapper clientAddressRepositoryWrapper;
|
||||
private final AddressCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final ClientChangeNotificationService clientChangeNotificationService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult addClientAddress(final Long clientId, final Long addressTypeId, final JsonCommand command) {
|
||||
@@ -170,6 +172,10 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
|
||||
|
||||
final Address addobj = this.addressRepository.getReferenceById(addressId);
|
||||
|
||||
// Snapshot before mutation — notification-only (no OTP gate for address; see class Javadoc /
|
||||
// feature plan). Compared against the post-update state below to build the change message.
|
||||
final String oldAddressSummary = formatAddressSummary(addobj);
|
||||
|
||||
if (!command.stringValueOfParameterNamed("addressLine1").isEmpty()) {
|
||||
|
||||
is_address_update = true;
|
||||
@@ -271,6 +277,38 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
|
||||
}
|
||||
this.clientAddressRepository.saveAndFlush(clientAddressObj);
|
||||
|
||||
if (is_address_update) {
|
||||
final String newAddressSummary = formatAddressSummary(addobj);
|
||||
final Client client = clientAddressObj.getClient();
|
||||
this.clientChangeNotificationService.notifyContactOrAddressChanged(client, ClientChangeNotificationService.FIELD_LABEL_ADDRESS,
|
||||
oldAddressSummary, newAddressSummary, client.getEmailAddress());
|
||||
}
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddressObj.getId()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable, comma-joined summary of the address's free-text fields — used only for the
|
||||
* change-notification email body (see {@link ClientChangeNotificationService}), not persisted.
|
||||
*/
|
||||
private String formatAddressSummary(final Address address) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
appendIfPresent(sb, address.getAddressLine1());
|
||||
appendIfPresent(sb, address.getAddressLine2());
|
||||
appendIfPresent(sb, address.getAddressLine3());
|
||||
appendIfPresent(sb, address.getTownVillage());
|
||||
appendIfPresent(sb, address.getCity());
|
||||
appendIfPresent(sb, address.getCountyDistrict());
|
||||
appendIfPresent(sb, address.getPostalCode());
|
||||
return sb.length() == 0 ? "(not set)" : sb.toString();
|
||||
}
|
||||
|
||||
private void appendIfPresent(final StringBuilder sb, final String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -33,6 +33,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashexchange")
|
||||
@Component
|
||||
@@ -94,7 +95,7 @@ public class CashExchangeApiResource {
|
||||
header.setNote(note);
|
||||
header.setCustomerIdProof(customerIdProof);
|
||||
header.setIsReversed(false);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) {
|
||||
@@ -107,7 +108,9 @@ public class CashExchangeApiResource {
|
||||
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||
Long.class, user.getId());
|
||||
header.setCashierId(cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// resolving the current user must never block recording the exchange
|
||||
@@ -160,14 +163,18 @@ public class CashExchangeApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
exchange.setIsReversed(true);
|
||||
exchange.setReversedAt(LocalDateTime.now());
|
||||
exchange.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
exchange.setReverseReason(reason);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) exchange.setReversedBy(user.getId());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
repository.save(exchange);
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("resourceId", id);
|
||||
|
||||
+49
-123
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
@Path("/v1/cashier/deposit-requests")
|
||||
@Component
|
||||
@@ -101,6 +104,13 @@ public class CashierDepositRequestApiResource {
|
||||
final String bankName = fromJsonHelper.extractStringNamed("bankName", root);
|
||||
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
|
||||
// DD + Cash + GL breakdown from Transaction Entry - null for ordinary deposit requests.
|
||||
// Persisted so the Sponsor DD issuance screen can read the breakdown back instead of
|
||||
// retyping face value/charges once this request is approved and selected as cash evidence.
|
||||
final BigDecimal ddFaceValue = parseBigDecimal(fromJsonHelper.extractStringNamed("ddFaceValue", root));
|
||||
final BigDecimal ddCharges = parseBigDecimal(fromJsonHelper.extractStringNamed("ddCharges", root));
|
||||
final BigDecimal ddGstPercentage = parseBigDecimal(fromJsonHelper.extractStringNamed("ddGstPercentage", root));
|
||||
final BigDecimal ddGstAmount = parseBigDecimal(fromJsonHelper.extractStringNamed("ddGstAmount", root));
|
||||
|
||||
if (accountNo == null || accountNo.isBlank()) {
|
||||
throw apiError(400, "Account number is required");
|
||||
@@ -115,6 +125,7 @@ public class CashierDepositRequestApiResource {
|
||||
throw apiError(400, "Amount must be greater than zero");
|
||||
}
|
||||
validateDenominationTotal(denominationData, amount);
|
||||
validateDdLimit(ddFaceValue);
|
||||
|
||||
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
|
||||
|
||||
@@ -123,8 +134,9 @@ public class CashierDepositRequestApiResource {
|
||||
(account_no, account_type, savings_account_id, gl_account_id, contra_gl_id, office_id, currency_code,
|
||||
client_id, client_name, amount,
|
||||
payment_type_id, payment_type_name, description, cheque_no, bank_name, txn_date,
|
||||
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at,
|
||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
RETURNING id
|
||||
""",
|
||||
Long.class,
|
||||
@@ -135,7 +147,8 @@ public class CashierDepositRequestApiResource {
|
||||
txnDate != null ? Date.valueOf(txnDate) : null,
|
||||
"PENDING",
|
||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||
denominationData, activityLog, Timestamp.from(Instant.now())
|
||||
denominationData, activityLog, Timestamp.from(Instant.now()),
|
||||
ddFaceValue, ddCharges, ddGstPercentage, ddGstAmount
|
||||
);
|
||||
|
||||
// Globally unique voucher number from the central m_voucher register
|
||||
@@ -189,7 +202,8 @@ public class CashierDepositRequestApiResource {
|
||||
+ " amount, payment_type_id, payment_type_name, description, cheque_no, bank_name,"
|
||||
+ " txn_date, status, cashier_id, cashier_name, cashier_username,"
|
||||
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
||||
+ " denomination_data, activity_log, submitted_at, actioned_at"
|
||||
+ " denomination_data, activity_log, submitted_at, actioned_at,"
|
||||
+ " dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount"
|
||||
+ " FROM m_cashier_deposit_request " + where
|
||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||
pageParams.toArray());
|
||||
@@ -242,7 +256,8 @@ public class CashierDepositRequestApiResource {
|
||||
SELECT id, voucher_no, account_no, account_type, client_name, amount,
|
||||
payment_type_name, description, txn_date, status,
|
||||
cashier_name, manager_name, manager_username, rejection_reason, txn_id,
|
||||
submitted_at, actioned_at
|
||||
submitted_at, actioned_at,
|
||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount
|
||||
FROM m_cashier_deposit_request
|
||||
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||
pageParams.toArray()
|
||||
@@ -352,7 +367,7 @@ public class CashierDepositRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
final String storedAccountType = (String) record.get("account_type");
|
||||
@@ -424,6 +439,7 @@ public class CashierDepositRequestApiResource {
|
||||
final JsonObject res = new JsonObject();
|
||||
res.addProperty("resourceId", id);
|
||||
res.addProperty("status", "APPROVED");
|
||||
res.addProperty("voucherNo", Optional.ofNullable(record.get("voucher_no")).map(Object::toString).orElse(""));
|
||||
if (txnId != null) res.addProperty("txnId", txnId);
|
||||
return gson.toJson(res);
|
||||
}
|
||||
@@ -500,7 +516,7 @@ public class CashierDepositRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
if (savingsAccountId == null || !isCurrentAccountById(savingsAccountId)) {
|
||||
@@ -532,7 +548,8 @@ public class CashierDepositRequestApiResource {
|
||||
amount, payment_type_id, payment_type_name, description, cheque_no, bank_name,
|
||||
txn_date, status, cashier_id, cashier_name, cashier_username,
|
||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||
denomination_data, activity_log, submitted_at, actioned_at
|
||||
denomination_data, activity_log, submitted_at, actioned_at,
|
||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount
|
||||
FROM m_cashier_deposit_request WHERE id = ?
|
||||
""", id);
|
||||
if (rows.isEmpty()) {
|
||||
@@ -545,7 +562,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -573,7 +590,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -630,7 +647,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Object paymentTypeId, final String txnDateStr, final String note,
|
||||
final boolean isDeposit) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
// Deposit (CR): cashier receives cash → credit selected GL, debit cash vault (contra)
|
||||
@@ -672,67 +689,6 @@ public class CashierDepositRequestApiResource {
|
||||
return result.getTransactionId() != null ? result.getTransactionId() : null;
|
||||
}
|
||||
|
||||
private void saveGlDenomination(final String denominationData,
|
||||
final Map<?, ?> record, final String txnIdRaw) {
|
||||
final JsonObject denomMap;
|
||||
try {
|
||||
denomMap = gson.fromJson(denominationData, JsonObject.class);
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
if (denomMap == null || denomMap.size() == 0) return;
|
||||
|
||||
// Convert hex transaction ID to Long (same logic as CashierGlDenominationApiResource)
|
||||
Long txnId = null;
|
||||
if (txnIdRaw != null && !txnIdRaw.isBlank()) {
|
||||
try {
|
||||
txnId = Long.valueOf(txnIdRaw);
|
||||
} catch (NumberFormatException ignored) {
|
||||
try {
|
||||
final String hex = txnIdRaw.length() > 15 ? txnIdRaw.substring(txnIdRaw.length() - 15) : txnIdRaw;
|
||||
txnId = Long.parseUnsignedLong(hex, 16);
|
||||
} catch (NumberFormatException ignored2) {
|
||||
txnId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String accountNo = (String) record.get("account_no");
|
||||
final Object cashierIdObj = record.get("cashier_id");
|
||||
final Long cashierId = cashierIdObj != null ? Long.valueOf(cashierIdObj.toString()) : null;
|
||||
final String cashierUsername = (String) record.get("cashier_username");
|
||||
|
||||
for (final Map.Entry<String, com.google.gson.JsonElement> entry : denomMap.entrySet()) {
|
||||
final int count;
|
||||
try { count = entry.getValue().getAsInt(); } catch (Exception e) { continue; }
|
||||
if (count <= 0) continue;
|
||||
|
||||
final String[] parts = entry.getKey().split(":", 2);
|
||||
if (parts.length != 2) continue;
|
||||
final String kind = parts[0];
|
||||
final int value;
|
||||
try { value = Integer.parseInt(parts[1]); } catch (Exception e) { continue; }
|
||||
|
||||
final List<Long> ids = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_denomination_master WHERE denomination = ? AND kind = ?"
|
||||
+ " AND (deleted IS NULL OR deleted = false) LIMIT 1",
|
||||
Long.class, value, kind);
|
||||
if (ids.isEmpty()) continue;
|
||||
final Long denomId = ids.get(0);
|
||||
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_savings_txn_denomination
|
||||
(txn_id, savings_account_id, account_no, client_id, denomination_id,
|
||||
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username)
|
||||
VALUES (?, NULL, ?, NULL, ?, ?, ?, ?, CURRENT_DATE, 'DEPOSIT', ?, ?)
|
||||
""",
|
||||
txnId, accountNo, denomId,
|
||||
value, count, BigDecimal.valueOf((long) value * count),
|
||||
cashierId, cashierUsername
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the denomination breakdown the cashier entered at submit time into
|
||||
* m_savings_txn_denomination once the deposit is approved, so the cashier's
|
||||
@@ -783,56 +739,6 @@ public class CashierDepositRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDepositDenomination(final String denominationData,
|
||||
final Map<?, ?> record, final Long txnId) {
|
||||
final JsonObject denomMap;
|
||||
try {
|
||||
denomMap = gson.fromJson(denominationData, JsonObject.class);
|
||||
} catch (Exception e) {
|
||||
return; // malformed JSON — skip silently
|
||||
}
|
||||
if (denomMap == null || denomMap.size() == 0) return;
|
||||
|
||||
final Object saObj = record.get("savings_account_id");
|
||||
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
|
||||
final String accountNo = (String) record.get("account_no");
|
||||
final Object clientIdObj = record.get("client_id");
|
||||
final Long clientId = clientIdObj != null ? Long.valueOf(clientIdObj.toString()) : null;
|
||||
final Object cashierIdObj = record.get("cashier_id");
|
||||
final Long cashierId = cashierIdObj != null ? Long.valueOf(cashierIdObj.toString()) : null;
|
||||
final String cashierUsername = (String) record.get("cashier_username");
|
||||
|
||||
for (final Map.Entry<String, JsonElement> entry : denomMap.entrySet()) {
|
||||
final int count;
|
||||
try { count = entry.getValue().getAsInt(); } catch (Exception e) { continue; }
|
||||
if (count <= 0) continue;
|
||||
|
||||
final String[] parts = entry.getKey().split(":", 2); // "note:500" or "coin:1"
|
||||
if (parts.length != 2) continue;
|
||||
final String kind = parts[0];
|
||||
final int value;
|
||||
try { value = Integer.parseInt(parts[1]); } catch (Exception e) { continue; }
|
||||
|
||||
final List<Long> ids = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_denomination_master WHERE denomination = ? AND kind = ?"
|
||||
+ " AND (deleted IS NULL OR deleted = false) LIMIT 1",
|
||||
Long.class, value, kind);
|
||||
if (ids.isEmpty()) continue;
|
||||
final Long denomId = ids.get(0);
|
||||
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_savings_txn_denomination
|
||||
(txn_id, savings_account_id, account_no, client_id, denomination_id,
|
||||
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, 'DEPOSIT', ?, ?)
|
||||
""",
|
||||
txnId, savingsAccountId, accountNo, clientId, denomId,
|
||||
value, count, BigDecimal.valueOf((long) value * count),
|
||||
cashierId, cashierUsername
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cash denomination breakdown is provided, its total must exactly equal
|
||||
* the deposit amount — prevents the recorded vault movement from drifting
|
||||
@@ -869,6 +775,26 @@ public class CashierDepositRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DD + Cash + GL deposit requests carry a Face Value (ddFaceValue) - reject the cash intake
|
||||
* itself, before the cashier even accepts the money, if it exceeds the admin-configured DD
|
||||
* limit (m_sponsor_dd_gl_mapping.dd_limit_amount). The same limit is enforced again later at
|
||||
* actual DD issuance (sponsordd module); this is a first line of defense so an over-limit
|
||||
* amount never even gets accepted as cash. ddFaceValue is null for ordinary (non-DD) deposit
|
||||
* requests, which this check skips entirely. Null/zero-or-negative limit means unconfigured.
|
||||
*/
|
||||
private void validateDdLimit(final BigDecimal ddFaceValue) {
|
||||
if (ddFaceValue == null) return;
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT dd_limit_amount FROM m_sponsor_dd_gl_mapping LIMIT 1");
|
||||
if (rows.isEmpty()) return;
|
||||
final BigDecimal ddLimitAmount = (BigDecimal) rows.get(0).get("dd_limit_amount");
|
||||
if (ddLimitAmount == null || ddLimitAmount.compareTo(BigDecimal.ZERO) <= 0) return;
|
||||
if (ddFaceValue.compareTo(ddLimitAmount) > 0) {
|
||||
throw apiError(400, "DD Face Value ₹" + ddFaceValue + " exceeds the configured DD limit of ₹" + ddLimitAmount + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checker authorization: only manager-level users may approve or reject.
|
||||
* Allowed: super users (ALL_FUNCTIONS), or any role whose name contains
|
||||
@@ -912,7 +838,7 @@ public class CashierDepositRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-6
@@ -31,6 +31,7 @@ import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRep
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashintercashier")
|
||||
@Component
|
||||
@@ -69,7 +70,7 @@ public class CashInterCashierApiResource {
|
||||
}
|
||||
|
||||
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||
? json.get("fromParty").getAsString() : null;
|
||||
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||
@@ -95,13 +96,15 @@ public class CashInterCashierApiResource {
|
||||
header.setToParty(toParty);
|
||||
header.setTransferAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
header.setIsReversed(false);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
CashInterCashier saved = repository.saveAndFlush(header);
|
||||
saveDetails(saved.getId(), denominations);
|
||||
|
||||
@@ -128,13 +131,17 @@ public class CashInterCashierApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
AppUser user = null;
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
record.setIsReversed(true);
|
||||
record.setReversedAt(LocalDateTime.now());
|
||||
record.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
record.setReverseReason(reason);
|
||||
if (user != null) record.setReversedBy(user.getId());
|
||||
repository.save(record);
|
||||
|
||||
+9
-6
@@ -32,6 +32,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashreconciliation")
|
||||
@Component
|
||||
@@ -62,7 +63,7 @@ public class CashReconciliationApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// In: vault → cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
|
||||
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
|
||||
@@ -187,7 +188,7 @@ public class CashReconciliationApiResource {
|
||||
@Path("expected/me")
|
||||
public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) {
|
||||
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Long appUserId;
|
||||
try {
|
||||
appUserId = context.authenticatedUser().getId();
|
||||
@@ -223,7 +224,7 @@ public class CashReconciliationApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
|
||||
Integer.class, cashierId, reconDate);
|
||||
@@ -247,7 +248,7 @@ public class CashReconciliationApiResource {
|
||||
"cashierId is required.");
|
||||
}
|
||||
final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull()
|
||||
? json.get("cashierName").getAsString() : null;
|
||||
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
|
||||
@@ -299,12 +300,14 @@ public class CashReconciliationApiResource {
|
||||
header.setVariance(variance);
|
||||
header.setStatus(status);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
CashReconciliation saved = repository.saveAndFlush(header);
|
||||
|
||||
for (CashReconciliationDetail d : details) {
|
||||
|
||||
+19
-10
@@ -18,6 +18,7 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
|
||||
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashreports")
|
||||
@Component
|
||||
@@ -34,7 +35,7 @@ public class CashReportApiResource {
|
||||
@GET
|
||||
@Path("closing")
|
||||
public String closing(@QueryParam("date") String date) {
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// Single query — conditional aggregation for deposit, withdrawal, net per denomination
|
||||
final String sql = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
|
||||
@@ -238,7 +239,7 @@ public class CashReportApiResource {
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
final String today = LocalDate.now().toString();
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// Find the earliest transfer date before today for which no settlement exists on or after that date
|
||||
final String sql = "SELECT MIN(ct.transfer_date) AS earliest_unsettled "
|
||||
@@ -256,7 +257,9 @@ public class CashReportApiResource {
|
||||
java.sql.Date earliestDate = null;
|
||||
try {
|
||||
earliestDate = jdbcTemplate.queryForObject(sql, java.sql.Date.class, cashierId, today, cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
final boolean unsettled = earliestDate != null;
|
||||
final String unsettledSince = earliestDate != null ? earliestDate.toString() : null;
|
||||
@@ -265,7 +268,9 @@ public class CashReportApiResource {
|
||||
if (unsettled) {
|
||||
try {
|
||||
pendingAmount = denominationBreakdownWriter.getCashierTotalCash(cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether today's morning cash transfer has been received
|
||||
@@ -276,7 +281,9 @@ public class CashReportApiResource {
|
||||
Integer todayTransferCount = 0;
|
||||
try {
|
||||
todayTransferCount = jdbcTemplate.queryForObject(morningTransferSql, Integer.class, cashierId, today);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
final boolean morningTransferReceived = todayTransferCount != null && todayTransferCount > 0;
|
||||
|
||||
result.put("cashierId", cashierId);
|
||||
@@ -295,7 +302,7 @@ public class CashReportApiResource {
|
||||
@Path("cashier-position")
|
||||
public String cashierPosition(@QueryParam("cashierId") Long cashierId,
|
||||
@QueryParam("asOfDate") String asOfDate) {
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// CASHIER role: always scope to their own position only
|
||||
final Long scopedId = cashierRoleHelper.getCashierIdForFiltering();
|
||||
@@ -326,7 +333,7 @@ public class CashReportApiResource {
|
||||
@GET
|
||||
@Path("cashier-position/me")
|
||||
public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) {
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Long appUserId;
|
||||
try {
|
||||
appUserId = context.authenticatedUser().getId();
|
||||
@@ -362,7 +369,7 @@ public class CashReportApiResource {
|
||||
@Path("cashier-morning")
|
||||
public String cashierMorning(@QueryParam("cashierId") Long cashierId,
|
||||
@QueryParam("date") String date) {
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
if (cashierId == null) {
|
||||
Map<String, Object> empty = new LinkedHashMap<>();
|
||||
empty.put("cashierId", null);
|
||||
@@ -515,7 +522,9 @@ public class CashReportApiResource {
|
||||
cashierName = jdbcTemplate.queryForObject(
|
||||
"SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?",
|
||||
String.class, cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("cashierId", cashierId);
|
||||
@@ -562,7 +571,7 @@ public class CashReportApiResource {
|
||||
@QueryParam("offset") Integer offset,
|
||||
@QueryParam("limit") Integer limit) {
|
||||
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
final int resolvedLimit = (limit != null && limit > 0) ? limit : 20;
|
||||
final int resolvedOffset = (offset != null && offset >= 0) ? offset : 0;
|
||||
|
||||
|
||||
+24
-11
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashsettlement")
|
||||
@Component
|
||||
@@ -70,7 +71,7 @@ public class CashSettlementApiResource {
|
||||
}
|
||||
|
||||
final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||
? json.get("fromParty").getAsString() : null;
|
||||
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||
@@ -116,13 +117,15 @@ public class CashSettlementApiResource {
|
||||
header.setToParty(toParty);
|
||||
header.setSettlementAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
header.setIsReversed(false);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
// Commit header + details first, then GL dispatch separately to avoid
|
||||
// EclipseLink flushing pending detail rows inside Fineract's GL transaction.
|
||||
@@ -142,7 +145,9 @@ public class CashSettlementApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
|
||||
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
note != null ? note : "");
|
||||
@@ -174,13 +179,17 @@ public class CashSettlementApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
AppUser user = null;
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
settlement.setIsReversed(true);
|
||||
settlement.setReversedAt(LocalDateTime.now());
|
||||
settlement.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
settlement.setReverseReason(reason);
|
||||
if (user != null) settlement.setReversedBy(user.getId());
|
||||
repository.save(settlement);
|
||||
@@ -192,12 +201,16 @@ public class CashSettlementApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.allocateCashToCashier(glTellerId,
|
||||
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR",
|
||||
LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
"Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
@@ -214,7 +227,7 @@ public class CashSettlementApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
|
||||
Integer.class, cashierId, settlementDate);
|
||||
|
||||
+20
-9
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashtransfer")
|
||||
@Component
|
||||
@@ -105,7 +106,7 @@ public class CashTransferApiResource {
|
||||
header.setToCashierId(toCashierId);
|
||||
header.setTransferAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) {
|
||||
@@ -136,8 +137,10 @@ public class CashTransferApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now();
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
|
||||
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
note != null ? note : "");
|
||||
@@ -169,15 +172,19 @@ public class CashTransferApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
transfer.setReversedBy(user.getId());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
transfer.setIsReversed(true);
|
||||
transfer.setReversedAt(java.time.LocalDateTime.now());
|
||||
transfer.setReversedAt(java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
transfer.setReverseReason(reason);
|
||||
repository.save(transfer);
|
||||
|
||||
@@ -188,12 +195,16 @@ public class CashTransferApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.settleCashFromCashier(glTellerId,
|
||||
transfer.getToCashierId(), transfer.getTransferAmount(), "INR",
|
||||
java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
"Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
|
||||
+6
-3
@@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomin
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Records cashier denomination breakdown for GL account transactions (journal entries).
|
||||
@@ -90,7 +91,7 @@ public class CashierGlDenominationApiResource {
|
||||
final String txnDateStr = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||
final LocalDate txnDate = (txnDateStr != null && !txnDateStr.isBlank())
|
||||
? LocalDate.parse(txnDateStr)
|
||||
: LocalDate.now();
|
||||
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
if (!fromJsonHelper.parameterExists("denominationBreakdown", root)) {
|
||||
return "{\"result\":\"skipped\"}";
|
||||
@@ -109,7 +110,9 @@ public class CashierGlDenominationApiResource {
|
||||
officerId = user.getId();
|
||||
officerUsername = user.getUsername();
|
||||
}
|
||||
} catch (final Exception ignored) {}
|
||||
} catch (final Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
final String resolvedTxnType = (txnType != null && !txnType.isBlank())
|
||||
? txnType.toUpperCase()
|
||||
@@ -144,7 +147,7 @@ public class CashierGlDenominationApiResource {
|
||||
.count(count)
|
||||
.totalAmount(lineTotal)
|
||||
.txnDate(txnDate)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.txnType(resolvedTxnType)
|
||||
.officerId(officerId)
|
||||
.officerUsername(officerUsername)
|
||||
|
||||
+3
-2
@@ -29,6 +29,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Cashier Share Collection API — POST /v1/cashier/sharecollection
|
||||
@@ -131,7 +132,7 @@ public class ShareCollectionApiResource {
|
||||
final LocalDate entryDate;
|
||||
try {
|
||||
entryDate = transactionDateStr != null
|
||||
? LocalDate.parse(transactionDateStr) : LocalDate.now();
|
||||
? LocalDate.parse(transactionDateStr) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.sharecollection.date.invalid",
|
||||
@@ -193,7 +194,7 @@ public class ShareCollectionApiResource {
|
||||
final String txnId = "SC-" + shareAccountId + "-" + System.currentTimeMillis();
|
||||
final long userId = currentUser.getId();
|
||||
final String collectorName = currentUser.getUsername();
|
||||
final LocalDateTime now = LocalDateTime.now();
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// ── Post GL journal entries ────────────────────────────────────────
|
||||
// Dr Cash at Vault (SHARES_REFERENCE) = totalAmount
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.service.CfcfrmsComplaintService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ import org.springframework.stereotype.Component;
|
||||
* POST /v1/simulator/cfcfrms/verify-account – Verify account at bank
|
||||
* POST /v1/simulator/cfcfrms/verify-transaction – Verify transaction ref
|
||||
*
|
||||
* --- STATUS & REPORTING ---
|
||||
* --- STATUS & REPORTING ---
|
||||
* GET /v1/simulator/cfcfrms/complaint-status/{ref} – Complaint status from CFCFRMS side
|
||||
* GET /v1/simulator/cfcfrms/bank-performance – Bank performance metrics
|
||||
* </pre>
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Seeds CFCFRMS complaint tickets, audit logs, and simulator logs on startup.
|
||||
@@ -59,7 +60,7 @@ public class CfcfrmsDataSeeder {
|
||||
}
|
||||
|
||||
LOG.info("Seeding CFCFRMS complaint data...");
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// ─── 3 RECEIVED ──────────────────────────────────────────────
|
||||
seedTicket("TKT-20250318-001", "Rajesh Kumar Sharma", "9876543210",
|
||||
|
||||
+3
-2
@@ -46,6 +46,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -206,7 +207,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
|
||||
final Map<String, Object> result = new HashMap<>();
|
||||
result.put("escalatedTo", nextIfsc);
|
||||
result.put("escalationRef", escalationRef);
|
||||
result.put("timestamp", OffsetDateTime.now().toString());
|
||||
result.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
|
||||
}
|
||||
|
||||
private String generateTicketId() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
final String date = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
final int seq = (int) (ticketRepository.count() + 1);
|
||||
return String.format("TKT-%s-%03d", date, seq);
|
||||
}
|
||||
|
||||
+8
-9
@@ -25,12 +25,10 @@ import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.CfcfrmsConstants;
|
||||
@@ -44,6 +42,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -76,7 +75,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("ticketId", ticketData.getTicketId());
|
||||
response.put("pushedToBankAt", OffsetDateTime.now().toString());
|
||||
response.put("pushedToBankAt", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
response.put("bankAcknowledged", true);
|
||||
response.put("message", "Complaint pushed to " + CfcfrmsConstants.BANK_IFSC + " successfully");
|
||||
return response;
|
||||
@@ -85,7 +84,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
@Override
|
||||
public Map<String, Object> pushHoldDirective(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String directiveRef = "DIR-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
final String directiveRef = "DIR-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
final String directiveType = getStringOrThrow(json, "directiveType");
|
||||
@@ -112,7 +111,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
response.put("directiveRef", directiveRef);
|
||||
response.put("status", "DIRECTIVE_SENT");
|
||||
response.put("bankResponse", bankResponse);
|
||||
response.put("timestamp", OffsetDateTime.now().toString());
|
||||
response.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -208,7 +207,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
public Map<String, Object> receiveEscalation(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String ticketId = getStringOrThrow(json, "ticketId");
|
||||
final String escalationRef = "ESC-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"))
|
||||
final String escalationRef = "ESC-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyy-MMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
// Extract nextBank from fundsMovedTo
|
||||
@@ -268,7 +267,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("transactionFound", true);
|
||||
response.put("transactionStatus", "COMPLETED");
|
||||
response.put("transactionTimestamp", OffsetDateTime.now().minusHours(2).toString());
|
||||
response.put("transactionTimestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).minusHours(2).toString());
|
||||
response.put("senderAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
response.put("receiverAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
|
||||
@@ -369,14 +368,14 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
.setScale(1, java.math.RoundingMode.HALF_UP) + "%"
|
||||
: "0.0%";
|
||||
response.put("tatComplianceRate", tatCompliance);
|
||||
response.put("lastActivityAt", OffsetDateTime.now().toString());
|
||||
response.put("lastActivityAt", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private String generateCfcfrmsRef() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"));
|
||||
final String date = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyy-MMdd"));
|
||||
return "CFCFRMS-" + date + "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
}
|
||||
|
||||
|
||||
+9
-7
@@ -28,12 +28,14 @@ public interface ChequeStockLotRepository extends JpaRepository<ChequeStockLot,
|
||||
List<ChequeStockLot> findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(Long seriesId, Long officeId, String status);
|
||||
|
||||
/**
|
||||
* Anti-duplication control: a newly received range must not overlap any existing lot's range within the SAME
|
||||
* series. Each series is its own stationery batch from the press (different leaf count/paper stock) and is
|
||||
* numbered independently — two different series legitimately reusing the same numeric range is normal in
|
||||
* real-world cheque stock, matching how the press assigns a fresh serial block per print order.
|
||||
* Anti-duplication control: a newly received range must not overlap any existing lot's range, REGARDLESS of
|
||||
* series. A real bank's leaf-serial numbers are allocated from one bank-wide numbering authority — two
|
||||
* different cheque designs (series) never legitimately reuse the same numeric range, because whichever
|
||||
* account eventually receives books from both would end up holding two leaves with the identical serial
|
||||
* (a collision NpciInwardPushService's drawer-account-number disambiguation can't resolve when both leaves
|
||||
* belong to the same account). This used to be scoped to "within the same series only", which is exactly
|
||||
* what let that collision happen in the first place.
|
||||
*/
|
||||
@Query("SELECT COUNT(l) FROM ChequeStockLot l WHERE l.series.id = :seriesId AND l.rangeStart <= :rangeEnd AND l.rangeEnd >= :rangeStart")
|
||||
long countOverlappingInSeries(@Param("seriesId") long seriesId, @Param("rangeStart") long rangeStart,
|
||||
@Param("rangeEnd") long rangeEnd);
|
||||
@Query("SELECT COUNT(l) FROM ChequeStockLot l WHERE l.rangeStart <= :rangeEnd AND l.rangeEnd >= :rangeStart")
|
||||
long countOverlappingAnySeries(@Param("rangeStart") long rangeStart, @Param("rangeEnd") long rangeEnd);
|
||||
}
|
||||
|
||||
+4
-2
@@ -223,8 +223,10 @@ public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatform
|
||||
|
||||
@Override
|
||||
public List<ChequeLeafData> retrieveLeafBySerialNo(final Long leafSerialNo) {
|
||||
// a bare serial number is only unique WITHIN a series (see ChequeStockLotRepository.countOverlappingInSeries) —
|
||||
// two different series can legitimately share a number, so this can return more than one match
|
||||
// new stock lots are now checked for overlap across every series (see
|
||||
// ChequeStockLotRepository.countOverlappingAnySeries), so a bare serial number stays globally unique going
|
||||
// forward. Older lots received before that check existed can still collide, so this stays defensive and
|
||||
// may return more than one match for pre-existing data.
|
||||
final String sql = "select " + this.leafMapper.schema() + " where f.leaf_serial_no = ? order by s.series_code asc";
|
||||
final List<ChequeLeafData> leaves = this.jdbcTemplate.query(sql, this.leafMapper, leafSerialNo);
|
||||
if (leaves.isEmpty()) {
|
||||
|
||||
+8
-5
@@ -185,13 +185,14 @@ public class ChequeBookWritePlatformServiceImpl implements ChequeBookWritePlatfo
|
||||
@Override
|
||||
public CommandProcessingResult receiveStock(final JsonCommand command) {
|
||||
this.validator.validateForStockLotReceive(command.json());
|
||||
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
|
||||
final Long rangeStart = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue());
|
||||
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
|
||||
// fail fast for the maker; re-checked authoritatively at approval time in case stock changed meanwhile
|
||||
if (this.chequeStockLotRepository.countOverlappingInSeries(seriesId, rangeStart, rangeEnd) > 0) {
|
||||
if (this.chequeStockLotRepository.countOverlappingAnySeries(rangeStart, rangeEnd) > 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
|
||||
"The given leaf-serial range overlaps with an existing stock lot for this series.", rangeStart);
|
||||
"The given leaf-serial range overlaps with an existing stock lot (checked across every series, "
|
||||
+ "not just this one) — pick a different range.",
|
||||
rangeStart);
|
||||
}
|
||||
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.RECEIVE_STOCK, command);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
|
||||
@@ -208,9 +209,11 @@ public class ChequeBookWritePlatformServiceImpl implements ChequeBookWritePlatfo
|
||||
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
|
||||
final String batchRef = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.BATCH_REF.getValue());
|
||||
|
||||
if (this.chequeStockLotRepository.countOverlappingInSeries(seriesId, rangeStart, rangeEnd) > 0) {
|
||||
if (this.chequeStockLotRepository.countOverlappingAnySeries(rangeStart, rangeEnd) > 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
|
||||
"The given leaf-serial range overlaps with an existing stock lot for this series.", rangeStart);
|
||||
"The given leaf-serial range overlaps with an existing stock lot (checked across every series, "
|
||||
+ "not just this one) — pick a different range.",
|
||||
rangeStart);
|
||||
}
|
||||
|
||||
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ public class NpciInwardPushService {
|
||||
final String presentingBranchCode = getString(instrument, "depositBranchCode");
|
||||
item.recordInwardMeta(getString(instrument, "serialNumber"), presentingBranchCode, batch.getReturnTatDeadline());
|
||||
|
||||
final Boolean instrumentStopPaymentFlag = instrument.has("stopPaymentFlag") && !instrument.get("stopPaymentFlag").isJsonNull()
|
||||
final Boolean instrumentStopPaymentFlag = (instrument.has("stopPaymentFlag") && !instrument.get("stopPaymentFlag").isJsonNull())
|
||||
? instrument.get("stopPaymentFlag").getAsBoolean()
|
||||
: null;
|
||||
item.recordSourceDetails(getString(instrument, "ctsReferenceNumber"), instrumentStopPaymentFlag,
|
||||
|
||||
+11
-54
@@ -70,6 +70,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/clerk/disbursement-requests")
|
||||
@Component
|
||||
@@ -383,7 +384,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final JsonArray creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class);
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final String description = (String) record.get("description");
|
||||
final String txnDateStr = LocalDate.now().toString();
|
||||
final String txnDateStr = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
final Long sourceGlId = record.get("gl_account_id") != null
|
||||
? ((Number) record.get("gl_account_id")).longValue() : null;
|
||||
@@ -556,7 +557,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final String entryType = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "?";
|
||||
final Long entryPtId = entry.has("paymentTypeId") && !entry.get("paymentTypeId").isJsonNull()
|
||||
? entry.get("paymentTypeId").getAsLong()
|
||||
? Long.valueOf(entry.get("paymentTypeId").getAsLong())
|
||||
: (ptId != null ? ((Number) ptId).longValue() : null);
|
||||
|
||||
if ("GL".equals(entryType)) {
|
||||
@@ -810,7 +811,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
? req.get("currencyCode").getAsString() : "INR";
|
||||
final String txnDateStr = req.has("transactionDate") && !req.get("transactionDate").isJsonNull()
|
||||
? req.get("transactionDate").getAsString()
|
||||
: LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
final String description = req.has("description") && !req.get("description").isJsonNull()
|
||||
? req.get("description").getAsString() : null;
|
||||
|
||||
@@ -830,7 +831,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
|
||||
// Validate all entries: savings/current only (no GL), savingsAccountId present, amount > 0
|
||||
final Set<Long> drAccountIds = new LinkedHashSet<>();
|
||||
final Set<Long> crAccountIds = new LinkedHashSet<>();
|
||||
|
||||
for (final JsonObject e : drEntries) {
|
||||
final String acTyp = e.has("accountType") ? e.get("accountType").getAsString() : "SAVINGS";
|
||||
@@ -858,7 +858,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
if (drAccountIds.contains(crId)) throw apiError(400,
|
||||
"Account " + (e.has("accountNo") ? e.get("accountNo").getAsString() : crId)
|
||||
+ " cannot appear on both DR and CR sides of the same transfer.");
|
||||
crAccountIds.add(crId);
|
||||
}
|
||||
|
||||
// Validate balanced entry: total DR == total CR
|
||||
@@ -999,7 +998,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
* under concurrent inserts on the same date.
|
||||
*/
|
||||
private String generateBtVoucherNo(final long logId) {
|
||||
return "BT" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
return "BT" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
+ String.format("%05d", logId);
|
||||
}
|
||||
|
||||
@@ -1053,7 +1052,9 @@ public class ClerkDisbursementRequestApiResource {
|
||||
try {
|
||||
final Long txnIdLong = Long.parseLong(txnId);
|
||||
undoOps.add(() -> undoAccountTransaction(accountId, txnIdLong, isCurrentAccount));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void undoAccountTransaction(final Long accountId, final Long transactionId,
|
||||
@@ -1072,50 +1073,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
}
|
||||
|
||||
private String executeSavingsDeposit(
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Long paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String date = formatForFineract(txnDateStr);
|
||||
final JsonObject payload = new JsonObject();
|
||||
payload.addProperty("locale", "en");
|
||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||
payload.addProperty("transactionDate", date);
|
||||
payload.addProperty("transactionAmount", amount);
|
||||
if (paymentTypeId != null) payload.addProperty("paymentTypeId", paymentTypeId);
|
||||
if (note != null && !note.isBlank()) payload.addProperty("note", note);
|
||||
|
||||
final CommandWrapper cmd = new CommandWrapperBuilder()
|
||||
.savingsAccountDeposit(savingsAccountId)
|
||||
.withJson(payload.toString())
|
||||
.build();
|
||||
fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null);
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||
}
|
||||
|
||||
private String executeCurrentAccountDeposit(
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Long paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String date = formatForFineract(txnDateStr);
|
||||
final JsonObject payload = new JsonObject();
|
||||
payload.addProperty("locale", "en");
|
||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||
payload.addProperty("transactionDate", date);
|
||||
payload.addProperty("transactionAmount", amount);
|
||||
if (paymentTypeId != null) payload.addProperty("paymentTypeId", paymentTypeId);
|
||||
if (note != null && !note.isBlank()) payload.addProperty("note", note);
|
||||
|
||||
final CommandWrapper cmd = new CommandWrapperBuilder()
|
||||
.currentAccountDeposit(currentAccountId)
|
||||
.withJson(payload.toString())
|
||||
.build();
|
||||
fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null);
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||
}
|
||||
|
||||
private boolean isCurrentAccountById(final Long accountId) {
|
||||
try {
|
||||
final Integer count = jdbcTemplate.queryForObject(
|
||||
@@ -1131,7 +1088,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,7 +1337,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
? (BigDecimal) accts.get(0).get("account_balance_derived") : BigDecimal.ZERO;
|
||||
|
||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
||||
final Long userId = currentUser.getId();
|
||||
|
||||
@@ -1534,7 +1491,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
+ ". Available: " + currentBalance + ", Requested: " + amount);
|
||||
|
||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
||||
final Long userId = currentUser.getId();
|
||||
|
||||
|
||||
+7
-4
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
@Path("/v1/clerk/withdrawal-requests")
|
||||
@Component
|
||||
@@ -437,7 +440,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
String txnId = null;
|
||||
@@ -713,7 +716,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -741,7 +744,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -780,7 +783,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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.client.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
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.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
import org.apache.fineract.portfolio.client.service.ClientEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.client.service.ClientPendingEditReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/clients")
|
||||
@Component
|
||||
@Tag(name = "Client Edit Approval",
|
||||
description = "Maker-checker workflow for editing an existing Client's core profile details. Edits are held in "
|
||||
+ "m_client_pending_edit (a pending row there is itself the lock) and only applied to the real client once a "
|
||||
+ "different user (the checker) approves — see /clients/{clientId}/edit/approve and /reject below.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientEditApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientEditWritePlatformService clientEditWritePlatformService;
|
||||
private final ClientPendingEditReadPlatformService clientPendingEditReadPlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<ClientPendingEditData> pendingEditApiJsonSerializer;
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Submit a details edit for a Client",
|
||||
description = "Stages the proposed changes in m_client_pending_edit instead of applying them immediately — see "
|
||||
+ "/clients/{clientId}/edit/approve and /reject below.")
|
||||
public String submitEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
this.context.authenticatedUser().validateHasPermissionTo("SUBMITEDIT_CLIENT");
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.submitEditForApproval(clientId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Client details edits pending approval",
|
||||
description = "Supports status/clientName/fromDate/toDate filtering (fromDate/toDate as yyyy-MM-dd, inclusive, filtered "
|
||||
+ "on submitted date; clientName is a case-insensitive substring match) plus offset/limit pagination — mirrors "
|
||||
+ "/chequebook/pending-requests.")
|
||||
public Page<ClientPendingEditData> retrieveAllPendingEdits(@QueryParam("status") final String status,
|
||||
@QueryParam("clientName") final String clientName, @QueryParam("fromDate") final String fromDate,
|
||||
@QueryParam("toDate") final String toDate, @QueryParam("offset") @DefaultValue("0") final Integer offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.clientPendingEditReadPlatformService.retrieveAllPendingEdits(status, clientName, fromDate, toDate, offset, safeLimit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{clientId}/edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending details edit for a Client, if any")
|
||||
public String retrievePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final ClientPendingEditData pendingEdit = this.clientPendingEditReadPlatformService.retrievePendingEdit(clientId);
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.pendingEditApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Client details edit",
|
||||
description = "Applies the proposed changes to the client. The maker of the edit cannot also approve it — no exceptions.")
|
||||
public String approvePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVEEDIT_CLIENT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve client edits");
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.approveEdit(clientId);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Client details edit",
|
||||
description = "Discards the proposed changes without applying them. The maker of the edit cannot also reject it — no exceptions.")
|
||||
public String rejectPendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECTEDIT_CLIENT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject client edits");
|
||||
}
|
||||
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.rejectEdit(clientId, rejectReason);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -39,6 +39,8 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import java.io.InputStream;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -158,7 +160,13 @@ public class ClientKycApiResource {
|
||||
template.put("clientTypeName", clientTypeName);
|
||||
template.put("legalFormId", legalFormId);
|
||||
|
||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId));
|
||||
// Minor detection mirrors the frontend's own rule (age < 18) so the document list
|
||||
// served here stays consistent with what the UI computes from the same date of birth.
|
||||
final LocalDate dateOfBirth = client.dateOfBirth();
|
||||
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 18;
|
||||
template.put("isMinor", isMinor);
|
||||
|
||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
||||
|
||||
return toApiJsonSerializer.serialize(template);
|
||||
}
|
||||
@@ -166,11 +174,21 @@ public class ClientKycApiResource {
|
||||
/**
|
||||
* Returns required KYC documents based on client type (RBI Master Direction on KYC).
|
||||
*/
|
||||
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId) {
|
||||
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId,
|
||||
final boolean isMinor) {
|
||||
final List<Map<String, Object>> docs = new ArrayList<>();
|
||||
|
||||
// Individual types (legalFormId=1)
|
||||
if (legalFormId != null && legalFormId == 1) {
|
||||
if (isMinor) {
|
||||
// Minors cannot hold PAN, Voter ID, Driving Licence or an NREGA job card — RBI
|
||||
// accepts Aadhaar, Passport, Birth Certificate or School ID Card instead.
|
||||
docs.add(doc("Aadhaar Card", "AADHAAR", true));
|
||||
docs.add(doc("Birth Certificate", "BIRTH_CERTIFICATE", false));
|
||||
docs.add(doc("School ID Card", "SCHOOL_ID_CARD", false));
|
||||
docs.add(doc("Passport", "PASSPORT", false));
|
||||
return docs;
|
||||
}
|
||||
docs.add(doc("Aadhaar Card", "AADHAAR", true));
|
||||
docs.add(doc("PAN Card", "PAN", true));
|
||||
docs.add(doc("Voter ID", "VOTER_ID", false));
|
||||
|
||||
+13
-5
@@ -44,6 +44,7 @@ import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.data.AuditData;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.AuditReadPlatformService;
|
||||
@@ -142,7 +143,8 @@ public class ClientsApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n"
|
||||
+ "Example Requests:\n" + "\n" + "clients\n" + "\n" + "clients?fields=displayName,officeName,timeline\n" + "\n"
|
||||
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC")
|
||||
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC\n\n"
|
||||
+ "clients?fromDate=2026-01-01&toDate=2026-01-31 (submittedon_date range, yyyy-MM-dd, inclusive)")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
|
||||
public String retrieveAll(@Context final UriInfo uriInfo,
|
||||
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
|
||||
@@ -157,10 +159,15 @@ public class ClientsApiResource {
|
||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
|
||||
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly,
|
||||
@QueryParam("legalForm") final Integer legalForm) {
|
||||
@QueryParam("legalForm") final Integer legalForm,
|
||||
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate) {
|
||||
|
||||
final LocalDate fromDateParsed = StringUtils.isNotBlank(fromDate) ? LocalDate.parse(fromDate) : null;
|
||||
final LocalDate toDateParsed = StringUtils.isNotBlank(toDate) ? LocalDate.parse(toDate) : null;
|
||||
|
||||
return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
|
||||
orderBy, sortOrder, orphansOnly, false);
|
||||
orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -437,7 +444,7 @@ public class ClientsApiResource {
|
||||
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName,
|
||||
final String firstname, final String lastname, final String status, final Integer legalForm, final String hierarchy,
|
||||
final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly,
|
||||
final boolean isSelfUser) {
|
||||
final boolean isSelfUser, final LocalDate fromDate, final LocalDate toDate) {
|
||||
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
sqlValidator.validate(orderBy);
|
||||
sqlValidator.validate(sortOrder);
|
||||
@@ -445,7 +452,8 @@ public class ClientsApiResource {
|
||||
sqlValidator.validate(hierarchy);
|
||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
|
||||
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly)
|
||||
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).build();
|
||||
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).fromDate(fromDate)
|
||||
.toDate(toDate).build();
|
||||
final Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
|
||||
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.client.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
import org.apache.fineract.portfolio.client.service.KycRiskRulesReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/kyc/risk-rules")
|
||||
@Component
|
||||
@Tag(name = "KYC Risk Rules", description = "Retrieve database-driven master rules for client AML/CFT risk rating calculation.")
|
||||
@RequiredArgsConstructor
|
||||
public class KycRiskRulesApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final KycRiskRulesReadPlatformService readPlatformService;
|
||||
private final DefaultToApiJsonSerializer<KycRiskRuleData> toApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve KYC risk rules", description = "Returns list of database-defined risk rules ordered by precedence.")
|
||||
public String retrieveRiskRules() {
|
||||
this.context.authenticatedUser();
|
||||
final List<KycRiskRuleData> rules = this.readPlatformService.retrieveAllRules();
|
||||
return this.toApiJsonSerializer.serialize(rules);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Dummy / stub implementation of {@link CkycSftpUploadService}.
|
||||
@@ -60,7 +61,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
|
||||
final String ckycNumber = generateCkycNumber(kyc.getClient().getId());
|
||||
final String batchId = generateBatchId();
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
return CkycUploadResult.builder()
|
||||
.ckycNumber(ckycNumber)
|
||||
@@ -82,7 +83,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
return CkycUploadResult.builder()
|
||||
.status("FAILED")
|
||||
.remarks("Uploaded file is not a valid ZIP archive (bad magic bytes).")
|
||||
.uploadedOn(LocalDate.now())
|
||||
.uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
return CkycUploadResult.builder()
|
||||
.ckycNumber(ckycNumber)
|
||||
.batchId(batchId)
|
||||
.uploadedOn(LocalDate.now())
|
||||
.uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.status("SUCCESS")
|
||||
.remarks("Dummy CKYC ZIP upload accepted (" + zipBytes.length + " bytes). "
|
||||
+ "Replace with real CERSAI SFTP integration before go-live.")
|
||||
@@ -112,7 +113,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
}
|
||||
|
||||
private String generateBatchId() {
|
||||
return "DUMMYCKYC-" + BATCH_ID_FORMAT.format(LocalDate.now()) + "-"
|
||||
return "DUMMYCKYC-" + BATCH_ID_FORMAT.format(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())) + "-"
|
||||
+ String.format("%06d", BATCH_COUNTER.getAndIncrement());
|
||||
}
|
||||
}
|
||||
|
||||
+59
-37
@@ -32,7 +32,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -50,6 +49,7 @@ import org.apache.fineract.portfolio.client.ckyc.domain.CkycBatchRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -61,16 +61,22 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
private static final DateTimeFormatter ZIP_NAME_TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
|
||||
|
||||
/**
|
||||
* Names of code values (from {@code KYCAddressProofType}) that unambiguously indicate an
|
||||
* address proof. Aadhaar / Passport / DL / Voter ID can be both ID and address proof — we
|
||||
* default them to ID (PI) unless the UI tags them explicitly.
|
||||
* Code (m_code.code_name) under which an identifier's document type must be classified for it
|
||||
* to count as an address proof. Matches the same m_code_value config that
|
||||
* KycProofTypesApiResource (GET /kyc/proof-types) reads to populate the CKYC tab's
|
||||
* "Address Proof Document" card — e.g. Aadhaar Card is seeded as a document type under BOTH
|
||||
* KYCIdProofType and KYCAddressProofType (two separate m_client_identifier rows), so trusting
|
||||
* the code a given identifier was actually tagged under is the only way this can agree with
|
||||
* what the CKYC tab already shows as "available".
|
||||
*/
|
||||
private static final Set<String> ADDRESS_PROOF_ONLY = Set.of(
|
||||
"UTILITY BILLS", "UTILITY BILL", "ELECTRICITY BILL", "WATER BILL", "GAS BILL", "INTERNET BILL",
|
||||
"BANK ACCOUNT STATEMENT / PASSBOOK", "BANK STATEMENT", "POSTPAID MOBILE BILL",
|
||||
"REGISTERED RENT AGREEMENT", "RENT AGREEMENT", "PROPERTY TAX RECEIPT",
|
||||
"EMPLOYER LETTER (FOR SALARIED EMPLOYEES)", "EMPLOYER LETTER",
|
||||
"PENSION PAYMENT ORDERS (FOR RETIREES)", "PENSION PAYMENT ORDER");
|
||||
private static final String ADDRESS_PROOF_CODE_NAME = "KYCAddressProofType";
|
||||
|
||||
// Top-level ZIP folders — one per artefact type, so every client's file of a given type lands
|
||||
// in the same folder instead of all 4 types being interleaved flat at the ZIP root.
|
||||
private static final String PHOTO_FOLDER = "PHOTO/";
|
||||
private static final String SIGNATURE_FOLDER = "SIGNATURE/";
|
||||
private static final String ID_PROOF_FOLDER = "ID_PROOF/";
|
||||
private static final String ADDRESS_PROOF_FOLDER = "ADDRESS_PROOF/";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DocumentReadPlatformService documentReadPlatformService;
|
||||
@@ -86,11 +92,11 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
throw new IllegalArgumentException("clientIds must not be empty");
|
||||
}
|
||||
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final int seq = nextBatchSequenceForToday(today);
|
||||
final String batchId = "BATCH-" + BATCH_ID_FMT.format(today) + "-" + String.format("%03d", seq);
|
||||
// ZIP filename: "CERSAI-ZIP-<yyyy-MM-dd-HH-mm-ss>.zip" — hyphen-separated per spec.
|
||||
final String filename = "CERSAI-ZIP-" + ZIP_NAME_TS_FMT.format(java.time.LocalDateTime.now()) + ".zip";
|
||||
final String filename = "CERSAI-ZIP-" + ZIP_NAME_TS_FMT.format(java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())) + ".zip";
|
||||
|
||||
final List<CkycBatchManifestEntry> manifest = new ArrayList<>();
|
||||
final ByteArrayOutputStream zipBytesOut = new ByteArrayOutputStream();
|
||||
@@ -111,13 +117,30 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
final byte[] zipBytes = zipBytesOut.toByteArray();
|
||||
final String checksum = "sha256:" + sha256Hex(zipBytes);
|
||||
|
||||
// Persist the ZIP as an m_document row (parent_entity_type='ckyc_batch', parent_entity_id=seq)
|
||||
// Persistence happens BEFORE we know m_ckyc_batch.id, so we use the synthetic seq as a
|
||||
// temporary parent_entity_id. The real FK is via m_ckyc_batch.document_id after save.
|
||||
// Persist the batch registry row FIRST (without documentId) so we have its real,
|
||||
// DB-generated id before creating the ZIP's m_document row. The document's
|
||||
// parent_entity_id must be the actual m_ckyc_batch.id — using the daily `seq` here
|
||||
// (as a stand-in for an id that doesn't exist yet) was wrong: seq resets every day
|
||||
// while m_ckyc_batch.id is a global auto-increment, so GET
|
||||
// /ckyc_batch/{id}/documents/{documentId}/attachment (which matches on the REAL batch
|
||||
// id) 404'd for any batch whose seq didn't coincidentally equal its DB id.
|
||||
final CkycBatch batch = new CkycBatch();
|
||||
batch.setBatchId(batchId);
|
||||
batch.setFilename(filename);
|
||||
batch.setChecksumSha256(checksum);
|
||||
batch.setRecordCount(includedCount);
|
||||
batch.setSizeBytes((long) zipBytes.length);
|
||||
batch.setCreatedOnDate(today);
|
||||
batch.setUploadedToCersai(false);
|
||||
batch.setManifestJson(new Gson().toJson(manifest));
|
||||
|
||||
final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch);
|
||||
|
||||
// Now persist the ZIP as an m_document row keyed to the batch's real id.
|
||||
final var docResponse = documentWritePlatformService.createDocument(
|
||||
DocumentCreateRequest.builder()
|
||||
.entityType("ckyc_batch")
|
||||
.entityId((long) seq)
|
||||
.entityId(saved.getId())
|
||||
.name(batchId)
|
||||
.fileName(filename)
|
||||
.description(StringUtils.defaultIfEmpty(remarks, "CERSAI CKYC batch ZIP"))
|
||||
@@ -126,19 +149,8 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
.stream(new ByteArrayInputStream(zipBytes))
|
||||
.build());
|
||||
|
||||
// Persist the batch registry row
|
||||
final CkycBatch batch = new CkycBatch();
|
||||
batch.setBatchId(batchId);
|
||||
batch.setFilename(filename);
|
||||
batch.setChecksumSha256(checksum);
|
||||
batch.setRecordCount(includedCount);
|
||||
batch.setSizeBytes((long) zipBytes.length);
|
||||
batch.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null);
|
||||
batch.setCreatedOnDate(today);
|
||||
batch.setUploadedToCersai(false);
|
||||
batch.setManifestJson(new Gson().toJson(manifest));
|
||||
|
||||
final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch);
|
||||
saved.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null);
|
||||
ckycBatchRepository.saveAndFlush(saved);
|
||||
|
||||
log.info("CKYC batch created: id={}, batchId={}, filename={}, recordCount={}/{}, bytes={}",
|
||||
saved.getId(), batchId, filename, includedCount, clientIds.size(), zipBytes.length);
|
||||
@@ -184,32 +196,40 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
entry.setCifNumber(cifNumber);
|
||||
entry.setDisplayName(displayName);
|
||||
|
||||
// Photo — packaged as "<CIF>-PI-PHOTO.<ext>" using m_client.image_id → m_image
|
||||
// Photo — packaged as "PHOTO/<CIF>-PI-PHOTO.<ext>" using m_client.image_id → m_image.
|
||||
// Every artefact type gets its own top-level ZIP folder, so a batch of N clients produces
|
||||
// one PHOTO/ folder containing all N client photos (and likewise for the other 3 types),
|
||||
// rather than every client's files interleaved flat at the ZIP root.
|
||||
final Map<String, Object> photo = new LinkedHashMap<>();
|
||||
photo.put("present", imageId != null);
|
||||
entry.setPhoto(photo);
|
||||
if (imageId != null && cifNumber != null) {
|
||||
packageClientImage(zip, clientId, cifNumber + "-PI-PHOTO", photo);
|
||||
packageClientImage(zip, clientId, PHOTO_FOLDER + cifNumber + "-PI-PHOTO", photo);
|
||||
}
|
||||
|
||||
// Signature — packaged as "<CIF>-PI-SIGNATURE.<ext>"
|
||||
// Signature — packaged as "SIGNATURE/<CIF>-PI-SIGNATURE.<ext>"
|
||||
final Long signatureDocId = findSignatureDocId(clientId);
|
||||
final Map<String, Object> signature = new LinkedHashMap<>();
|
||||
signature.put("present", signatureDocId != null);
|
||||
entry.setSignature(signature);
|
||||
if (signatureDocId != null && cifNumber != null) {
|
||||
packageFile(zip, "clients", clientId, signatureDocId,
|
||||
cifNumber + "-PI-SIGNATURE", signature);
|
||||
SIGNATURE_FOLDER + cifNumber + "-PI-SIGNATURE", signature);
|
||||
}
|
||||
|
||||
// ID proof + Address proof: iterate m_client_identifier, classify by code value name.
|
||||
// ID proof + Address proof: iterate m_client_identifier, classify by the m_code the
|
||||
// identifier's document type actually belongs to (KYCIdProofType vs KYCAddressProofType) —
|
||||
// the same source of truth KycProofTypesApiResource uses for the CKYC tab, so a document
|
||||
// the client sees as "Address Proof: Available" is never silently dropped here.
|
||||
// For each identifier, find the attached m_document (one-to-one, latest wins).
|
||||
// NOTE: the `active` column in m_client_identifier stores the status code (200 = active)
|
||||
// not a 1/0 boolean — so we filter on `status = 200` (ClientIdentifierStatus.ACTIVE).
|
||||
final List<Map<String, Object>> identifiers = jdbcTemplate.queryForList(
|
||||
"select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue "
|
||||
"select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue, "
|
||||
+ " c.code_name as codeName "
|
||||
+ " from m_client_identifier ci "
|
||||
+ " join m_code_value cv on cv.id = ci.document_type_id "
|
||||
+ " join m_code c on c.id = cv.code_id "
|
||||
+ " where ci.client_id = ? and (ci.status = 200 or ci.status is null)",
|
||||
clientId);
|
||||
|
||||
@@ -221,8 +241,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
for (final Map<String, Object> idRow : identifiers) {
|
||||
final Long identifierId = ((Number) idRow.get("identifierId")).longValue();
|
||||
final String codeValue = (String) idRow.get("codeValue");
|
||||
final String codeName = (String) idRow.get("codeName");
|
||||
final String docKey = (String) idRow.get("docKey");
|
||||
final boolean isAddress = ADDRESS_PROOF_ONLY.contains(codeValue == null ? "" : codeValue.toUpperCase(Locale.ROOT));
|
||||
final boolean isAddress = ADDRESS_PROOF_CODE_NAME.equalsIgnoreCase(codeName);
|
||||
final String role = isAddress ? "PA" : "PI";
|
||||
|
||||
final Long attachedDocId = findLatestDocForIdentifier(identifierId);
|
||||
@@ -242,8 +263,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
bucket.put("key", docKey);
|
||||
|
||||
if (cifNumber != null) {
|
||||
final String folder = isAddress ? ADDRESS_PROOF_FOLDER : ID_PROOF_FOLDER;
|
||||
packageFile(zip, "client_identifiers", identifierId, attachedDocId,
|
||||
cifNumber + "-" + role + "-" + shortCode, bucket);
|
||||
folder + cifNumber + "-" + role + "-" + shortCode, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -36,6 +35,7 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -219,6 +219,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
// ===== Customer List =====
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("StringSplitter")
|
||||
public Map<String, Object> listCustomers(String name, String branch, String city, String gender, String state, String dateFrom,
|
||||
String dateTo, Integer page, Integer pageSize, String sort) {
|
||||
|
||||
@@ -375,13 +376,13 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
switch (format != null ? format.toLowerCase() : "csv") {
|
||||
case "txt":
|
||||
contentType = "text/plain";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".txt";
|
||||
filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".txt";
|
||||
content = buildPipeDelimitedExport(rows);
|
||||
break;
|
||||
case "csv":
|
||||
default:
|
||||
contentType = "text/csv";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".csv";
|
||||
filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".csv";
|
||||
content = buildCsvExport(rows);
|
||||
break;
|
||||
}
|
||||
@@ -431,7 +432,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
errors.add("Parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
String fileId = "UPLOAD-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis();
|
||||
String fileId = "UPLOAD-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis();
|
||||
result.put("fileId", fileId);
|
||||
result.put("totalRecords", totalRecords);
|
||||
result.put("customers", customers);
|
||||
@@ -453,9 +454,9 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
@Override
|
||||
public Response generateCkycFile(Map<String, Object> request) {
|
||||
// Placeholder — delegates to CkycBatchBuilderService for actual ZIP generation
|
||||
String content = "10|BATCH|" + LocalDate.now() + "|0\n";
|
||||
String content = "10|BATCH|" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + "|0\n";
|
||||
return Response.ok(content.getBytes(StandardCharsets.UTF_8), "text/plain")
|
||||
.header("Content-Disposition", "attachment; filename=\"ckyc_submission_" + LocalDate.now() + ".txt\"")
|
||||
.header("Content-Disposition", "attachment; filename=\"ckyc_submission_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".txt\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -572,7 +573,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
|
||||
private String buildPipeDelimitedExport(List<Map<String, Object>> rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("10|CKYC_EXPORT|").append(LocalDate.now()).append("|").append(rows.size()).append("\n");
|
||||
sb.append("10|CKYC_EXPORT|").append(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).append("|").append(rows.size()).append("\n");
|
||||
for (Map<String, Object> row : rows) {
|
||||
sb.append("20|");
|
||||
sb.append(val(row.get("cif"))).append("|");
|
||||
|
||||
+3
-2
@@ -47,6 +47,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Dummy / stub implementation of {@link CkycBatchSftpUploadService}.
|
||||
@@ -149,10 +150,10 @@ public class DummyCkycBatchSftpUploadServiceImpl implements CkycBatchSftpUploadS
|
||||
}
|
||||
|
||||
// 3) Build the ACK reference and timestamp
|
||||
final LocalDateTime now = LocalDateTime.now();
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String ackReference = "CERSAI-ACK-" + ACK_TS_FMT.format(now) + "-"
|
||||
+ String.format("%04d", ACK_COUNTER.getAndIncrement());
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// 4) Parse manifest → generate a CKYC number per included client → persist on m_client_kyc
|
||||
final List<Map<String, Object>> perClient = new ArrayList<>();
|
||||
|
||||
+19
-7
@@ -71,6 +71,15 @@ public class ClientKycCommand {
|
||||
}
|
||||
|
||||
public void validateForInitiate() {
|
||||
validateForInitiate(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param skipIdentityDocumentCheck true for minors, who identify via the Birth Certificate /
|
||||
* School ID document-type picker (a separate upload path)
|
||||
* rather than PAN/Aadhaar/GSTIN.
|
||||
*/
|
||||
public void validateForInitiate(final boolean skipIdentityDocumentCheck) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||
|
||||
@@ -101,13 +110,16 @@ public class ClientKycCommand {
|
||||
"GSTIN must be a valid 15-character GST Identification Number");
|
||||
}
|
||||
|
||||
// At least one ID document must be provided
|
||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||
if (!hasPan && !hasAadhaar && !hasGstin) {
|
||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||
.notBlank();
|
||||
// At least one ID document must be provided — except for minors, who prove identity via
|
||||
// the Birth Certificate / School ID document-type picker instead.
|
||||
if (!skipIdentityDocumentCheck) {
|
||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||
if (!hasPan && !hasAadhaar && !hasGstin) {
|
||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||
.notBlank();
|
||||
}
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(errors);
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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.client.contactverification.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
|
||||
import org.apache.fineract.portfolio.client.contactverification.service.ClientContactOtpService;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Staff-facing endpoints used to gate a client's mobileNo / emailAddress change on
|
||||
* {@code PUT /v1/clients/{clientId}} behind dual OTP verification (old destination AND new
|
||||
* destination must each be OTP-verified before the update call will accept the change — see
|
||||
* {@code ClientDataValidator} and {@code ClientWritePlatformServiceJpaRepositoryImpl.updateClient()}).
|
||||
*
|
||||
* <p>
|
||||
* OTP is entered by the staff member on the customer's behalf (no self-service customer portal
|
||||
* exists in this deployment), which is why this resource requires a normal, fully-authenticated
|
||||
* staff session rather than being public like the pre-login {@code /v1/login/otp/*} endpoints.
|
||||
*
|
||||
* <p>
|
||||
* Deliberately does NOT contain the literal path segment {@code /self/} anywhere — that substring
|
||||
* causes {@code TenantAwareBasicAuthenticationFilter} to reject any non-self-service (staff) caller
|
||||
* with a 401, which triggers a browser Basic-Auth popup. See the class-level {@code @Path} below.
|
||||
*
|
||||
* <pre>
|
||||
* POST /v1/clients/{clientId}/contact-verification/otp body: {"channel":"MOBILE"|"EMAIL","destination":"..."}
|
||||
* POST /v1/clients/{clientId}/contact-verification/otp/verify body: {"otpId":123,"otp":"123456"}
|
||||
* </pre>
|
||||
*/
|
||||
@Path("/v1/clients/{clientId}/contact-verification/otp")
|
||||
@Component
|
||||
@Tag(name = "Client Contact Verification",
|
||||
description = "Dual-OTP verification of a client's old/new mobile number or email address, "
|
||||
+ "required before PUT /v1/clients/{clientId} will accept a mobileNo/emailAddress change.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientContactVerificationApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||
private final ClientContactOtpService clientContactOtpService;
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Generate an OTP against a client's mobile number or email address",
|
||||
description = "channel=MOBILE sends via LincPay to `destination`; channel=EMAIL sends a locally-generated "
|
||||
+ "6-digit code to `destination` via SMTP. Call this once for the client's *current* (old) "
|
||||
+ "destination and once for the *incoming* (new) destination before attempting the actual "
|
||||
+ "PUT /v1/clients/{clientId} update — both otpId's are required as mobileOldOtpId/mobileNewOtpId "
|
||||
+ "(or emailOldOtpId/emailNewOtpId) on that call.")
|
||||
public OtpGenerateResponse generate(@PathParam("clientId") final Long clientId, final String apiRequestBodyAsJson,
|
||||
@Context final HttpServletRequest httpRequest) {
|
||||
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final JsonObject payload = parse(apiRequestBodyAsJson);
|
||||
final String channel = stringOrNull(payload, "channel");
|
||||
final String destination = stringOrNull(payload, "destination");
|
||||
|
||||
final String ipAddress = clientIp(httpRequest);
|
||||
final String userAgent = httpRequest != null ? httpRequest.getHeader("User-Agent") : null;
|
||||
|
||||
if ("EMAIL".equalsIgnoreCase(channel)) {
|
||||
return clientContactOtpService.generateEmailOtp(destination, ipAddress, userAgent);
|
||||
}
|
||||
if ("MOBILE".equalsIgnoreCase(channel)) {
|
||||
return clientContactOtpService.generateMobileOtp(destination, ipAddress, userAgent);
|
||||
}
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.channel.invalid",
|
||||
"channel must be one of MOBILE, EMAIL", channel);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("verify")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Verify a previously generated client-contact-change OTP")
|
||||
public OtpVerifyResponse verify(@PathParam("clientId") final Long clientId, final String apiRequestBodyAsJson) {
|
||||
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final JsonObject payload = parse(apiRequestBodyAsJson);
|
||||
final Long otpId = longOrNull(payload, "otpId");
|
||||
final String otp = stringOrNull(payload, "otp");
|
||||
return clientContactOtpService.verify(otpId, otp);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private JsonObject parse(final String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return new JsonObject();
|
||||
}
|
||||
try {
|
||||
return JsonParser.parseString(body).getAsJsonObject();
|
||||
} catch (final Exception e) {
|
||||
return new JsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private String stringOrNull(final JsonObject obj, final String key) {
|
||||
return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsString() : null;
|
||||
}
|
||||
|
||||
private Long longOrNull(final JsonObject obj, final String key) {
|
||||
try {
|
||||
return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsLong() : null;
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String clientIp(final HttpServletRequest req) {
|
||||
if (req == null) {
|
||||
return null;
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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.client.contactverification.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
|
||||
import org.apache.fineract.portfolio.client.contactverification.service.ClientContactOtpService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Sibling of {@link ClientContactVerificationApiResource}, for the CLIENT CREATION flow — a
|
||||
* brand-new client has no id yet, so the {@code {clientId}} path segment used by the edit-time
|
||||
* resource (which looks the client up via {@code findOneWithNotFoundDetection}) can't be reused
|
||||
* as-is here. Same {@code ClientContactOtpService} underneath, same request/response shapes —
|
||||
* only the path (no {@code clientId} segment, so no client-existence check) and the permission
|
||||
* (CREATE instead of UPDATE) differ.
|
||||
*
|
||||
* <p>
|
||||
* Segment count disambiguates this from {@code ClientContactVerificationApiResource}'s path at the
|
||||
* JAX-RS routing level (4 segments here vs. 5 with {@code {clientId}}), so both resources coexist
|
||||
* without ambiguity.
|
||||
*
|
||||
* <p>
|
||||
* Deliberately does NOT contain the literal path segment {@code /self/} anywhere — see
|
||||
* {@link ClientContactVerificationApiResource}'s class Javadoc for why that substring is unsafe in
|
||||
* this codebase.
|
||||
*
|
||||
* <pre>
|
||||
* POST /v1/clients/contact-verification/otp body: {"channel":"MOBILE"|"EMAIL","destination":"..."}
|
||||
* POST /v1/clients/contact-verification/otp/verify body: {"otpId":123,"otp":"123456"}
|
||||
* </pre>
|
||||
*/
|
||||
@Path("/v1/clients/contact-verification/otp")
|
||||
@Component
|
||||
@Tag(name = "Client Creation Contact Verification",
|
||||
description = "OTP verification of a brand-new client's mobile number / email address at creation time, "
|
||||
+ "required before POST /v1/clients will accept the value — guarantees every mobileNo/emailAddress "
|
||||
+ "ever stored was reachable at the moment it was recorded.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientCreationContactVerificationApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientContactOtpService clientContactOtpService;
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Generate an OTP against a not-yet-created client's mobile number or email address",
|
||||
description = "channel=MOBILE sends via LincPay to `destination`; channel=EMAIL sends a locally-generated "
|
||||
+ "6-digit code to `destination` via SMTP. The returned otpId is required as mobileNewOtpId / "
|
||||
+ "emailNewOtpId on the subsequent POST /v1/clients call.")
|
||||
public OtpGenerateResponse generate(final String apiRequestBodyAsJson, @Context final HttpServletRequest httpRequest) {
|
||||
|
||||
this.context.authenticatedUser().validateHasCreatePermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final JsonObject payload = parse(apiRequestBodyAsJson);
|
||||
final String channel = stringOrNull(payload, "channel");
|
||||
final String destination = stringOrNull(payload, "destination");
|
||||
|
||||
final String ipAddress = clientIp(httpRequest);
|
||||
final String userAgent = httpRequest != null ? httpRequest.getHeader("User-Agent") : null;
|
||||
|
||||
if ("EMAIL".equalsIgnoreCase(channel)) {
|
||||
return clientContactOtpService.generateEmailOtp(destination, ipAddress, userAgent);
|
||||
}
|
||||
if ("MOBILE".equalsIgnoreCase(channel)) {
|
||||
return clientContactOtpService.generateMobileOtp(destination, ipAddress, userAgent);
|
||||
}
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.channel.invalid",
|
||||
"channel must be one of MOBILE, EMAIL", channel);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("verify")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Verify a previously generated client-creation contact OTP")
|
||||
public OtpVerifyResponse verify(final String apiRequestBodyAsJson) {
|
||||
|
||||
this.context.authenticatedUser().validateHasCreatePermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final JsonObject payload = parse(apiRequestBodyAsJson);
|
||||
final Long otpId = longOrNull(payload, "otpId");
|
||||
final String otp = stringOrNull(payload, "otp");
|
||||
return clientContactOtpService.verify(otpId, otp);
|
||||
}
|
||||
|
||||
// ── Helpers (mirrors ClientContactVerificationApiResource) ────────────────
|
||||
|
||||
private JsonObject parse(final String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return new JsonObject();
|
||||
}
|
||||
try {
|
||||
return JsonParser.parseString(body).getAsJsonObject();
|
||||
} catch (final Exception e) {
|
||||
return new JsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private String stringOrNull(final JsonObject obj, final String key) {
|
||||
return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsString() : null;
|
||||
}
|
||||
|
||||
private Long longOrNull(final JsonObject obj, final String key) {
|
||||
try {
|
||||
return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsLong() : null;
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String clientIp(final HttpServletRequest req) {
|
||||
if (req == null) {
|
||||
return null;
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.client.contactverification.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
|
||||
/**
|
||||
* Client-facing OTP generate/verify used to gate mobileNo / emailAddress changes on
|
||||
* {@code PUT /v1/clients/{clientId}} (see {@code ClientDataValidator} and
|
||||
* {@code ClientWritePlatformServiceJpaRepositoryImpl.updateClient()}).
|
||||
*
|
||||
* <p>
|
||||
* This mirrors {@code LoginOtpService} / {@code EmailLoginOtpService} (same LincPay / local-hash
|
||||
* mechanics, same {@code m_otp_log} bookkeeping) but takes a raw destination directly instead of
|
||||
* resolving one from an {@code AppUser} — here the "user" being verified is a client's contact
|
||||
* channel, entered by a staff member on the client's behalf, not a login user.
|
||||
*
|
||||
* <p>
|
||||
* Every row created by this service uses {@code purpose = "CLIENT_CONTACT_CHANGE"} so it is
|
||||
* unambiguously distinct from the login-2FA rows in the same table.
|
||||
*/
|
||||
public interface ClientContactOtpService {
|
||||
|
||||
/**
|
||||
* Send an OTP to {@code mobileNumber} via LincPay (same call shape as
|
||||
* {@code LoginOtpServiceImpl.generate()}). {@code userId}/{@code username} are left null on the
|
||||
* {@code m_otp_log} row since no AppUser is involved.
|
||||
*/
|
||||
OtpGenerateResponse generateMobileOtp(String mobileNumber, String ipAddress, String userAgent);
|
||||
|
||||
/**
|
||||
* Generate a 6-digit OTP locally, store its SHA-256 hash, and email it to {@code emailAddress}
|
||||
* (same mechanics as {@code EmailLoginOtpServiceImpl.generate()}).
|
||||
*/
|
||||
OtpGenerateResponse generateEmailOtp(String emailAddress, String ipAddress, String userAgent);
|
||||
|
||||
/**
|
||||
* Verify an OTP issued by either of the above. Dispatches to the LincPay delegate or the local
|
||||
* hash-compare depending on the referenced row's {@code provider}.
|
||||
*/
|
||||
OtpVerifyResponse verify(Long otpId, String otp);
|
||||
|
||||
/**
|
||||
* Assert that {@code otpId} references a {@code CLIENT_CONTACT_CHANGE}, {@code VERIFIED}
|
||||
* {@code m_otp_log} row whose stored mobile number equals {@code expectedMobileNumber}. Throws
|
||||
* {@link org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException} on
|
||||
* any mismatch. Used by {@code ClientWritePlatformServiceJpaRepositoryImpl} immediately before
|
||||
* applying a mobileNo change — never trusts the client-supplied JSON, always re-derives the
|
||||
* expected destination from the DB (old) or the incoming command (new).
|
||||
*
|
||||
* @param otpIdParamLabel the request parameter name this otpId came from (e.g. {@code mobileOldOtpId},
|
||||
* {@code mobileNewOtpId}, {@code emailOldOtpId}, {@code emailNewOtpId}) — used
|
||||
* only to make the error message useful.
|
||||
*/
|
||||
void assertVerifiedMobile(Long otpId, String expectedMobileNumber, String otpIdParamLabel);
|
||||
|
||||
/** Email counterpart of {@link #assertVerifiedMobile(Long, String, String)}. */
|
||||
void assertVerifiedEmail(Long otpId, String expectedEmailAddress, String otpIdParamLabel);
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* 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.client.contactverification.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.email.service.EmailSenderService;
|
||||
import org.apache.fineract.infrastructure.email.service.EmailTemplateService;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpGenerateResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.data.OtpVerifyResponse;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLog;
|
||||
import org.apache.fineract.infrastructure.security.otp.domain.OtpLogRepository;
|
||||
import org.apache.fineract.infrastructure.security.otp.util.OtpDestinationMasker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* See {@link ClientContactOtpService}. Deliberately does NOT touch {@code LoginOtpServiceImpl} /
|
||||
* {@code EmailLoginOtpServiceImpl} — this is a parallel, client-contact-change-specific variant that
|
||||
* happens to talk to the same LincPay endpoints / SMTP path with a raw destination instead of an
|
||||
* AppUser lookup. Small helper methods (JSON post/parse, sha256, six-digit generator) are duplicated
|
||||
* rather than extracted, matching how this codebase already keeps each OTP-issuing service
|
||||
* self-contained (only the genuinely cross-cutting {@link OtpDestinationMasker} was pulled out).
|
||||
*/
|
||||
@Service
|
||||
public class ClientContactOtpServiceImpl implements ClientContactOtpService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientContactOtpServiceImpl.class);
|
||||
|
||||
public static final String PURPOSE_CLIENT_CONTACT_CHANGE = "CLIENT_CONTACT_CHANGE";
|
||||
private static final String PROVIDER_LINCPAY = "LINCPAY";
|
||||
private static final String PROVIDER_EMAIL = "EMAIL";
|
||||
|
||||
private static final String STATUS_PENDING = "PENDING";
|
||||
private static final String STATUS_SENT = "SENT";
|
||||
private static final String STATUS_FAILED = "FAILED";
|
||||
private static final String STATUS_VERIFIED = "VERIFIED";
|
||||
private static final String STATUS_EXPIRED = "EXPIRED";
|
||||
|
||||
private static final DateTimeFormatter SUBJECT_TIMESTAMP = DateTimeFormatter.ofPattern("dd-MM-yy-HH:mm");
|
||||
|
||||
@Value("${fineract.kyc.lincpay.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${fineract.kyc.lincpay.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${fineract.kyc.lincpay.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${fineract.security.otp.lincpay.generate-path:/mobile/generate-otp}")
|
||||
private String generateOtpPath;
|
||||
|
||||
@Value("${fineract.security.otp.lincpay.verify-path:/verify-mobile-otp}")
|
||||
private String verifyOtpPath;
|
||||
|
||||
@Value("${fineract.security.otp.ttl-seconds:300}")
|
||||
private int ttlSeconds;
|
||||
|
||||
@Value("${fineract.security.otp.max-attempts:5}")
|
||||
private int maxAttempts;
|
||||
|
||||
@Value("${fineract.email.from-name:CBSnexora}")
|
||||
private String appName;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final OtpLogRepository otpLogRepository;
|
||||
private final EmailSenderService emailSenderService;
|
||||
private final EmailTemplateService emailTemplateService;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
@Autowired
|
||||
public ClientContactOtpServiceImpl(final OtpLogRepository otpLogRepository, final EmailSenderService emailSenderService,
|
||||
final EmailTemplateService emailTemplateService) {
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.otpLogRepository = otpLogRepository;
|
||||
this.emailSenderService = emailSenderService;
|
||||
this.emailTemplateService = emailTemplateService;
|
||||
}
|
||||
|
||||
// ── Generate ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public OtpGenerateResponse generateMobileOtp(final String mobileNumber, final String ipAddress, final String userAgent) {
|
||||
if (StringUtils.isBlank(mobileNumber)) {
|
||||
return OtpGenerateResponse.builder().success(false).message("mobileNumber is required").build();
|
||||
}
|
||||
|
||||
final OtpLog log = new OtpLog();
|
||||
log.setPurpose(PURPOSE_CLIENT_CONTACT_CHANGE);
|
||||
log.setProvider(PROVIDER_LINCPAY);
|
||||
log.setMobileNumber(mobileNumber);
|
||||
log.setStatus(STATUS_PENDING);
|
||||
log.setIpAddress(ipAddress);
|
||||
log.setUserAgent(userAgent);
|
||||
log.setExpiresAt(OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(ttlSeconds));
|
||||
|
||||
final JsonObject requestBody = new JsonObject();
|
||||
requestBody.addProperty("apiKey", apiKey);
|
||||
requestBody.addProperty("mobileNumber", mobileNumber);
|
||||
requestBody.addProperty("secretKey", secret);
|
||||
|
||||
final JsonObject redactedRequest = new JsonObject();
|
||||
redactedRequest.addProperty("apiKey", "***REDACTED***");
|
||||
redactedRequest.addProperty("mobileNumber", mobileNumber);
|
||||
redactedRequest.addProperty("secretKey", "***REDACTED***");
|
||||
log.setRequestPayload(redactedRequest.toString());
|
||||
|
||||
try {
|
||||
final String url = baseUrl + generateOtpPath;
|
||||
final String rawResponse = post(url, requestBody.toString());
|
||||
log.setResponsePayload(truncate(rawResponse, 65000));
|
||||
|
||||
final JsonObject response = parseJson(rawResponse);
|
||||
if (isSuccess(response)) {
|
||||
log.setStatus(STATUS_SENT);
|
||||
log.setOtpReference(extractField(response, "reference_id", "refNumber", "ref_number", "transactionId", "txnId",
|
||||
"requestId"));
|
||||
final OtpLog saved = otpLogRepository.saveAndFlush(log);
|
||||
LOG.info("Client contact-change mobile OTP sent. otpId={}, mobile={}", saved.getId(),
|
||||
OtpDestinationMasker.maskMobile(mobileNumber));
|
||||
return OtpGenerateResponse.builder().success(true).otpId(saved.getId()).otpReference(saved.getOtpReference())
|
||||
.maskedMobile(OtpDestinationMasker.maskMobile(mobileNumber)).expiresInSeconds(ttlSeconds)
|
||||
.message("OTP sent").build();
|
||||
}
|
||||
|
||||
final String errorMsg = extractField(response, "message", "errorMessage", "error", "description");
|
||||
log.setStatus(STATUS_FAILED);
|
||||
log.setErrorMessage(errorMsg);
|
||||
otpLogRepository.saveAndFlush(log);
|
||||
LOG.warn("Client contact-change mobile OTP failed: {}", errorMsg);
|
||||
return OtpGenerateResponse.builder().success(false).message(errorMsg != null ? errorMsg : "OTP send failed").build();
|
||||
|
||||
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||
log.setStatus(STATUS_FAILED);
|
||||
log.setErrorMessage("HTTP " + e.getStatusCode() + ": " + truncate(e.getResponseBodyAsString(), 900));
|
||||
log.setResponsePayload(truncate(e.getResponseBodyAsString(), 65000));
|
||||
otpLogRepository.saveAndFlush(log);
|
||||
LOG.error("Client contact-change mobile OTP HTTP error: {} {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
return OtpGenerateResponse.builder().success(false).message("OTP send failed (HTTP " + e.getStatusCode() + ")").build();
|
||||
} catch (final Exception e) {
|
||||
log.setStatus(STATUS_FAILED);
|
||||
log.setErrorMessage(truncate(e.getMessage(), 900));
|
||||
otpLogRepository.saveAndFlush(log);
|
||||
LOG.error("Client contact-change mobile OTP unexpected error", e);
|
||||
return OtpGenerateResponse.builder().success(false).message("OTP send failed: " + e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public OtpGenerateResponse generateEmailOtp(final String emailAddress, final String ipAddress, final String userAgent) {
|
||||
if (StringUtils.isBlank(emailAddress)) {
|
||||
return OtpGenerateResponse.builder().success(false).message("emailAddress is required").build();
|
||||
}
|
||||
|
||||
final String otp = generateSixDigitOtp();
|
||||
final int validityMinutes = Math.max(1, ttlSeconds / 60);
|
||||
|
||||
final OtpLog log = new OtpLog();
|
||||
log.setPurpose(PURPOSE_CLIENT_CONTACT_CHANGE);
|
||||
log.setProvider(PROVIDER_EMAIL);
|
||||
log.setEmailAddress(emailAddress);
|
||||
log.setOtpHash(sha256(otp));
|
||||
log.setStatus(STATUS_PENDING);
|
||||
log.setIpAddress(ipAddress);
|
||||
log.setUserAgent(userAgent);
|
||||
log.setExpiresAt(OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(ttlSeconds));
|
||||
|
||||
final String html = emailTemplateService.renderOtp(null, otp, validityMinutes);
|
||||
final String subject = "Your OTP for Contact Verification - " + appName + " (" + LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(SUBJECT_TIMESTAMP)
|
||||
+ ")";
|
||||
|
||||
final boolean sent = emailSenderService.sendHtml(emailAddress, subject, html);
|
||||
if (sent) {
|
||||
log.setStatus(STATUS_SENT);
|
||||
final OtpLog saved = otpLogRepository.saveAndFlush(log);
|
||||
LOG.info("Client contact-change email OTP sent. otpId={}, email={}", saved.getId(),
|
||||
OtpDestinationMasker.maskEmail(emailAddress));
|
||||
return OtpGenerateResponse.builder().success(true).otpId(saved.getId())
|
||||
.maskedMobile(OtpDestinationMasker.maskEmail(emailAddress)).expiresInSeconds(ttlSeconds)
|
||||
.message("OTP sent to the given email address").build();
|
||||
}
|
||||
|
||||
log.setStatus(STATUS_FAILED);
|
||||
log.setErrorMessage("SMTP send failed");
|
||||
otpLogRepository.saveAndFlush(log);
|
||||
return OtpGenerateResponse.builder().success(false).message("OTP send failed").build();
|
||||
}
|
||||
|
||||
// ── Verify ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public OtpVerifyResponse verify(final Long otpId, final String otp) {
|
||||
if (otpId == null || StringUtils.isBlank(otp)) {
|
||||
return OtpVerifyResponse.builder().verified(false).message("otpId and otp are required").build();
|
||||
}
|
||||
|
||||
final OtpLog row = otpLogRepository.findFirstByIdAndIsDeletedFalseOrderByIdDesc(otpId).orElse(null);
|
||||
if (row == null) {
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP not found").build();
|
||||
}
|
||||
if (!PURPOSE_CLIENT_CONTACT_CHANGE.equals(row.getPurpose())) {
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP purpose mismatch").build();
|
||||
}
|
||||
if (STATUS_VERIFIED.equals(row.getStatus())) {
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP already used").build();
|
||||
}
|
||||
if (row.getExpiresAt() != null && OffsetDateTime.now(ZoneOffset.UTC).isAfter(row.getExpiresAt())) {
|
||||
row.setStatus(STATUS_EXPIRED);
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP expired").build();
|
||||
}
|
||||
if (row.getAttempts() != null && row.getAttempts() >= maxAttempts) {
|
||||
row.setStatus(STATUS_EXPIRED);
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
return OtpVerifyResponse.builder().verified(false).message("Max attempts exceeded").build();
|
||||
}
|
||||
|
||||
row.setAttempts((row.getAttempts() == null ? 0 : row.getAttempts()) + 1);
|
||||
|
||||
if (PROVIDER_LINCPAY.equals(row.getProvider())) {
|
||||
return verifyViaLincPay(row, otp);
|
||||
}
|
||||
return verifyViaLocalHash(row, otp);
|
||||
}
|
||||
|
||||
private OtpVerifyResponse verifyViaLincPay(final OtpLog row, final String otp) {
|
||||
row.setOtpHash(sha256(otp));
|
||||
|
||||
final JsonObject requestBody = new JsonObject();
|
||||
requestBody.addProperty("apiKey", apiKey);
|
||||
requestBody.addProperty("mobileNumber", row.getMobileNumber());
|
||||
requestBody.addProperty("otp", otp);
|
||||
requestBody.addProperty("secretKey", secret);
|
||||
|
||||
try {
|
||||
final String url = baseUrl + verifyOtpPath;
|
||||
final String rawResponse = post(url, requestBody.toString());
|
||||
final String existing = row.getResponsePayload() == null ? "" : row.getResponsePayload();
|
||||
row.setResponsePayload(truncate(existing + "\n--verify--\n" + rawResponse, 65000));
|
||||
|
||||
final JsonObject response = parseJson(rawResponse);
|
||||
if (isSuccess(response)) {
|
||||
row.setStatus(STATUS_VERIFIED);
|
||||
row.setVerifiedAt(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
LOG.info("Client contact-change mobile OTP verified. otpId={}, mobile={}", row.getId(),
|
||||
OtpDestinationMasker.maskMobile(row.getMobileNumber()));
|
||||
return OtpVerifyResponse.builder().verified(true).message("OTP verified").build();
|
||||
}
|
||||
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
final String providerMsg = extractField(response, "message", "errorMessage", "error", "description");
|
||||
final int remaining = Math.max(0, maxAttempts - row.getAttempts());
|
||||
return OtpVerifyResponse.builder().verified(false).message(providerMsg != null ? providerMsg : "Invalid OTP")
|
||||
.attemptsRemaining(remaining).build();
|
||||
|
||||
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||
row.setErrorMessage("HTTP " + e.getStatusCode() + ": " + truncate(e.getResponseBodyAsString(), 900));
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
LOG.error("Client contact-change verify-mobile-otp HTTP error: {} {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP verification failed (HTTP " + e.getStatusCode() + ")")
|
||||
.build();
|
||||
} catch (final Exception e) {
|
||||
row.setErrorMessage(truncate(e.getMessage(), 900));
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
LOG.error("Client contact-change verify-mobile-otp unexpected error", e);
|
||||
return OtpVerifyResponse.builder().verified(false).message("OTP verification failed: " + e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private OtpVerifyResponse verifyViaLocalHash(final OtpLog row, final String otp) {
|
||||
final boolean matched = row.getOtpHash() != null && row.getOtpHash().equals(sha256(otp));
|
||||
if (matched) {
|
||||
row.setStatus(STATUS_VERIFIED);
|
||||
row.setVerifiedAt(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
LOG.info("Client contact-change email OTP verified. otpId={}", row.getId());
|
||||
return OtpVerifyResponse.builder().verified(true).message("OTP verified").build();
|
||||
}
|
||||
|
||||
otpLogRepository.saveAndFlush(row);
|
||||
final int remaining = Math.max(0, maxAttempts - row.getAttempts());
|
||||
return OtpVerifyResponse.builder().verified(false).message("Invalid OTP").attemptsRemaining(remaining).build();
|
||||
}
|
||||
|
||||
// ── Assertion helpers used by ClientWritePlatformServiceJpaRepositoryImpl ──
|
||||
|
||||
@Override
|
||||
public void assertVerifiedMobile(final Long otpId, final String expectedMobileNumber, final String otpIdParamLabel) {
|
||||
final OtpLog row = requireVerifiedRow(otpId, otpIdParamLabel);
|
||||
if (!equalsIgnoringWhitespace(row.getMobileNumber(), expectedMobileNumber)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.destination.mismatch",
|
||||
"The OTP referenced by '" + otpIdParamLabel + "' was not issued for this mobile number.", otpIdParamLabel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertVerifiedEmail(final Long otpId, final String expectedEmailAddress, final String otpIdParamLabel) {
|
||||
final OtpLog row = requireVerifiedRow(otpId, otpIdParamLabel);
|
||||
if (!equalsIgnoreCaseAndWhitespace(row.getEmailAddress(), expectedEmailAddress)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.destination.mismatch",
|
||||
"The OTP referenced by '" + otpIdParamLabel + "' was not issued for this email address.", otpIdParamLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private OtpLog requireVerifiedRow(final Long otpId, final String otpIdParamLabel) {
|
||||
if (otpId == null) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.required",
|
||||
"'" + otpIdParamLabel + "' is required to change this contact detail.", otpIdParamLabel);
|
||||
}
|
||||
final OtpLog row = otpLogRepository.findFirstByIdAndIsDeletedFalseOrderByIdDesc(otpId).orElse(null);
|
||||
if (row == null) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.not.found",
|
||||
"No OTP record found for '" + otpIdParamLabel + "' = " + otpId + ".", otpIdParamLabel, otpId);
|
||||
}
|
||||
if (!PURPOSE_CLIENT_CONTACT_CHANGE.equals(row.getPurpose())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.purpose.mismatch",
|
||||
"'" + otpIdParamLabel + "' does not reference a contact-change OTP.", otpIdParamLabel);
|
||||
}
|
||||
if (!STATUS_VERIFIED.equals(row.getStatus())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.client.contact.otp.not.verified",
|
||||
"'" + otpIdParamLabel + "' has not been verified yet. Complete the OTP verification step first.", otpIdParamLabel);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private boolean equalsIgnoringWhitespace(final String a, final String b) {
|
||||
final String na = a == null ? null : a.trim();
|
||||
final String nb = b == null ? null : b.trim();
|
||||
return na != null && na.equals(nb);
|
||||
}
|
||||
|
||||
private boolean equalsIgnoreCaseAndWhitespace(final String a, final String b) {
|
||||
final String na = a == null ? null : a.trim().toLowerCase(Locale.ROOT);
|
||||
final String nb = b == null ? null : b.trim().toLowerCase(Locale.ROOT);
|
||||
return na != null && na.equals(nb);
|
||||
}
|
||||
|
||||
// ── Helpers (deliberately duplicated from LoginOtpServiceImpl/EmailLoginOtpServiceImpl —
|
||||
// see class Javadoc) ────────────────────────────────────────────────────
|
||||
|
||||
private String post(final String url, final String requestBodyJson) {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("accept", "*/*");
|
||||
final HttpEntity<String> entity = new HttpEntity<>(requestBodyJson, headers);
|
||||
final ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
private JsonObject parseJson(final String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return new JsonObject();
|
||||
}
|
||||
final JsonElement parsed = JsonParser.parseString(raw);
|
||||
return parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
|
||||
}
|
||||
|
||||
private boolean isSuccess(final JsonObject response) {
|
||||
for (final String key : new String[] { "status", "statusCode", "responseCode", "code" }) {
|
||||
if (response.has(key)) {
|
||||
final JsonElement v = response.get(key);
|
||||
if (v.isJsonPrimitive()) {
|
||||
final String s = v.getAsString();
|
||||
if ("200".equals(s) || "0".equals(s) || "true".equalsIgnoreCase(s) || "success".equalsIgnoreCase(s)
|
||||
|| "SUCCESS".equalsIgnoreCase(s)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.has("success") && response.get("success").isJsonPrimitive()) {
|
||||
return response.get("success").getAsBoolean();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String extractField(final JsonObject obj, final String... keys) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
for (final String key : keys) {
|
||||
if (obj.has(key) && obj.get(key).isJsonPrimitive()) {
|
||||
return obj.get(key).getAsString();
|
||||
}
|
||||
}
|
||||
if (obj.has("data") && obj.get("data").isJsonObject()) {
|
||||
final JsonObject data = obj.getAsJsonObject("data");
|
||||
for (final String key : keys) {
|
||||
if (data.has(key) && data.get(key).isJsonPrimitive()) {
|
||||
return data.get(key).getAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String generateSixDigitOtp() {
|
||||
final int n = secureRandom.nextInt(1_000_000);
|
||||
return String.format("%06d", n);
|
||||
}
|
||||
|
||||
private String sha256(final String input) {
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(md.digest(input.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String truncate(final String s, final int max) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s.length() <= max ? s : s.substring(0, max);
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -32,14 +32,16 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
||||
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
||||
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
||||
educationLevelIdParamName, degreeQualificationIdParamName,
|
||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
|
||||
agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName,
|
||||
monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName,
|
||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
|
||||
nameVerifiedParamName, screeningLogIdParamName,
|
||||
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName, kycParamName));
|
||||
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName, kycParamName,
|
||||
mobileNewOtpIdParamName, emailNewOtpIdParamName));
|
||||
|
||||
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
|
||||
@@ -54,14 +56,16 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
||||
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
||||
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName,
|
||||
educationLevelIdParamName, degreeQualificationIdParamName,
|
||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
|
||||
agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName,
|
||||
monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||
isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName,
|
||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
|
||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
|
||||
nameVerifiedParamName, screeningLogIdParamName,
|
||||
submittedOnDateParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, isStaffParamName));
|
||||
submittedOnDateParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, isStaffParamName,
|
||||
mobileOldOtpIdParamName, mobileNewOtpIdParamName, emailOldOtpIdParamName, emailNewOtpIdParamName));
|
||||
|
||||
protected static final Set<String> CLIENT_NON_PERSON_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList(localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName, incorpValidityTillParamName,
|
||||
|
||||
+54
@@ -166,6 +166,24 @@ public final class ClientDataValidator {
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
|
||||
// A mobile number/email address entered at client creation is never re-verified before this
|
||||
// point — if it's a typo (wrong number, doesn't belong to this customer), the client would be
|
||||
// permanently stuck later: editing it requires proving ownership of the OLD (wrong) value via
|
||||
// OTP, which is impossible if that value was never real. Requiring a verified OTP for the
|
||||
// value here — the same ClientContactOtpService used by the edit-time dual-OTP gate, just the
|
||||
// single "new" leg since there's no "old" value yet — guarantees every mobileNo/emailAddress
|
||||
// ever stored was actually reachable at the moment it was recorded.
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.mobileNoParamName, element)) {
|
||||
final Long mobileNewOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.mobileNewOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.mobileNewOtpIdParamName).value(mobileNewOtpId).notNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.emailAddressParamName, element)) {
|
||||
final Long emailNewOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.emailNewOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.emailNewOtpIdParamName).value(emailNewOtpId).notNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateMobileNoParamName, element)) {
|
||||
final String alternateMobileNo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.alternateMobileNoParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.alternateMobileNoParamName).value(alternateMobileNo).ignoreIfNull()
|
||||
@@ -495,6 +513,42 @@ public final class ClientDataValidator {
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
|
||||
// Dual-OTP contact-change gate: a mobileNo or emailAddress change must reference an
|
||||
// already-VERIFIED m_otp_log row for the OLD destination and one for the NEW destination
|
||||
// (see ClientContactOtpService + ClientWritePlatformServiceJpaRepositoryImpl.updateClient()).
|
||||
//
|
||||
// IMPORTANT: this validator only sees the raw request JSON, not the client's current DB row —
|
||||
// it cannot tell "emailAddress is present but unchanged" (this codebase's edit-client screen
|
||||
// always PUTs the full client object, changed fields or not) apart from "emailAddress is
|
||||
// present and being changed". Asserting these params mandatory here would reject ordinary
|
||||
// saves that don't touch contact details at all. The real gate — requiring a verified otpId
|
||||
// ONLY when the value actually differs from the DB — already lives correctly in
|
||||
// ClientWritePlatformServiceJpaRepositoryImpl.updateClient() (inside the
|
||||
// isChangeInStringParameterNamed(...) blocks) via ClientContactOtpService.assertVerifiedMobile/
|
||||
// assertVerifiedEmail, which itself throws if the otpId is null. So here we only validate
|
||||
// shape (a positive long) when the ids happen to be present, and leave presence-enforcement to
|
||||
// the write service, which is the only place that knows whether this is a real change.
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.mobileOldOtpIdParamName, element)) {
|
||||
final Long mobileOldOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.mobileOldOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.mobileOldOtpIdParamName).value(mobileOldOtpId).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.mobileNewOtpIdParamName, element)) {
|
||||
final Long mobileNewOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.mobileNewOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.mobileNewOtpIdParamName).value(mobileNewOtpId).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.emailOldOtpIdParamName, element)) {
|
||||
final Long emailOldOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.emailOldOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.emailOldOtpIdParamName).value(emailOldOtpId).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.emailNewOtpIdParamName, element)) {
|
||||
final Long emailNewOtpId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.emailNewOtpIdParamName, element);
|
||||
baseDataValidator.reset().parameter(ClientApiConstants.emailNewOtpIdParamName).value(emailNewOtpId).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
}
|
||||
|
||||
final Boolean active = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.activeParamName, element);
|
||||
if (active != null) {
|
||||
atLeastOneParameterPassedForUpdate = true;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.client.data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class ClientPendingEditData {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientDisplayName;
|
||||
private final String payloadJson;
|
||||
private final String originalJson;
|
||||
private final String status;
|
||||
private final Long makerId;
|
||||
private final String makerName;
|
||||
private final OffsetDateTime submittedDate;
|
||||
private final Long checkerId;
|
||||
private final String checkerName;
|
||||
private final OffsetDateTime checkedDate;
|
||||
private final String rejectReason;
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class KycRiskRuleData implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final String ruleName;
|
||||
private final Integer legalFormId;
|
||||
private final Long occupationCategoryCvId;
|
||||
private final String occupationCategoryName;
|
||||
private final Long occupationCvId;
|
||||
private final String occupationName;
|
||||
private final BigDecimal minMonthlyIncome;
|
||||
private final BigDecimal maxMonthlyIncome;
|
||||
private final BigDecimal minAnnualIncome;
|
||||
private final BigDecimal maxAnnualIncome;
|
||||
private final BigDecimal minAnnualTurnover;
|
||||
private final BigDecimal maxAnnualTurnover;
|
||||
private final Integer riskCategory;
|
||||
private final String reason;
|
||||
private final Integer orderPosition;
|
||||
|
||||
public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId,
|
||||
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||
final Long occupationCvId, final String occupationName,
|
||||
final BigDecimal minMonthlyIncome, final BigDecimal maxMonthlyIncome,
|
||||
final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
||||
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
||||
final Integer riskCategory, final String reason, final Integer orderPosition) {
|
||||
this.id = id;
|
||||
this.ruleName = ruleName;
|
||||
this.legalFormId = legalFormId;
|
||||
this.occupationCategoryCvId = occupationCategoryCvId;
|
||||
this.occupationCategoryName = occupationCategoryName;
|
||||
this.occupationCvId = occupationCvId;
|
||||
this.occupationName = occupationName;
|
||||
this.minMonthlyIncome = minMonthlyIncome;
|
||||
this.maxMonthlyIncome = maxMonthlyIncome;
|
||||
this.minAnnualIncome = minAnnualIncome;
|
||||
this.maxAnnualIncome = maxAnnualIncome;
|
||||
this.minAnnualTurnover = minAnnualTurnover;
|
||||
this.maxAnnualTurnover = maxAnnualTurnover;
|
||||
this.riskCategory = riskCategory;
|
||||
this.reason = reason;
|
||||
this.orderPosition = orderPosition;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getRuleName() {
|
||||
return ruleName;
|
||||
}
|
||||
|
||||
public Integer getLegalFormId() {
|
||||
return legalFormId;
|
||||
}
|
||||
|
||||
public Long getOccupationCategoryCvId() {
|
||||
return occupationCategoryCvId;
|
||||
}
|
||||
|
||||
public String getOccupationCategoryName() {
|
||||
return occupationCategoryName;
|
||||
}
|
||||
|
||||
public Long getOccupationCvId() {
|
||||
return occupationCvId;
|
||||
}
|
||||
|
||||
public String getOccupationName() {
|
||||
return occupationName;
|
||||
}
|
||||
|
||||
public BigDecimal getMinMonthlyIncome() {
|
||||
return minMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxMonthlyIncome() {
|
||||
return maxMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualIncome() {
|
||||
return minAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualIncome() {
|
||||
return maxAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualTurnover() {
|
||||
return minAnnualTurnover;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualTurnover() {
|
||||
return maxAnnualTurnover;
|
||||
}
|
||||
|
||||
public Integer getRiskCategory() {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Integer getOrderPosition() {
|
||||
return orderPosition;
|
||||
}
|
||||
}
|
||||
+8
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.apache.fineract.portfolio.client.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
@@ -52,4 +53,11 @@ public interface ClientFamilyMembersRepository
|
||||
@Param("relationshipId") Long relationshipId,
|
||||
@Param("excludeFamilyMemberId") Long excludeFamilyMemberId);
|
||||
|
||||
/**
|
||||
* All non-deleted family members for a client, with their relationship CodeValue. Backs the
|
||||
* "Mother required, plus Father or Caretaker" activation gate.
|
||||
*/
|
||||
@Query("SELECT fm FROM ClientFamilyMembers fm WHERE fm.client.id = :clientId AND fm.isDeleted = false")
|
||||
List<ClientFamilyMembers> findAllActiveByClient(@Param("clientId") Long clientId);
|
||||
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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.client.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Bespoke maker-checker for Client edits — modelled on {@link ClientPendingEditStatus} /
|
||||
* {@code m_share_account_pending_edit} elsewhere in this codebase rather than Fineract's native
|
||||
* command-approval engine, because that engine's hold is bypassed by any role carrying
|
||||
* ALL_FUNCTIONS (Super user, Clerk in this tenant), making it a no-op for this CBS's actual admin
|
||||
* accounts.
|
||||
*
|
||||
* <p>Holds the maker's raw PUT /clients/{id}/edit payload as-is (rather than one typed column per
|
||||
* editable Client field, which — given how many fields Client actually has across general,
|
||||
* financial-classification and KYC-adjacent data — would mean a huge, fragile shadow-column table
|
||||
* that has to be kept in lockstep with the entity forever). Approval reconstructs a
|
||||
* {@code JsonCommand} from the stored JSON and replays it through the existing
|
||||
* {@code ClientWritePlatformService.updateClient(...)}, so all of Client's existing validation and
|
||||
* change-detection logic runs exactly as it would for a direct edit — just deferred until a
|
||||
* different user approves it. The maker can never approve/reject their own submission — enforced
|
||||
* in the write service, not just by role, with no bypass for CHECKER_SUPER_USER.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_client_pending_edit")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class ClientPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "payload_json", nullable = false)
|
||||
private String payloadJson;
|
||||
|
||||
/** Point-in-time "before" snapshot the maker's client had when the edit was submitted — see
|
||||
* client-edit-diff.util.ts's buildOriginalValuesSnapshot(). Null for edits submitted before
|
||||
* this column existed. Lets the checker's compare view show a true historical diff regardless
|
||||
* of whether the live client has since changed (e.g. once approved, current == proposed). */
|
||||
@Column(name = "original_json")
|
||||
private String originalJson;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@Column(name = "maker_id", nullable = false)
|
||||
private Long makerId;
|
||||
|
||||
@Column(name = "submitted_date", nullable = false)
|
||||
private OffsetDateTime submittedDate;
|
||||
|
||||
@Column(name = "checker_id")
|
||||
private Long checkerId;
|
||||
|
||||
@Column(name = "checked_date")
|
||||
private OffsetDateTime checkedDate;
|
||||
|
||||
@Column(name = "reject_reason", length = 500)
|
||||
private String rejectReason;
|
||||
|
||||
@Column(name = "pending_lock_key", length = 60, unique = true)
|
||||
private String pendingLockKey;
|
||||
|
||||
public static ClientPendingEdit submit(final Client client, final String payloadJson, final String originalJson,
|
||||
final AppUser maker) {
|
||||
final ClientPendingEdit edit = new ClientPendingEdit();
|
||||
edit.client = client;
|
||||
edit.payloadJson = payloadJson;
|
||||
edit.originalJson = originalJson;
|
||||
edit.status = ClientPendingEditStatus.PENDING.name();
|
||||
edit.makerId = maker.getId();
|
||||
edit.submittedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
edit.pendingLockKey = lockKeyFor(client.getId());
|
||||
return edit;
|
||||
}
|
||||
|
||||
public static String lockKeyFor(final Long clientId) {
|
||||
return "CLIENT_EDIT:" + clientId;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return ClientPendingEditStatus.PENDING.name().equals(this.status);
|
||||
}
|
||||
|
||||
/** The maker can never be their own checker — a distinct human must approve/reject an edit. */
|
||||
public boolean isMadeBy(final Long userId) {
|
||||
return this.makerId != null && this.makerId.equals(userId);
|
||||
}
|
||||
|
||||
public void approve(final AppUser checker) {
|
||||
this.status = ClientPendingEditStatus.APPROVED.name();
|
||||
this.checkerId = checker.getId();
|
||||
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
public void reject(final AppUser checker, final String reason) {
|
||||
this.status = ClientPendingEditStatus.REJECTED.name();
|
||||
this.checkerId = checker.getId();
|
||||
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.rejectReason = reason;
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.client.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClientPendingEditRepository extends JpaRepository<ClientPendingEdit, Long> {
|
||||
|
||||
Optional<ClientPendingEdit> findByClientIdAndStatus(Long clientId, String status);
|
||||
|
||||
List<ClientPendingEdit> findByStatusOrderBySubmittedDateDesc(String status);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.client.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNoPendingEditException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClientPendingEditRepositoryWrapper {
|
||||
|
||||
private final ClientPendingEditRepository repository;
|
||||
|
||||
public ClientPendingEdit findPendingByClientIdWithNotFoundDetection(final Long clientId) {
|
||||
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name())
|
||||
.orElseThrow(() -> new ClientNoPendingEditException(clientId));
|
||||
}
|
||||
|
||||
public Optional<ClientPendingEdit> findPendingByClientId(final Long clientId) {
|
||||
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name());
|
||||
}
|
||||
|
||||
public List<ClientPendingEdit> findAllPending() {
|
||||
return this.repository.findByStatusOrderBySubmittedDateDesc(ClientPendingEditStatus.PENDING.name());
|
||||
}
|
||||
|
||||
public ClientPendingEdit saveAndFlush(final ClientPendingEdit edit) {
|
||||
return this.repository.saveAndFlush(edit);
|
||||
}
|
||||
}
|
||||
+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.client.domain;
|
||||
|
||||
/** Lifecycle of a {@link ClientPendingEdit}. Terminal once APPROVED or REJECTED. */
|
||||
public enum ClientPendingEditStatus {
|
||||
|
||||
PENDING, //
|
||||
APPROVED, //
|
||||
REJECTED; //
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when the user attempting to activate a client is the same user who created it — a
|
||||
* four-eyes (maker-checker) requirement with no bypass, including for users holding
|
||||
* CHECKER_SUPER_USER, since a bypass would recreate the exact "ALL_FUNCTIONS/super user ignores
|
||||
* the hold" loophole this check exists to close.
|
||||
*/
|
||||
public class ClientActivationSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientActivationSelfApprovalNotAllowedException() {
|
||||
super("error.msg.client.activation.self.approval.not.allowed",
|
||||
"You cannot activate a client you created yourself — a different user must activate it.");
|
||||
}
|
||||
}
|
||||
+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.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ClientEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientEditPendingApprovalException(final Long clientId) {
|
||||
super("error.msg.client.edit.pending.approval", "Client with identifier " + clientId + " already has an edit pending approval",
|
||||
clientId);
|
||||
}
|
||||
}
|
||||
+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.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when the user attempting to approve/reject a Client's pending edit is the same user who
|
||||
* submitted it — no bypass, including for users holding CHECKER_SUPER_USER, since a bypass would
|
||||
* recreate the exact "role bypasses the hold" loophole this check exists to close.
|
||||
*/
|
||||
public class ClientEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientEditSelfApprovalNotAllowedException(final Long clientId) {
|
||||
super("error.msg.client.edit.self.approval.not.allowed",
|
||||
"You cannot approve or reject an edit you submitted yourself for client " + clientId
|
||||
+ " — a different user must review it.",
|
||||
clientId);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown on client activation when neither an active "Father" nor "Caretaker" family member has
|
||||
* been recorded. Kept separate from {@link ClientFamilyMemberMotherRequiredException} so the two
|
||||
* failure reasons surface as distinct errors instead of one merged message.
|
||||
*/
|
||||
public class ClientFamilyMemberFatherOrCaretakerRequiredException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientFamilyMemberFatherOrCaretakerRequiredException() {
|
||||
super("error.msg.client.family.member.father.or.caretaker.required",
|
||||
"Either Father or a Caretaker is required as a family member before this client can be activated.");
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown on client activation when no active "Mother" family member has been recorded. Kept
|
||||
* separate from {@link ClientFamilyMemberFatherOrCaretakerRequiredException} so the two failure
|
||||
* reasons surface as distinct errors instead of one merged message.
|
||||
*/
|
||||
public class ClientFamilyMemberMotherRequiredException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientFamilyMemberMotherRequiredException() {
|
||||
super("error.msg.client.family.member.mother.required",
|
||||
"Mother is required as a family member before this client can be activated.");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ClientNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientNoPendingEditException(final Long clientId) {
|
||||
super("error.msg.client.no.pending.edit", "Client with identifier " + clientId + " has no edit pending approval", clientId);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -34,6 +34,7 @@ import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.Named;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface ClientMapper {
|
||||
@@ -65,12 +66,14 @@ public interface ClientMapper {
|
||||
@Mapping(target = "degreeQualification", source = "source", qualifiedByName = "degreeQualificationCode")
|
||||
@Mapping(target = "religion", source = "source", qualifiedByName = "religionCode")
|
||||
@Mapping(target = "socialCategory", source = "source", qualifiedByName = "socialCategoryCode")
|
||||
@Mapping(target = "pwdCategory", source = "source", qualifiedByName = "pwdCategoryCode")
|
||||
@Mapping(target = "occupationCategoryOptions", ignore = true)
|
||||
@Mapping(target = "occupationOptions", ignore = true)
|
||||
@Mapping(target = "educationLevelOptions", ignore = true)
|
||||
@Mapping(target = "degreeQualificationOptions", ignore = true)
|
||||
@Mapping(target = "religionOptions", ignore = true)
|
||||
@Mapping(target = "socialCategoryOptions", ignore = true)
|
||||
@Mapping(target = "pwdCategoryOptions", ignore = true)
|
||||
@Mapping(target = "legalForm", source = "source", qualifiedByName = "clientLegalFormEnum")
|
||||
@Mapping(target = "isStaff", source = "source", qualifiedByName = "clientIsStaff")
|
||||
@Mapping(target = "ageYears", ignore = true)
|
||||
@@ -196,6 +199,15 @@ public interface ClientMapper {
|
||||
return CodeValueData.instance(code.getId(), code.getLabel());
|
||||
}
|
||||
|
||||
@Named("pwdCategoryCode")
|
||||
default CodeValueData pwdCategoryCode(Client client) {
|
||||
final CodeValue code = client.getPwdCategory();
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
return CodeValueData.instance(code.getId(), code.getLabel());
|
||||
}
|
||||
|
||||
@Named("clientSubStatusCode")
|
||||
default CodeValueData clientSubStatusCode(Client client) {
|
||||
final CodeValue code = client.getSubStatus();
|
||||
@@ -271,7 +283,7 @@ public interface ClientMapper {
|
||||
default void computeAge(Client source, @MappingTarget ClientData target) {
|
||||
final LocalDate dob = source.getDateOfBirth();
|
||||
if (dob != null) {
|
||||
final Period age = Period.between(dob, LocalDate.now());
|
||||
final Period age = Period.between(dob, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
target.setAgeYears(age.getYears());
|
||||
target.setAgeMonths(age.getMonths());
|
||||
target.setAgeDays(age.getDays());
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Generates CIF numbers in the format <code>TTYYYYNNNNNN</code>:
|
||||
@@ -90,6 +91,6 @@ public class CifNumberGenerator {
|
||||
|
||||
private int resolveYear(final Client client) {
|
||||
final LocalDate submittedOn = client.getSubmittedOnDate();
|
||||
return (submittedOn != null ? submittedOn : LocalDate.now()).getYear();
|
||||
return (submittedOn != null ? submittedOn : LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).getYear();
|
||||
}
|
||||
}
|
||||
|
||||
+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.client.service;
|
||||
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
|
||||
/**
|
||||
* Notifies a client (SMS + email) whenever their mobileNo, emailAddress, or address changes. Called
|
||||
* from {@code ClientWritePlatformServiceJpaRepositoryImpl.updateClient()} (after a successful
|
||||
* mobileNo/emailAddress save) and from {@code AddressWritePlatformServiceImpl.updateClientAddress()}
|
||||
* (after a successful address save, notification-only — address changes are not gated by OTP).
|
||||
*/
|
||||
public interface ClientChangeNotificationService {
|
||||
|
||||
String FIELD_LABEL_MOBILE_NO = "Mobile Number";
|
||||
String FIELD_LABEL_EMAIL_ADDRESS = "Email Address";
|
||||
String FIELD_LABEL_ADDRESS = "Address";
|
||||
|
||||
/**
|
||||
* @param client the client whose contact detail changed (already saved with the NEW value at
|
||||
* the time this is called)
|
||||
* @param fieldLabel one of {@link #FIELD_LABEL_MOBILE_NO}, {@link #FIELD_LABEL_EMAIL_ADDRESS}, or
|
||||
* {@link #FIELD_LABEL_ADDRESS} — use these exact constants so the implementation's
|
||||
* "notify both old and new email owner" special case is triggered correctly.
|
||||
* @param oldValue the value being replaced (raw, unmasked — the implementation masks contact
|
||||
* destinations for display and uses the raw value to decide where to send)
|
||||
* @param newValue the value it was changed to (raw, unmasked)
|
||||
* @param emailAddressBeforeUpdate the client's email address as it was BEFORE this update
|
||||
* transaction touched anything — used (for the mobile/address case) as the
|
||||
* notification recipient instead of {@code client.getEmailAddress()}. This
|
||||
* matters when mobile and email are both changed in the same request: by the
|
||||
* time this is called the in-memory client already carries the NEW email, so
|
||||
* reading it directly would send the "Mobile Number Updated" alert to the new
|
||||
* (unverified-by-the-real-owner-until-now) address instead of the address the
|
||||
* customer actually still controls. Ignored for the
|
||||
* {@link #FIELD_LABEL_EMAIL_ADDRESS} case, which already always notifies
|
||||
* {@code oldValue} directly.
|
||||
*/
|
||||
void notifyContactOrAddressChanged(Client client, String fieldLabel, String oldValue, String newValue,
|
||||
String emailAddressBeforeUpdate);
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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.client.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.email.service.EmailSenderService;
|
||||
import org.apache.fineract.infrastructure.email.service.EmailTemplateService;
|
||||
import org.apache.fineract.infrastructure.security.otp.util.OtpDestinationMasker;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* See {@link ClientChangeNotificationService}.
|
||||
*
|
||||
* <p>
|
||||
* Email uses {@link EmailTemplateService#renderAlert(String, String, String)} (the existing generic
|
||||
* branded "alert" template — not the OTP one) sent via {@link EmailSenderService}, the same
|
||||
* lightweight direct-SMTP path (configured from this app's own {@code spring.mail.*} properties)
|
||||
* already used by {@code EmailLoginOtpServiceImpl} for OTP emails. Deliberately does NOT use
|
||||
* {@code PlatformEmailService} / {@code GmailBackedPlatformEmailService} — that path resolves SMTP
|
||||
* credentials from {@code ExternalServicesPropertiesReadPlatformService} (a DB-configured "external
|
||||
* services" table used by the campaign-email pipeline), which is a separate, likely-unconfigured
|
||||
* configuration surface in this deployment. See class Javadoc discussion in the feature plan.
|
||||
*
|
||||
* <p>
|
||||
* SMS is a deliberate no-op-with-warning-log: LincPay's confirmed API surface in this codebase
|
||||
* ({@code fineract.security.otp.lincpay.generate-path=/mobile/generate-otp} and
|
||||
* {@code .../verify-path}) only exposes OTP generate/verify, not a plain "send this exact message"
|
||||
* SMS endpoint. Routing a notification through the OTP-generate endpoint would create a real,
|
||||
* verifiable OTP the client never asked to receive, which is worse than sending nothing. See
|
||||
* {@link #sendSmsNotification(Client, String, String, String)}.
|
||||
*/
|
||||
@Service
|
||||
public class ClientChangeNotificationServiceImpl implements ClientChangeNotificationService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientChangeNotificationServiceImpl.class);
|
||||
|
||||
private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm");
|
||||
|
||||
@Value("${fineract.email.from-name:CBSnexora}")
|
||||
private String appName;
|
||||
|
||||
private final EmailSenderService emailSenderService;
|
||||
private final EmailTemplateService emailTemplateService;
|
||||
|
||||
@Autowired
|
||||
public ClientChangeNotificationServiceImpl(final EmailSenderService emailSenderService,
|
||||
final EmailTemplateService emailTemplateService) {
|
||||
this.emailSenderService = emailSenderService;
|
||||
this.emailTemplateService = emailTemplateService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyContactOrAddressChanged(final Client client, final String fieldLabel, final String oldValue,
|
||||
final String newValue, final String emailAddressBeforeUpdate) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
sendEmailNotification(client, fieldLabel, oldValue, newValue, emailAddressBeforeUpdate);
|
||||
sendSmsNotification(client, fieldLabel, oldValue, newValue);
|
||||
}
|
||||
|
||||
private void sendEmailNotification(final Client client, final String fieldLabel, final String oldValue, final String newValue,
|
||||
final String emailAddressBeforeUpdate) {
|
||||
final String recipientName = displayName(client);
|
||||
final String alertTitle = fieldLabel + " Updated";
|
||||
final String alertMessage = "Your " + fieldLabel.toLowerCase() + " on file was changed from "
|
||||
+ describeForDisplay(fieldLabel, oldValue) + " to " + describeForDisplay(fieldLabel, newValue) + " on "
|
||||
+ LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(TIMESTAMP) + ". If you did not authorize this change, please contact us immediately.";
|
||||
final String html = emailTemplateService.renderAlert(recipientName, alertTitle, alertMessage);
|
||||
final String subject = alertTitle + " - " + appName;
|
||||
|
||||
if (FIELD_LABEL_EMAIL_ADDRESS.equals(fieldLabel)) {
|
||||
// When the email address itself is what changed, alert only the OLD address — the new
|
||||
// address already proved ownership via the dual-OTP verification step at change time, so a
|
||||
// duplicate confirmation there is unnecessary; the security-relevant alert is to the
|
||||
// previous owner, in case the change wasn't authorized by them.
|
||||
sendIfPresent(oldValue, subject, html);
|
||||
} else {
|
||||
// Mobile-number or address change: always alert the email the customer had BEFORE this
|
||||
// update, never `client.getEmailAddress()` — if email was also changed in the very same
|
||||
// request, the in-memory client already carries the NEW address by the time this runs, and
|
||||
// sending "Mobile Number Updated" there would tell nobody but the (possibly unauthorized)
|
||||
// new owner. The old address is the one the real customer is guaranteed to still read.
|
||||
sendIfPresent(emailAddressBeforeUpdate, subject, html);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendIfPresent(final String to, final String subject, final String html) {
|
||||
if (StringUtils.isBlank(to)) {
|
||||
LOG.warn("Change-notification email skipped — no recipient address available.");
|
||||
return;
|
||||
}
|
||||
emailSenderService.sendHtml(to, subject, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS notification — see class Javadoc. Left as a clearly-logged no-op pending a confirmed
|
||||
* LincPay plain-SMS endpoint; deliberately does NOT call the OTP-generate endpoint as a
|
||||
* workaround.
|
||||
*/
|
||||
private void sendSmsNotification(final Client client, final String fieldLabel, final String oldValue, final String newValue) {
|
||||
final String mobileNo = client.getMobileNo();
|
||||
if (StringUtils.isBlank(mobileNo)) {
|
||||
LOG.warn("Change-notification SMS skipped — client {} has no registered mobile number.", client.getId());
|
||||
return;
|
||||
}
|
||||
LOG.warn(
|
||||
"SMS change-notification NOT sent for client {} (field='{}') — no confirmed LincPay plain-SMS endpoint exists yet "
|
||||
+ "(only /mobile/generate-otp and /verify-mobile-otp are configured). Falling back to email-only "
|
||||
+ "notification. See ClientChangeNotificationServiceImpl Javadoc / TODO. mobile={}",
|
||||
client.getId(), fieldLabel, OtpDestinationMasker.maskMobile(mobileNo));
|
||||
}
|
||||
|
||||
private String describeForDisplay(final String fieldLabel, final String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return "(not set)";
|
||||
}
|
||||
if (FIELD_LABEL_MOBILE_NO.equals(fieldLabel)) {
|
||||
return OtpDestinationMasker.maskMobile(value);
|
||||
}
|
||||
if (FIELD_LABEL_EMAIL_ADDRESS.equals(fieldLabel)) {
|
||||
return OtpDestinationMasker.maskEmail(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String displayName(final Client client) {
|
||||
if (StringUtils.isNotBlank(client.getFirstname())) {
|
||||
return client.getFirstname();
|
||||
}
|
||||
if (StringUtils.isNotBlank(client.getDisplayName())) {
|
||||
return client.getDisplayName();
|
||||
}
|
||||
return "Customer";
|
||||
}
|
||||
}
|
||||
+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.client.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
/** Maker-checker for Client edits — see {@link org.apache.fineract.portfolio.client.domain.ClientPendingEdit}. */
|
||||
public interface ClientEditWritePlatformService {
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long clientId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long clientId, String rejectReason);
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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.client.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientPendingEdit;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientPendingEditRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientEditSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ClientEditWritePlatformServiceImpl implements ClientEditWritePlatformService {
|
||||
|
||||
/** Matches ORIGINAL_VALUES_PAYLOAD_KEY in client-edit-diff.util.ts. */
|
||||
private static final String ORIGINAL_VALUES_PAYLOAD_KEY = "_originalValues";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRepositoryWrapper clientRepository;
|
||||
private final ClientPendingEditRepositoryWrapper pendingEditRepository;
|
||||
private final ClientWritePlatformService clientWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long clientId, final JsonCommand command) {
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
// Four-eyes lock: refuse a second maker while one edit is already awaiting approval for
|
||||
// this client, rather than silently overwriting or queueing it — matches
|
||||
// m_share_account_pending_edit / m_savings_product_temp elsewhere in this codebase.
|
||||
if (this.pendingEditRepository.findPendingByClientId(clientId).isPresent()) {
|
||||
throw new ClientEditPendingApprovalException(clientId);
|
||||
}
|
||||
|
||||
// The frontend rides its point-in-time "before" snapshot along under _originalValues so
|
||||
// the checker's compare view can show a true historical diff later, even after the edit
|
||||
// has been applied (by then the live client equals the proposed values, not the original
|
||||
// ones — see client-edit-diff.util.ts). Split it out here: the rest replays through
|
||||
// updateClient() at approval time and must not carry an unrecognised parameter.
|
||||
final JsonElement parsedPayload = this.fromApiJsonHelper.parse(command.json());
|
||||
String originalJson = null;
|
||||
String cleanPayloadJson = command.json();
|
||||
if (parsedPayload != null && parsedPayload.isJsonObject()) {
|
||||
final JsonObject payloadObject = parsedPayload.getAsJsonObject();
|
||||
final JsonElement originalValues = payloadObject.remove(ORIGINAL_VALUES_PAYLOAD_KEY);
|
||||
if (originalValues != null) {
|
||||
originalJson = originalValues.toString();
|
||||
}
|
||||
cleanPayloadJson = payloadObject.toString();
|
||||
}
|
||||
|
||||
final ClientPendingEdit pendingEdit = ClientPendingEdit.submit(client, cleanPayloadJson, originalJson, maker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withOfficeId(client.officeId()) //
|
||||
.withClientId(clientId) //
|
||||
.withEntityId(pendingEdit.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long clientId) {
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ClientPendingEdit pendingEdit = this.pendingEditRepository.findPendingByClientIdWithNotFoundDetection(clientId);
|
||||
|
||||
// No bypass — including CHECKER_SUPER_USER — a bypass here would recreate the exact
|
||||
// "role bypasses the hold" loophole this whole feature exists to close.
|
||||
if (pendingEdit.isMadeBy(checker.getId())) {
|
||||
throw new ClientEditSelfApprovalNotAllowedException(clientId);
|
||||
}
|
||||
|
||||
// Replay the staged JSON through the SAME write-service method a direct PUT would have
|
||||
// used, so every bit of Client's existing validation/change-detection logic runs exactly
|
||||
// as it would for an immediate edit — just deferred until a different user approves it.
|
||||
final JsonElement parsedJson = this.fromApiJsonHelper.parse(pendingEdit.getPayloadJson());
|
||||
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(clientId, parsedJson, this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = this.clientWritePlatformService.updateClient(clientId, replayedCommand);
|
||||
|
||||
pendingEdit.approve(checker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long clientId, final String rejectReason) {
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ClientPendingEdit pendingEdit = this.pendingEditRepository.findPendingByClientIdWithNotFoundDetection(clientId);
|
||||
|
||||
if (pendingEdit.isMadeBy(checker.getId())) {
|
||||
throw new ClientEditSelfApprovalNotAllowedException(clientId);
|
||||
}
|
||||
|
||||
pendingEdit.reject(checker, rejectReason);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withClientId(clientId) //
|
||||
.withEntityId(pendingEdit.getId()) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+8
@@ -33,4 +33,12 @@ public interface ClientFamilyMembersWritePlatformService {
|
||||
|
||||
CommandProcessingResult deleteFamilyMember(Long familyMemberId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Enforces that an individual client has "Mother" plus either "Father" or "Caretaker" recorded
|
||||
* as active family members. No-op for non-individual (entity) clients. Throws
|
||||
* {@code ClientFamilyMemberMotherRequiredException} or
|
||||
* {@code ClientFamilyMemberFatherOrCaretakerRequiredException} on failure.
|
||||
*/
|
||||
void validateFamilyRequirementsForActivation(Client client);
|
||||
|
||||
}
|
||||
|
||||
+44
@@ -37,10 +37,13 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientFamilyMembers;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientFamilyMembersRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberFatherOrCaretakerRequiredException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberMotherRequiredException;
|
||||
import org.apache.fineract.portfolio.client.serialization.ClientFamilyMemberCommandFromApiJsonDeserializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -131,6 +134,47 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateFamilyRequirementsForActivation(final Client client) {
|
||||
if (client == null || client.getId() == null || client.getLegalForm() == null) {
|
||||
return;
|
||||
}
|
||||
// Family (Mother/Father/Caretaker) is an individual-client concept — see
|
||||
// assertCanHaveFamilyMembers above; non-individual entities are exempt from this gate.
|
||||
if (org.apache.fineract.portfolio.client.domain.LegalForm.fromInt(client.getLegalForm()).isEntity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ClientFamilyMembers> members = clientFamilyRepository.findAllActiveByClient(client.getId());
|
||||
boolean hasMother = false;
|
||||
boolean hasFather = false;
|
||||
boolean hasCaretaker = false;
|
||||
for (final ClientFamilyMembers member : members) {
|
||||
final CodeValue relationship = member.getRelationship();
|
||||
if (relationship == null || relationship.getLabel() == null) {
|
||||
continue;
|
||||
}
|
||||
final String label = relationship.getLabel().trim().toLowerCase(Locale.ROOT);
|
||||
switch (label) {
|
||||
case "mother" -> hasMother = true;
|
||||
case "father" -> hasFather = true;
|
||||
case "caretaker" -> hasCaretaker = true;
|
||||
default -> {
|
||||
// other relationships don't affect this rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reported as two independent failures rather than one merged message, so the caller can
|
||||
// tell which specific requirement is unmet.
|
||||
if (!hasMother) {
|
||||
throw new ClientFamilyMemberMotherRequiredException();
|
||||
}
|
||||
if (!hasFather && !hasCaretaker) {
|
||||
throw new ClientFamilyMemberFatherOrCaretakerRequiredException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult addFamilyMember(final long clientId, final JsonCommand command) {
|
||||
|
||||
|
||||
+7
-3
@@ -84,9 +84,13 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
this.context.authenticatedUser();
|
||||
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
|
||||
|
||||
final Client clientForValidation = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||
kycCommand.validateForInitiate();
|
||||
// Minors identify via the Birth Certificate / School ID document-type picker (a separate
|
||||
// upload path), not PAN/Aadhaar/GSTIN — so the "at least one ID" check doesn't apply to them.
|
||||
kycCommand.validateForInitiate(Boolean.TRUE.equals(clientForValidation.getIsMinor()));
|
||||
|
||||
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
||||
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
|
||||
@@ -104,7 +108,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
}
|
||||
|
||||
try {
|
||||
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
final Client client = clientForValidation;
|
||||
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
|
||||
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||
|
||||
@@ -957,7 +961,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
public void initiateKycInline(final Client client, final String kycJson) {
|
||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||
kycCommand.validateForInitiate();
|
||||
kycCommand.validateForInitiate(Boolean.TRUE.equals(client.getIsMinor()));
|
||||
|
||||
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
|
||||
LOG.info("KYC record already exists for client {}. Skipping inline initiation.", client.getId());
|
||||
|
||||
+14
-1
@@ -44,6 +44,8 @@ import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -62,6 +64,7 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
private final ClientNomineeRepository nomineeRepository;
|
||||
private final CodeValueRepositoryWrapper codeValueRepository;
|
||||
private final SavingsAccountRepository savingsAccountRepository;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final LoanRepository loanRepository;
|
||||
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
|
||||
|
||||
@@ -299,8 +302,18 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.loan.account.not.found",
|
||||
"Loan account '" + accountNo + "' not found for client " + clientId + ".", "accountNo");
|
||||
}
|
||||
} else if ("CB".equals(accountType)) {
|
||||
final CurrentAccount ca = this.currentAccountRepository.findByAccountNo(accountNo).orElse(null);
|
||||
if (ca == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.current.account.not.found",
|
||||
"Account '" + accountNo + "' not found.", "accountNo");
|
||||
}
|
||||
if (ca.getClient() == null || !ca.getClient().getId().equals(clientId)) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.account.does.not.belong.to.client",
|
||||
"Account '" + accountNo + "' does not belong to client " + clientId + ".", "accountNo");
|
||||
}
|
||||
} else {
|
||||
// SB, CB, FD, RD are all savings accounts with different deposit_type_enum
|
||||
// SB, FD, RD are all savings accounts with different deposit_type_enum
|
||||
final SavingsAccount sa = savingsAccountRepository.findSavingsAccountByAccountNumber(accountNo);
|
||||
if (sa == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.savings.account.not.found",
|
||||
|
||||
+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.client.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
|
||||
public interface ClientPendingEditReadPlatformService {
|
||||
|
||||
ClientPendingEditData retrievePendingEdit(Long clientId);
|
||||
|
||||
/**
|
||||
* @param status defaults to PENDING when blank
|
||||
* @param clientName case-insensitive substring match against the client's display name
|
||||
* @param fromDate ISO (yyyy-MM-dd), inclusive, filters on submitted_date
|
||||
* @param toDate ISO (yyyy-MM-dd), inclusive, filters on submitted_date
|
||||
*/
|
||||
Page<ClientPendingEditData> retrieveAllPendingEdits(String status, String clientName, String fromDate, String toDate, Integer offset,
|
||||
Integer limit);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNoPendingEditException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ClientPendingEditReadPlatformServiceImpl implements ClientPendingEditReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final String SELECT_SQL = """
|
||||
SELECT pe.id AS id, pe.client_id AS clientId, c.display_name AS clientDisplayName,
|
||||
pe.payload_json AS payloadJson, pe.original_json AS originalJson, pe.status AS status,
|
||||
pe.maker_id AS makerId, (maker.firstname || ' ' || maker.lastname) AS makerName,
|
||||
pe.submitted_date AS submittedDate,
|
||||
pe.checker_id AS checkerId, (checker.firstname || ' ' || checker.lastname) AS checkerName,
|
||||
pe.checked_date AS checkedDate, pe.reject_reason AS rejectReason
|
||||
FROM m_client_pending_edit pe
|
||||
JOIN m_client c ON c.id = pe.client_id
|
||||
JOIN m_appuser maker ON maker.id = pe.maker_id
|
||||
LEFT JOIN m_appuser checker ON checker.id = pe.checker_id
|
||||
""";
|
||||
|
||||
private static final RowMapper<ClientPendingEditData> MAPPER = (ResultSet rs, int rowNum) -> {
|
||||
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||
return ClientPendingEditData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.clientId(rs.getLong("clientId")) //
|
||||
.clientDisplayName(rs.getString("clientDisplayName")) //
|
||||
.payloadJson(rs.getString("payloadJson")) //
|
||||
.originalJson(rs.getString("originalJson")) //
|
||||
.status(rs.getString("status")) //
|
||||
.makerId(rs.getLong("makerId")) //
|
||||
.makerName(rs.getString("makerName")) //
|
||||
.submittedDate(submittedDate) //
|
||||
.checkerId(JdbcSupport.getLong(rs, "checkerId")) //
|
||||
.checkerName(rs.getString("checkerName")) //
|
||||
.checkedDate(checkedDate) //
|
||||
.rejectReason(rs.getString("rejectReason")) //
|
||||
.build();
|
||||
};
|
||||
|
||||
@Override
|
||||
public ClientPendingEditData retrievePendingEdit(final Long clientId) {
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(SELECT_SQL + " WHERE pe.client_id = ? AND pe.status = 'PENDING'", MAPPER, clientId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ClientNoPendingEditException(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ClientPendingEditData> retrieveAllPendingEdits(final String status, final String clientName, final String fromDate,
|
||||
final String toDate, final Integer offset, final Integer limit) {
|
||||
final StringBuilder where = new StringBuilder(" WHERE 1=1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
where.append(" AND pe.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
} else {
|
||||
where.append(" AND pe.status = 'PENDING'");
|
||||
}
|
||||
if (StringUtils.isNotBlank(clientName)) {
|
||||
where.append(" AND c.display_name ILIKE ?");
|
||||
args.add("%" + clientName.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(fromDate)) {
|
||||
where.append(" AND pe.submitted_date::date >= ?::date");
|
||||
args.add(fromDate);
|
||||
}
|
||||
if (StringUtils.isNotBlank(toDate)) {
|
||||
where.append(" AND pe.submitted_date::date <= ?::date");
|
||||
args.add(toDate);
|
||||
}
|
||||
|
||||
final String countSql = "SELECT COUNT(*) FROM m_client_pending_edit pe JOIN m_client c ON c.id = pe.client_id" + where;
|
||||
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final int pageOffset = offset == null ? 0 : offset;
|
||||
final int pageLimit = limit == null ? 10 : limit;
|
||||
final String pageSql = SELECT_SQL + where + " ORDER BY pe.submitted_date DESC LIMIT ? OFFSET ?";
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(pageLimit);
|
||||
pageArgs.add(pageOffset);
|
||||
final List<ClientPendingEditData> items = this.jdbcTemplate.query(pageSql, MAPPER, pageArgs.toArray());
|
||||
|
||||
return new Page<>(items, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||
}
|
||||
}
|
||||
+51
-2
@@ -63,6 +63,7 @@ import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -177,6 +178,16 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
extraCriteria += " and c.status_enum = " + clientStatus.getValue().toString() + " ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasFromDate()) {
|
||||
paramList.add(searchParameters.getFromDate().toString());
|
||||
extraCriteria += " and c.submittedon_date >= ?::date ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasToDate()) {
|
||||
paramList.add(searchParameters.getToDate().toString());
|
||||
extraCriteria += " and c.submittedon_date <= ?::date ";
|
||||
}
|
||||
|
||||
if (firstname != null) {
|
||||
paramList.add(firstname);
|
||||
extraCriteria += " and c.firstname like ? ";
|
||||
@@ -321,7 +332,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
sqlBuilder.append("c.occupation_cv_id as occupationId, ");
|
||||
sqlBuilder.append("cvOccupation.code_value as occupationValue, ");
|
||||
sqlBuilder.append("c.monthly_income as monthlyIncome, ");
|
||||
sqlBuilder.append("c.annual_income as annualIncome, ");
|
||||
sqlBuilder.append("c.annual_turnover as annualTurnover, ");
|
||||
sqlBuilder.append("c.monthly_turnover as monthlyTurnover, ");
|
||||
sqlBuilder.append("c.is_senior_citizen as isSeniorCitizen, ");
|
||||
sqlBuilder.append("c.is_pwd as isPwd, ");
|
||||
sqlBuilder.append("c.pwd_category_cv_id as pwdCategoryId, ");
|
||||
sqlBuilder.append("cvPwdCategory.code_value as pwdCategoryValue, ");
|
||||
sqlBuilder.append("c.nationality as nationality, ");
|
||||
sqlBuilder.append("c.is_minor as isMinor, ");
|
||||
sqlBuilder.append("c.guardian_client_id as guardianClientId, ");
|
||||
@@ -383,6 +400,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
sqlBuilder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id ");
|
||||
sqlBuilder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status ");
|
||||
sqlBuilder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id ");
|
||||
sqlBuilder.append("left join m_code_value cvPwdCategory on cvPwdCategory.id = c.pwd_category_cv_id ");
|
||||
sqlBuilder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id ");
|
||||
sqlBuilder.append("left join m_client gc on gc.id = c.guardian_client_id ");
|
||||
sqlBuilder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id ");
|
||||
@@ -482,13 +500,20 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue);
|
||||
|
||||
final BigDecimal monthlyIncome1 = rs.getBigDecimal("monthlyIncome");
|
||||
final BigDecimal annualIncome1 = rs.getBigDecimal("annualIncome");
|
||||
final BigDecimal annualTurnover1 = rs.getBigDecimal("annualTurnover");
|
||||
final BigDecimal monthlyTurnover1 = rs.getBigDecimal("monthlyTurnover");
|
||||
final Boolean isSeniorCitizen1 = rs.getBoolean("isSeniorCitizen");
|
||||
final Boolean isPwd1 = rs.getBoolean("isPwd");
|
||||
final Long pwdCategoryId1 = JdbcSupport.getLong(rs, "pwdCategoryId");
|
||||
final String pwdCategoryValue1 = rs.getString("pwdCategoryValue");
|
||||
final CodeValueData pwdCategory1 = CodeValueData.instance(pwdCategoryId1, pwdCategoryValue1);
|
||||
|
||||
Integer ageYears1 = null;
|
||||
Integer ageMonths1 = null;
|
||||
Integer ageDays1 = null;
|
||||
if (dateOfBirth != null) {
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now());
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
ageYears1 = age.getYears();
|
||||
ageMonths1 = age.getMonths();
|
||||
ageDays1 = age.getDays();
|
||||
@@ -520,7 +545,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm,
|
||||
clientNonPerson, isStaff);
|
||||
clientData.setMonthlyIncome(monthlyIncome1);
|
||||
clientData.setAnnualIncome(annualIncome1);
|
||||
clientData.setAnnualTurnover(annualTurnover1);
|
||||
clientData.setMonthlyTurnover(monthlyTurnover1);
|
||||
clientData.setIsSeniorCitizen(isSeniorCitizen1);
|
||||
clientData.setIsPwd(isPwd1);
|
||||
clientData.setPwdCategory(pwdCategory1);
|
||||
clientData.setAgeYears(ageYears1);
|
||||
clientData.setAgeMonths(ageMonths1);
|
||||
clientData.setAgeDays(ageDays1);
|
||||
@@ -714,7 +744,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
builder.append("c.occupation_cv_id as occupationId, ");
|
||||
builder.append("cvOccupation.code_value as occupationValue, ");
|
||||
builder.append("c.monthly_income as monthlyIncome, ");
|
||||
builder.append("c.annual_income as annualIncome, ");
|
||||
builder.append("c.annual_turnover as annualTurnover, ");
|
||||
builder.append("c.monthly_turnover as monthlyTurnover, ");
|
||||
builder.append("c.is_senior_citizen as isSeniorCitizen, ");
|
||||
builder.append("c.is_pwd as isPwd, ");
|
||||
builder.append("c.pwd_category_cv_id as pwdCategoryId, ");
|
||||
builder.append("cvPwdCategory.code_value as pwdCategoryValue, ");
|
||||
builder.append("c.nationality as nationality, ");
|
||||
builder.append("c.is_minor as isMinor, ");
|
||||
builder.append("c.guardian_client_id as guardianClientId, ");
|
||||
@@ -775,6 +811,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
builder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id ");
|
||||
builder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status ");
|
||||
builder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id ");
|
||||
builder.append("left join m_code_value cvPwdCategory on cvPwdCategory.id = c.pwd_category_cv_id ");
|
||||
builder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id ");
|
||||
builder.append("left join m_client gc on gc.id = c.guardian_client_id ");
|
||||
builder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id ");
|
||||
@@ -873,13 +910,20 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue);
|
||||
|
||||
final BigDecimal monthlyIncome = rs.getBigDecimal("monthlyIncome");
|
||||
final BigDecimal annualIncome = rs.getBigDecimal("annualIncome");
|
||||
final BigDecimal annualTurnover = rs.getBigDecimal("annualTurnover");
|
||||
final BigDecimal monthlyTurnover = rs.getBigDecimal("monthlyTurnover");
|
||||
final Boolean isSeniorCitizen = rs.getBoolean("isSeniorCitizen");
|
||||
final Boolean isPwd = rs.getBoolean("isPwd");
|
||||
final Long pwdCategoryId = JdbcSupport.getLong(rs, "pwdCategoryId");
|
||||
final String pwdCategoryValue = rs.getString("pwdCategoryValue");
|
||||
final CodeValueData pwdCategory = CodeValueData.instance(pwdCategoryId, pwdCategoryValue);
|
||||
|
||||
Integer ageYears = null;
|
||||
Integer ageMonths = null;
|
||||
Integer ageDays = null;
|
||||
if (dateOfBirth != null) {
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now());
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
ageYears = age.getYears();
|
||||
ageMonths = age.getMonths();
|
||||
ageDays = age.getDays();
|
||||
@@ -911,7 +955,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm,
|
||||
clientNonPerson, isStaff);
|
||||
clientData.setMonthlyIncome(monthlyIncome);
|
||||
clientData.setAnnualIncome(annualIncome);
|
||||
clientData.setAnnualTurnover(annualTurnover);
|
||||
clientData.setMonthlyTurnover(monthlyTurnover);
|
||||
clientData.setIsSeniorCitizen(isSeniorCitizen);
|
||||
clientData.setIsPwd(isPwd);
|
||||
clientData.setPwdCategory(pwdCategory);
|
||||
clientData.setAgeYears(ageYears);
|
||||
clientData.setAgeMonths(ageMonths);
|
||||
clientData.setAgeDays(ageDays);
|
||||
|
||||
+4
@@ -126,6 +126,9 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead
|
||||
final List<CodeValueData> socialCategoryOptions = new ArrayList<>(
|
||||
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.SOCIAL_CATEGORY));
|
||||
|
||||
final List<CodeValueData> pwdCategoryOptions = new ArrayList<>(
|
||||
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.PWD_CATEGORY));
|
||||
|
||||
final List<CodeValueData> clientNonPersonConstitutionOptions = new ArrayList<>(
|
||||
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.CLIENT_NON_PERSON_CONSTITUTION));
|
||||
|
||||
@@ -146,6 +149,7 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead
|
||||
templateData.setDegreeQualificationOptions(degreeQualificationOptions);
|
||||
templateData.setReligionOptions(religionOptions);
|
||||
templateData.setSocialCategoryOptions(socialCategoryOptions);
|
||||
templateData.setPwdCategoryOptions(pwdCategoryOptions);
|
||||
return templateData;
|
||||
}
|
||||
|
||||
|
||||
+165
@@ -63,6 +63,7 @@ import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
|
||||
import org.apache.fineract.portfolio.address.service.AddressWritePlatformService;
|
||||
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
|
||||
import org.apache.fineract.portfolio.client.contactverification.service.ClientContactOtpService;
|
||||
import org.apache.fineract.portfolio.client.data.ClientDataValidator;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
|
||||
@@ -73,6 +74,7 @@ import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientStatus;
|
||||
import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientActivationSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientActiveForUpdateException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientHasNoStaffException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientMustBePendingToBeDeletedException;
|
||||
@@ -132,6 +134,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository;
|
||||
private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository;
|
||||
private final CifNumberGenerator cifNumberGenerator;
|
||||
private final ClientContactOtpService clientContactOtpService;
|
||||
private final ClientChangeNotificationService clientChangeNotificationService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -392,6 +396,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.SOCIAL_CATEGORY, socialCategoryId);
|
||||
}
|
||||
|
||||
CodeValue pwdCategory = null;
|
||||
final Long pwdCategoryId = command.longValueOfParameterNamed(ClientApiConstants.pwdCategoryIdParamName);
|
||||
if (pwdCategoryId != null) {
|
||||
pwdCategory = this.codeValueRepository
|
||||
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.PWD_CATEGORY, pwdCategoryId);
|
||||
}
|
||||
|
||||
final Long savingsProductId = command.longValueOfParameterNamed(ClientApiConstants.savingsProductIdParamName);
|
||||
if (savingsProductId != null) {
|
||||
this.savingsProductRepository.findById(savingsProductId)
|
||||
@@ -416,6 +427,22 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
final String mobileNo = command.stringValueOfParameterNamed(ClientApiConstants.mobileNoParamName);
|
||||
final String alternateMobileNo = command.stringValueOfParameterNamed(ClientApiConstants.alternateMobileNoParamName);
|
||||
final String emailAddress = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName);
|
||||
|
||||
// A wrong/unreachable mobileNo or emailAddress entered at creation can never be fixed
|
||||
// later — editing it requires a verified OTP against the OLD (wrong) value, which is
|
||||
// impossible if that value was never real. Verifying here (single "new" leg — there's no
|
||||
// "old" value for a brand-new client) guarantees every value ever stored was reachable at
|
||||
// the moment it was recorded. See ClientDataValidator.validateForCreate() for the mandatory
|
||||
// otpId presence check.
|
||||
if (StringUtils.isNotBlank(mobileNo)) {
|
||||
final Long mobileNewOtpId = command.longValueOfParameterNamed(ClientApiConstants.mobileNewOtpIdParamName);
|
||||
this.clientContactOtpService.assertVerifiedMobile(mobileNewOtpId, mobileNo, ClientApiConstants.mobileNewOtpIdParamName);
|
||||
}
|
||||
if (StringUtils.isNotBlank(emailAddress)) {
|
||||
final Long emailNewOtpId = command.longValueOfParameterNamed(ClientApiConstants.emailNewOtpIdParamName);
|
||||
this.clientContactOtpService.assertVerifiedEmail(emailNewOtpId, emailAddress, ClientApiConstants.emailNewOtpIdParamName);
|
||||
}
|
||||
|
||||
// Title-case names at the boundary so every persistence path (REST, internal,
|
||||
// bulk import) stores "Aditya Kumar Vyas" regardless of how the caller cased the
|
||||
// input. This is the single write seam for new clients — normalising here means we
|
||||
@@ -462,11 +489,31 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
newClient.setMonthlyIncome(monthlyIncome);
|
||||
}
|
||||
|
||||
final java.math.BigDecimal annualIncome = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualIncomeParamName);
|
||||
if (annualIncome != null) {
|
||||
newClient.setAnnualIncome(annualIncome);
|
||||
}
|
||||
|
||||
final java.math.BigDecimal annualTurnover = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName);
|
||||
if (annualTurnover != null) {
|
||||
newClient.setAnnualTurnover(annualTurnover);
|
||||
}
|
||||
|
||||
final java.math.BigDecimal monthlyTurnover = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyTurnoverParamName);
|
||||
if (monthlyTurnover != null) {
|
||||
newClient.setMonthlyTurnover(monthlyTurnover);
|
||||
}
|
||||
|
||||
final Boolean isSeniorCitizen = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isSeniorCitizenParamName);
|
||||
newClient.setIsSeniorCitizen(Boolean.TRUE.equals(isSeniorCitizen));
|
||||
|
||||
final Boolean isPwd = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isPwdParamName);
|
||||
newClient.setIsPwd(Boolean.TRUE.equals(isPwd));
|
||||
|
||||
if (pwdCategory != null) {
|
||||
newClient.updatePwdCategory(pwdCategory);
|
||||
}
|
||||
|
||||
final java.math.BigDecimal agricultureIncome = command.bigDecimalValueOfParameterNamed(ClientApiConstants.agricultureIncomeParamName);
|
||||
if (agricultureIncome != null) {
|
||||
newClient.setAgricultureIncome(agricultureIncome);
|
||||
@@ -724,6 +771,42 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
return element.toString().trim().length() < 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate a mobileNo/emailAddress change on dual OTP verification: both the OLD destination (the
|
||||
* client's current DB value, re-derived here — never trusted from client-supplied JSON) and the
|
||||
* NEW destination (the incoming command value) must each be backed by an already-VERIFIED
|
||||
* {@code m_otp_log} row of purpose {@code CLIENT_CONTACT_CHANGE}. {@code ClientDataValidator}
|
||||
* guarantees the relevant field-specific old/new otpId params are structurally present whenever
|
||||
* mobileNo/emailAddress appears in the request; this is the semantic check that they actually
|
||||
* prove ownership of the right destinations. Param pairs are field-specific
|
||||
* ({@code mobileOldOtpId}/{@code mobileNewOtpId}, {@code emailOldOtpId}/{@code emailNewOtpId})
|
||||
* rather than one generic pair, so mobileNo and emailAddress can both change in the same request
|
||||
* without ambiguity.
|
||||
*
|
||||
* <p>
|
||||
* The OLD leg is skipped when the client doesn't currently have a value for this field (e.g. a
|
||||
* client onboarded without a mobile number is having one set for the first time) — there is no
|
||||
* existing verified channel to prove possession of in that case.
|
||||
*/
|
||||
private void verifyDualOtpForContactChange(final JsonCommand command, final String changedParamName, final String oldValue,
|
||||
final String newValue) {
|
||||
if (ClientApiConstants.mobileNoParamName.equals(changedParamName)) {
|
||||
final Long mobileOldOtpId = command.longValueOfParameterNamed(ClientApiConstants.mobileOldOtpIdParamName);
|
||||
final Long mobileNewOtpId = command.longValueOfParameterNamed(ClientApiConstants.mobileNewOtpIdParamName);
|
||||
if (StringUtils.isNotBlank(oldValue)) {
|
||||
this.clientContactOtpService.assertVerifiedMobile(mobileOldOtpId, oldValue, ClientApiConstants.mobileOldOtpIdParamName);
|
||||
}
|
||||
this.clientContactOtpService.assertVerifiedMobile(mobileNewOtpId, newValue, ClientApiConstants.mobileNewOtpIdParamName);
|
||||
} else if (ClientApiConstants.emailAddressParamName.equals(changedParamName)) {
|
||||
final Long emailOldOtpId = command.longValueOfParameterNamed(ClientApiConstants.emailOldOtpIdParamName);
|
||||
final Long emailNewOtpId = command.longValueOfParameterNamed(ClientApiConstants.emailNewOtpIdParamName);
|
||||
if (StringUtils.isNotBlank(oldValue)) {
|
||||
this.clientContactOtpService.assertVerifiedEmail(emailOldOtpId, oldValue, ClientApiConstants.emailOldOtpIdParamName);
|
||||
}
|
||||
this.clientContactOtpService.assertVerifiedEmail(emailNewOtpId, newValue, ClientApiConstants.emailNewOtpIdParamName);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateClient(final Long clientId, final JsonCommand command) {
|
||||
@@ -761,10 +844,30 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
clientForUpdate.setExternalId(externalId);
|
||||
}
|
||||
|
||||
// Dual-OTP contact-change gate — see ClientContactOtpService / ClientDataValidator.
|
||||
// Track what actually changed here so we can fire the customer notification (SMS+email)
|
||||
// AFTER the row is successfully saved, further down this method.
|
||||
// Snapshotted BEFORE any field below is mutated — if mobile AND email are both changed in
|
||||
// this same request, the mobile-change alert must still reach this original address, not
|
||||
// whatever the client's email gets updated to a few lines down. See
|
||||
// ClientChangeNotificationService#notifyContactOrAddressChanged Javadoc.
|
||||
final String emailAddressBeforeUpdate = clientForUpdate.getEmailAddress();
|
||||
String oldMobileNoForNotification = null;
|
||||
String newMobileNoForNotification = null;
|
||||
boolean mobileNoChanged = false;
|
||||
String oldEmailAddressForNotification = null;
|
||||
String newEmailAddressForNotification = null;
|
||||
boolean emailAddressChanged = false;
|
||||
|
||||
if (command.isChangeInStringParameterNamed(ClientApiConstants.mobileNoParamName, clientForUpdate.getMobileNo())) {
|
||||
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.mobileNoParamName);
|
||||
final String oldValue = clientForUpdate.getMobileNo();
|
||||
verifyDualOtpForContactChange(command, ClientApiConstants.mobileNoParamName, oldValue, newValue);
|
||||
changes.put(ClientApiConstants.mobileNoParamName, newValue);
|
||||
clientForUpdate.setMobileNo(StringUtils.defaultIfEmpty(newValue, null));
|
||||
oldMobileNoForNotification = oldValue;
|
||||
newMobileNoForNotification = newValue;
|
||||
mobileNoChanged = true;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed(ClientApiConstants.alternateMobileNoParamName, clientForUpdate.getAlternateMobileNo())) {
|
||||
@@ -775,8 +878,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
|
||||
if (command.isChangeInStringParameterNamed(ClientApiConstants.emailAddressParamName, clientForUpdate.getEmailAddress())) {
|
||||
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName);
|
||||
final String oldValue = clientForUpdate.getEmailAddress();
|
||||
verifyDualOtpForContactChange(command, ClientApiConstants.emailAddressParamName, oldValue, newValue);
|
||||
changes.put(ClientApiConstants.emailAddressParamName, newValue);
|
||||
clientForUpdate.setEmailAddress(StringUtils.defaultIfEmpty(newValue, null));
|
||||
oldEmailAddressForNotification = oldValue;
|
||||
newEmailAddressForNotification = newValue;
|
||||
emailAddressChanged = true;
|
||||
}
|
||||
|
||||
// Title-case incoming names so an update with "ADITYA" or "aditya" still ends up
|
||||
@@ -884,12 +992,47 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
clientForUpdate.setMonthlyIncome(newValue);
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.annualIncomeParamName, clientForUpdate.getAnnualIncome())) {
|
||||
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualIncomeParamName);
|
||||
changes.put(ClientApiConstants.annualIncomeParamName, newValue);
|
||||
clientForUpdate.setAnnualIncome(newValue);
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.annualTurnoverParamName, clientForUpdate.getAnnualTurnover())) {
|
||||
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName);
|
||||
changes.put(ClientApiConstants.annualTurnoverParamName, newValue);
|
||||
clientForUpdate.setAnnualTurnover(newValue);
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.monthlyTurnoverParamName, clientForUpdate.getMonthlyTurnover())) {
|
||||
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyTurnoverParamName);
|
||||
changes.put(ClientApiConstants.monthlyTurnoverParamName, newValue);
|
||||
clientForUpdate.setMonthlyTurnover(newValue);
|
||||
}
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.isSeniorCitizenParamName, clientForUpdate.getIsSeniorCitizen())) {
|
||||
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isSeniorCitizenParamName);
|
||||
changes.put(ClientApiConstants.isSeniorCitizenParamName, newValue);
|
||||
clientForUpdate.setIsSeniorCitizen(Boolean.TRUE.equals(newValue));
|
||||
}
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.isPwdParamName, clientForUpdate.getIsPwd())) {
|
||||
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isPwdParamName);
|
||||
changes.put(ClientApiConstants.isPwdParamName, newValue);
|
||||
clientForUpdate.setIsPwd(Boolean.TRUE.equals(newValue));
|
||||
}
|
||||
|
||||
if (command.isChangeInLongParameterNamed(ClientApiConstants.pwdCategoryIdParamName, clientForUpdate.pwdCategoryId())) {
|
||||
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.pwdCategoryIdParamName);
|
||||
changes.put(ClientApiConstants.pwdCategoryIdParamName, newValue);
|
||||
CodeValue newCodeVal = null;
|
||||
if (newValue != null) {
|
||||
newCodeVal = this.codeValueRepository
|
||||
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.PWD_CATEGORY, newValue);
|
||||
}
|
||||
clientForUpdate.updatePwdCategory(newCodeVal);
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.agricultureIncomeParamName, clientForUpdate.getAgricultureIncome())) {
|
||||
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.agricultureIncomeParamName);
|
||||
changes.put(ClientApiConstants.agricultureIncomeParamName, newValue);
|
||||
@@ -1184,6 +1327,20 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
this.clientRepository.saveAndFlush(clientForUpdate);
|
||||
}
|
||||
|
||||
// Customer notification (SMS + email) — fired only after the row is durably saved, so we
|
||||
// never notify about a change that ultimately failed to persist. Address changes are
|
||||
// notified from AddressWritePlatformServiceImpl instead (no OTP gate there).
|
||||
if (mobileNoChanged) {
|
||||
this.clientChangeNotificationService.notifyContactOrAddressChanged(clientForUpdate,
|
||||
ClientChangeNotificationService.FIELD_LABEL_MOBILE_NO, oldMobileNoForNotification, newMobileNoForNotification,
|
||||
emailAddressBeforeUpdate);
|
||||
}
|
||||
if (emailAddressChanged) {
|
||||
this.clientChangeNotificationService.notifyContactOrAddressChanged(clientForUpdate,
|
||||
ClientChangeNotificationService.FIELD_LABEL_EMAIL_ADDRESS, oldEmailAddressForNotification,
|
||||
newEmailAddressForNotification, emailAddressBeforeUpdate);
|
||||
}
|
||||
|
||||
if (changes.containsKey(ClientApiConstants.legalFormIdParamName)) {
|
||||
Integer legalFormValue = clientForUpdate.getLegalForm();
|
||||
boolean isChangedToEntity = false;
|
||||
@@ -1293,6 +1450,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId, true);
|
||||
validateParentGroupRulesBeforeClientActivation(client);
|
||||
this.clientFamilyMembersWritePlatformService.validateFamilyRequirementsForActivation(client);
|
||||
final Locale locale = command.extractLocale();
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate");
|
||||
@@ -1300,6 +1458,12 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
runEntityDatatableCheck(clientId, client.getLegalForm());
|
||||
|
||||
final AppUser currentUser = this.context.authenticatedUser();
|
||||
// Four-eyes principle: whoever created this client (the maker) cannot also be the one
|
||||
// who activates it (the checker) — no bypass, including for CHECKER_SUPER_USER, since
|
||||
// that would recreate the exact "role bypasses the hold" loophole this closes.
|
||||
if (client.getCreatedBy().isPresent() && client.getCreatedBy().get().equals(currentUser.getId())) {
|
||||
throw new ClientActivationSelfApprovalNotAllowedException();
|
||||
}
|
||||
client.activate(currentUser, fmt, activationDate);
|
||||
CommandProcessingResult result = openSavingsAccount(client, fmt);
|
||||
clientRepository.saveAndFlush(client);
|
||||
@@ -1692,6 +1856,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
* verifying KYC names against ID documents). Null / blank → null so we don't accidentally
|
||||
* persist an empty string in place of an unset field.
|
||||
*/
|
||||
@SuppressWarnings("StringSplitter")
|
||||
private static String toTitleCase(final String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
|
||||
+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.client.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
|
||||
public interface KycRiskRulesReadPlatformService {
|
||||
List<KycRiskRuleData> retrieveAllRules();
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.client.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRule;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService {
|
||||
|
||||
private final ClientRiskRuleRepository repository;
|
||||
|
||||
@Override
|
||||
public List<KycRiskRuleData> retrieveAllRules() {
|
||||
final List<ClientRiskRule> rules = this.repository.findAllOrderedByOrderPosition();
|
||||
return rules.stream().map(this::mapToData).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private KycRiskRuleData mapToData(final ClientRiskRule rule) {
|
||||
final Long occCatId = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getId() : null;
|
||||
final String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null;
|
||||
final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null;
|
||||
final String occName = rule.getOccupation() != null ? rule.getOccupation().getLabel() : null;
|
||||
|
||||
return new KycRiskRuleData(
|
||||
rule.getId(),
|
||||
rule.getRuleName(),
|
||||
rule.getLegalFormId(),
|
||||
occCatId,
|
||||
occCatName,
|
||||
occId,
|
||||
occName,
|
||||
rule.getMinMonthlyIncome(),
|
||||
rule.getMaxMonthlyIncome(),
|
||||
rule.getMinAnnualIncome(),
|
||||
rule.getMaxAnnualIncome(),
|
||||
rule.getMinAnnualTurnover(),
|
||||
rule.getMaxAnnualTurnover(),
|
||||
rule.getRiskCategory(),
|
||||
rule.getReason(),
|
||||
rule.getOrderPosition()
|
||||
);
|
||||
}
|
||||
}
|
||||
-15
@@ -582,21 +582,6 @@ public class CtsClearingWritePlatformServiceImpl implements CtsClearingWritePlat
|
||||
}
|
||||
}
|
||||
|
||||
/** SHA-256 hex digest of the given content, used as a visible integrity fingerprint in the CTS header. */
|
||||
private String sha256Hex(final String content) {
|
||||
try {
|
||||
final java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
final byte[] hash = digest.digest(content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
final StringBuilder hex = new StringBuilder();
|
||||
for (final byte b : hash) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
} catch (final java.security.NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 is not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Masks all but the last 4 characters of an account number (matches the CTS file convention of not exposing full numbers). */
|
||||
private String maskAccountNumber(final String accountNumber) {
|
||||
if (accountNumber == null || accountNumber.isBlank()) {
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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.customersearch.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
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.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.customersearch.data.CustomerSearchResultData;
|
||||
import org.apache.fineract.portfolio.customersearch.service.CustomerSearchReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/customer-search")
|
||||
@Component
|
||||
@Tag(name = "Customer Search", description = "Multi-field customer lookup by name, account number (savings/current/FD/RD), Aadhaar, PAN, and/or address.")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerSearchApiResource {
|
||||
|
||||
private final CustomerSearchReadPlatformService readPlatformService;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Search for a customer", description = "AND-combines whichever of name/accountNumber/aadhaarNumber/panNumber/address/accountTypes are supplied - at least one is required. accountTypes is a comma-separated list (SAVINGS,CURRENT,FIXED_DEPOSIT,RECURRING_DEPOSIT).")
|
||||
public Collection<CustomerSearchResultData> search(@QueryParam("name") final String name,
|
||||
@QueryParam("accountNumber") final String accountNumber, @QueryParam("aadhaarNumber") final String aadhaarNumber,
|
||||
@QueryParam("panNumber") final String panNumber, @QueryParam("address") final String address,
|
||||
@QueryParam("accountTypes") final String accountTypes) {
|
||||
final List<String> accountTypeList = accountTypes == null || accountTypes.isBlank() ? List.of()
|
||||
: Arrays.asList(accountTypes.split(","));
|
||||
return safely(() -> this.readPlatformService.search(name, accountNumber, aadhaarNumber, panNumber, address, accountTypeList));
|
||||
}
|
||||
|
||||
/** Surfaces IllegalArgumentException as a clean 400 instead of an opaque 500. */
|
||||
private <T> T safely(final Supplier<T> action) {
|
||||
try {
|
||||
return action.get();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
final JsonObject body = new JsonObject();
|
||||
body.addProperty("message", e.getMessage());
|
||||
body.addProperty("defaultUserMessage", e.getMessage());
|
||||
body.addProperty("httpStatus", 400);
|
||||
final JsonArray errors = new JsonArray();
|
||||
final JsonObject err = new JsonObject();
|
||||
err.addProperty("defaultUserMessage", e.getMessage());
|
||||
errors.add(err);
|
||||
body.add("errors", errors);
|
||||
throw new WebApplicationException(
|
||||
Response.status(400).entity(this.gson.toJson(body)).type(MediaType.APPLICATION_JSON).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.customersearch.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* One matching (client, account) combination from a multi-field customer search. A client with
|
||||
* several accounts appears once per matching account; a client with no accounts at all still
|
||||
* appears once, with accountNo/accountType left null.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CustomerSearchResultData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long clientId;
|
||||
private String displayName;
|
||||
private String mobileNo;
|
||||
private String officeName;
|
||||
private String accountNo;
|
||||
/** SAVINGS, CURRENT, FIXED_DEPOSIT, or RECURRING_DEPOSIT. */
|
||||
private String accountType;
|
||||
private String panNumber;
|
||||
private String aadhaarNumber;
|
||||
private String address;
|
||||
}
|
||||
+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.customersearch.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.customersearch.data.CustomerSearchResultData;
|
||||
|
||||
public interface CustomerSearchReadPlatformService {
|
||||
|
||||
/**
|
||||
* Multi-field AND search - only non-blank parameters are applied. At least one must be
|
||||
* supplied. name/address are partial, case-insensitive matches; accountNumber/aadhaarNumber/
|
||||
* panNumber are exact matches (they're precise identifiers, not free text). accountTypes, if
|
||||
* non-empty, restricts results to those account types only (SAVINGS/CURRENT/FIXED_DEPOSIT/
|
||||
* RECURRING_DEPOSIT) - a client with no account, or only accounts of other types, is excluded
|
||||
* entirely when this filter is active (unlike the unfiltered case, where a client with no
|
||||
* account at all still appears once for informational purposes).
|
||||
*/
|
||||
Collection<CustomerSearchResultData> search(String name, String accountNumber, String aadhaarNumber, String panNumber,
|
||||
String address, List<String> accountTypes);
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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.customersearch.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.customersearch.data.CustomerSearchResultData;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final Set<String> VALID_ACCOUNT_TYPES = Set.of("SAVINGS", "CURRENT", "FIXED_DEPOSIT", "RECURRING_DEPOSIT");
|
||||
|
||||
private static final String BASE_SELECT = """
|
||||
SELECT DISTINCT c.id AS client_id, c.display_name, c.mobile_no, o.name AS office_name,
|
||||
acct.account_no, acct.account_type,
|
||||
pan.document_key AS pan_number, aad.document_key AS aadhaar_number,
|
||||
addr.address_line_1, addr.address_line_2, addr.town_village, addr.city,
|
||||
addr.county_district, addr.postal_code
|
||||
FROM m_client c
|
||||
LEFT JOIN m_office o ON o.id = c.office_id
|
||||
LEFT JOIN m_client_identifier pan ON pan.client_id = c.id AND pan.document_type_id = (
|
||||
SELECT id FROM m_code_value WHERE code_value = 'PAN Card'
|
||||
AND code_id = (SELECT id FROM m_code WHERE code_name = 'KYCIdProofType'))
|
||||
LEFT JOIN m_client_identifier aad ON aad.client_id = c.id AND aad.document_type_id = (
|
||||
SELECT id FROM m_code_value WHERE code_value = 'Aadhaar Card'
|
||||
AND code_id = (SELECT id FROM m_code WHERE code_name = 'KYCIdProofType'))
|
||||
LEFT JOIN m_address addr ON addr.client_id = c.id
|
||||
LEFT JOIN (
|
||||
SELECT client_id, account_no,
|
||||
CASE deposit_type_enum WHEN 100 THEN 'SAVINGS' WHEN 200 THEN 'FIXED_DEPOSIT'
|
||||
WHEN 300 THEN 'RECURRING_DEPOSIT' ELSE 'SAVINGS' END AS account_type
|
||||
FROM m_savings_account
|
||||
UNION ALL
|
||||
SELECT client_id, account_no, 'CURRENT' AS account_type FROM m_current_account
|
||||
) acct ON acct.client_id = c.id
|
||||
""";
|
||||
|
||||
@Override
|
||||
public Collection<CustomerSearchResultData> search(final String name, final String accountNumber, final String aadhaarNumber,
|
||||
final String panNumber, final String address, final List<String> accountTypes) {
|
||||
final boolean hasName = name != null && !name.isBlank();
|
||||
final boolean hasAccountNumber = accountNumber != null && !accountNumber.isBlank();
|
||||
final boolean hasAadhaar = aadhaarNumber != null && !aadhaarNumber.isBlank();
|
||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||
final boolean hasAddress = address != null && !address.isBlank();
|
||||
final List<String> effectiveAccountTypes = accountTypes == null ? List.of()
|
||||
: accountTypes.stream().filter(t -> t != null && !t.isBlank()).map(String::trim).map(String::toUpperCase).toList();
|
||||
|
||||
if (!hasName && !hasAccountNumber && !hasAadhaar && !hasPan && !hasAddress && effectiveAccountTypes.isEmpty()) {
|
||||
throw new IllegalArgumentException("Enter at least one search field - name, account number, Aadhaar, PAN, or address.");
|
||||
}
|
||||
for (final String type : effectiveAccountTypes) {
|
||||
if (!VALID_ACCOUNT_TYPES.contains(type)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown account type: " + type + ". Valid values: SAVINGS, CURRENT, FIXED_DEPOSIT, RECURRING_DEPOSIT.");
|
||||
}
|
||||
}
|
||||
|
||||
final StringBuilder sql = new StringBuilder(BASE_SELECT).append(" WHERE 1=1 ");
|
||||
final List<Object> params = new ArrayList<>();
|
||||
|
||||
if (hasName) {
|
||||
sql.append(" AND LOWER(c.display_name) LIKE LOWER(?) ");
|
||||
params.add("%" + name.trim() + "%");
|
||||
}
|
||||
if (hasAccountNumber) {
|
||||
sql.append(" AND acct.account_no = ? ");
|
||||
params.add(accountNumber.trim());
|
||||
}
|
||||
if (hasAadhaar) {
|
||||
sql.append(" AND aad.document_key = ? ");
|
||||
params.add(aadhaarNumber.trim());
|
||||
}
|
||||
if (hasPan) {
|
||||
sql.append(" AND UPPER(pan.document_key) = UPPER(?) ");
|
||||
params.add(panNumber.trim());
|
||||
}
|
||||
if (hasAddress) {
|
||||
sql.append(" AND (LOWER(addr.address_line_1) LIKE LOWER(?) OR LOWER(addr.address_line_2) LIKE LOWER(?) "
|
||||
+ "OR LOWER(addr.town_village) LIKE LOWER(?) OR LOWER(addr.city) LIKE LOWER(?) "
|
||||
+ "OR LOWER(addr.county_district) LIKE LOWER(?) OR LOWER(addr.postal_code) LIKE LOWER(?)) ");
|
||||
final String likeAddress = "%" + address.trim() + "%";
|
||||
for (int i = 0; i < 6; i++) {
|
||||
params.add(likeAddress);
|
||||
}
|
||||
}
|
||||
if (!effectiveAccountTypes.isEmpty()) {
|
||||
// Restricting to specific account types excludes both non-matching-type accounts AND
|
||||
// clients with no account at all (acct.account_type would be NULL for those, which can
|
||||
// never equal one of the listed types) - that's the point of the filter.
|
||||
final String placeholders = String.join(",", effectiveAccountTypes.stream().map(t -> "?").toList());
|
||||
sql.append(" AND acct.account_type IN (").append(placeholders).append(") ");
|
||||
params.addAll(effectiveAccountTypes);
|
||||
}
|
||||
sql.append(" ORDER BY c.display_name ASC LIMIT 100");
|
||||
|
||||
return this.jdbcTemplate.query(sql.toString(), new CustomerSearchResultMapper(), params.toArray());
|
||||
}
|
||||
|
||||
private static final class CustomerSearchResultMapper implements RowMapper<CustomerSearchResultData> {
|
||||
@Override
|
||||
public CustomerSearchResultData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Long clientId = rs.getLong("client_id");
|
||||
final String displayName = rs.getString("display_name");
|
||||
final String mobileNo = rs.getString("mobile_no");
|
||||
final String officeName = rs.getString("office_name");
|
||||
final String accountNo = rs.getString("account_no");
|
||||
final String accountType = rs.getString("account_type");
|
||||
final String panNumber = rs.getString("pan_number");
|
||||
final String aadhaarNumber = rs.getString("aadhaar_number");
|
||||
|
||||
final List<String> addressParts = new ArrayList<>();
|
||||
addPart(addressParts, rs.getString("address_line_1"));
|
||||
addPart(addressParts, rs.getString("address_line_2"));
|
||||
addPart(addressParts, rs.getString("town_village"));
|
||||
addPart(addressParts, rs.getString("city"));
|
||||
addPart(addressParts, rs.getString("county_district"));
|
||||
addPart(addressParts, rs.getString("postal_code"));
|
||||
final String address = addressParts.isEmpty() ? null : String.join(", ", addressParts);
|
||||
|
||||
return new CustomerSearchResultData(clientId, displayName, mobileNo, officeName, accountNo, accountType, panNumber,
|
||||
aadhaarNumber, address);
|
||||
}
|
||||
|
||||
private void addPart(final List<String> parts, final String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
parts.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMaste
|
||||
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/denominationmaster")
|
||||
@Component
|
||||
@@ -105,7 +106,7 @@ public class DenominationMasterApiResource {
|
||||
writeAudit("ACTIVATE", dm, null);
|
||||
} else if ("deactivate".equals(command)) {
|
||||
dm.setIsActive(false);
|
||||
dm.setDiscontinuedOn(LocalDate.now());
|
||||
dm.setDiscontinuedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
repository.save(dm);
|
||||
writeAudit("DEACTIVATE", dm, null);
|
||||
} else if ("restore".equals(command)) {
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_denomination_master_audit")
|
||||
@@ -77,7 +78,7 @@ public class DenominationMasterAudit {
|
||||
a.changes = changes;
|
||||
a.performedBy = performedBy;
|
||||
a.performedByUsername = performedByUsername;
|
||||
a.performedOn = LocalDateTime.now();
|
||||
a.performedOn = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomin
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Validates and persists the per-transaction cash denomination breakdown sent by the UI
|
||||
@@ -163,7 +164,7 @@ public class DenominationBreakdownWriter {
|
||||
}
|
||||
if (cashierId == null) return;
|
||||
|
||||
final String today = java.time.LocalDate.now().toString();
|
||||
final String today = java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct "
|
||||
+ "WHERE ct.to_cashier_id = ? "
|
||||
+ " AND ct.transfer_date < CAST(? AS DATE) "
|
||||
@@ -203,7 +204,7 @@ public class DenominationBreakdownWriter {
|
||||
}
|
||||
if (cashierId == null) return;
|
||||
|
||||
final String today = java.time.LocalDate.now().toString();
|
||||
final String today = java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct "
|
||||
+ "WHERE ct.to_cashier_id = ? "
|
||||
+ " AND ct.transfer_date = CAST(? AS DATE) "
|
||||
@@ -511,7 +512,7 @@ public class DenominationBreakdownWriter {
|
||||
.count(count)
|
||||
.totalAmount(lineTotal)
|
||||
.txnDate(txnDate)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.txnType(resolvedTxnType)
|
||||
.officerId(officerId)
|
||||
.officerUsername(officerUsername)
|
||||
|
||||
+8
-1
@@ -1,6 +1,13 @@
|
||||
package org.apache.fineract.portfolio.election.api;
|
||||
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
+12
-2
@@ -3,7 +3,17 @@ package org.apache.fineract.portfolio.election.api;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PATCH;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
@@ -138,6 +148,6 @@ public class CommitteeMeetingApiResource {
|
||||
|
||||
private String clientIp(HttpServletRequest req) {
|
||||
String ip = req.getHeader("X-Forwarded-For");
|
||||
return (ip != null && !ip.isBlank()) ? ip.split(",")[0].trim() : req.getRemoteAddr();
|
||||
return (ip != null && !ip.isBlank()) ? Iterables.get(Splitter.on(',').split(ip), 0).trim() : req.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -4,7 +4,18 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
@@ -125,6 +136,6 @@ public class DirectorAppointmentApiResource {
|
||||
|
||||
private String clientIp(HttpServletRequest req) {
|
||||
String ip = req.getHeader("X-Forwarded-For");
|
||||
return (ip != null && !ip.isBlank()) ? ip.split(",")[0].trim() : req.getRemoteAddr();
|
||||
return (ip != null && !ip.isBlank()) ? Iterables.get(Splitter.on(',').split(ip), 0).trim() : req.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -15,6 +15,8 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
@@ -123,7 +125,9 @@ public class ElectionCandidateApiResource {
|
||||
if (existing != null && !existing.startsWith("data:") && existing.contains("|")) {
|
||||
String[] ep = existing.split("\\|", 2);
|
||||
if (ep.length == 2) {
|
||||
try { contentStoreService.delete(ep[1]); } catch (Exception ignored) {}
|
||||
try { contentStoreService.delete(ep[1]); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +222,7 @@ public class ElectionCandidateApiResource {
|
||||
private static String clientIp(HttpServletRequest req) {
|
||||
if (req == null) return null;
|
||||
String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim();
|
||||
if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
@@ -17,6 +16,8 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionConfigData;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionConfigRequest;
|
||||
@@ -89,7 +90,9 @@ public class ElectionConfigApiResource {
|
||||
throw e;
|
||||
}
|
||||
String updateDetails = null;
|
||||
try { updateDetails = objectMapper.writeValueAsString(request); } catch (Exception ignored) {}
|
||||
try { updateDetails = objectMapper.writeValueAsString(request); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
electionAuditService.log(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId,
|
||||
"Election updated: " + request.getTitle(), updateDetails, clientIp(httpRequest));
|
||||
return Response.ok(Map.of("resourceId", electionId)).build();
|
||||
@@ -214,7 +217,7 @@ public class ElectionConfigApiResource {
|
||||
private static String clientIp(HttpServletRequest req) {
|
||||
if (req == null) return null;
|
||||
String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim();
|
||||
if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -15,6 +15,8 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionApprovalStepData;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionNominationData;
|
||||
import org.apache.fineract.portfolio.election.data.ElectionNominationRequest;
|
||||
@@ -295,7 +297,7 @@ public class ElectionNominationApiResource {
|
||||
private static String clientIp(HttpServletRequest req) {
|
||||
if (req == null) return null;
|
||||
String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim();
|
||||
if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -15,6 +15,8 @@ import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
@@ -104,10 +106,10 @@ public class ElectionVotingApiResource {
|
||||
}
|
||||
winDet.append("]");
|
||||
String declareDetails = wFirst ? null : winDet.toString();
|
||||
String declareDesc = "Results declared" + (declareDetails != null ? " — " + (results.stream()
|
||||
String declareDesc = "Results declared" + (declareDetails != null ? " — " + results.stream()
|
||||
.filter(r -> Integer.valueOf(1).equals(r.getRankPosition()) || "WON".equalsIgnoreCase(r.getResult()))
|
||||
.map(r -> r.getCandidateName() + " won " + r.getPostName())
|
||||
.reduce((a, b) -> a + "; " + b).orElse("")) : "");
|
||||
.reduce((a, b) -> a + "; " + b).orElse("") : "");
|
||||
electionAuditService.log(ElectionAuditAction.DECLARE_RESULTS, "RESULT", electionId, electionId,
|
||||
declareDesc, declareDetails, clientIp(httpRequest));
|
||||
return Response.ok(results).build();
|
||||
@@ -127,7 +129,7 @@ public class ElectionVotingApiResource {
|
||||
private static String clientIp(HttpServletRequest req) {
|
||||
if (req == null) return null;
|
||||
String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim();
|
||||
if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -2,7 +2,12 @@ package org.apache.fineract.portfolio.election.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.*;
|
||||
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 jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
+7
-1
@@ -2,7 +2,13 @@ package org.apache.fineract.portfolio.election.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
+8
-1
@@ -1,6 +1,13 @@
|
||||
package org.apache.fineract.portfolio.election.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user