Compare commits

...
79 Commits
Author SHA1 Message Date
Jaanu423andClaude Sonnet 5 383dfdc6e9 feat(chequebook): silent balance hold instead of activation-time charge
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
- Removed the "CHEQUE_BOOK" facility charge from account-activation
  auto-apply (both savings and current) — the per-leaf issuance charge is
  now the only cheque-book charge, applied at request/approval time only.
- Cheque book request now reserves the charge amount via a direct
  on-hold-funds field update instead of Fineract's native hold-amount
  command, so no "Amount on hold"/"Release Amount" transaction row is ever
  created — nothing shows in the passbook/transactions list, and the
  reservation is released the same way (silently) on approval (then
  charged) or rejection (returned), for both savings and current accounts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:03:30 +05:30
Jaanu423andClaude Sonnet 5 5ed95c27e8 fix(chequebook,current-account): current-account transaction fixes for cheque-book charge
- CurrentAccountTransactionsApiResource.retrieveOne() was calling the savings
  read-service to fetch a single transaction, which always queries
  m_savings_account_transaction and can never find a current-account row —
  crashed with EmptyResultDataAccessException. Added a dedicated
  CurrentAccountReadPlatformService.retrieveOneTransaction() backed by
  m_current_account_transaction instead.
- CurrentAccountReadPlatformServiceImpl.buildTransactionType() (and the
  search-transactions endpoint) had no cases for hold (8) / release (9)
  transaction types, so they displayed as "Deposit"/"Other" instead of
  "Amount on hold"/"Release Amount".
- ChequeBookIssuanceChargeService.deductFromCurrent() never set
  runningBalanceDerived on the charge-deduction transaction, leaving the
  Balance column blank for that row in the transactions list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 15:06:35 +05:30
Jaanu423andClaude Sonnet 5 fde670e644 feat(chequebook): per-leaf issuance charge with balance validation and fund hold
Requesting a cheque book now computes a per-leaf charge (reusing the existing
"Cheque Book Charge" facility mapping) and blocks the request at submit time if
the account can't absorb it without breaching its minimum required balance.

The charge amount is reserved via Fineract's native hold-amount mechanism when
the request is submitted, released and actually deducted (with GL posting) on
checker approval, or released back to the customer on rejection — so funds are
never left dangling on hold regardless of outcome.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 11:19:11 +05:30
Jaanu423andClaude Sonnet 5 3bb7fc8050 feat(chequebook): add RETURNED leaf status for dishonoured/bounced cheques
Adds a maker-checker-gated "return" command on cheque books that flags
presented-and-dishonoured leaves as terminal RETURNED (distinct from
STOP_PAYMENT/drawer-initiated and LOST/custody-failure), following the
same reason/actor/timestamp pattern already used for lost/cancelled/used
leaves on m_cheque_leaf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 16:07:04 +05:30
Jaanu423 04d65d857f feat(chequebook): add cheque book module with Maker-Checker workflow
Adds cheque series, stock lot/transfer, cheque book issuance,
stop-payment/lost/cancel-leaf actions, and pending-request approval
APIs with Maker-Checker command handlers and liquibase changelogs.
2026-07-15 11:13:06 +05:30
Jaanu423andClaude Sonnet 4.6 9d09d9ebd4 fix(db): add missing m_saving_product_temp table causing 500 on product list APIs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 15:58:17 +05:30
Jaanu423andClaude Sonnet 4.6 eb88fb0813 fix(db): add missing m_savings_account columns causing 500 on savings list
introducer_required, share_linking_enabled, terminate_true were missing
from m_savings_account in fineract_saga. Applied directly to DB and
added 0368 catch-up migration with preConditions for idempotency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 15:47:14 +05:30
Jaanu423andClaude Sonnet 4.6 f13a9c6bba merge(release): merge feature/minor-update into release/share-account-module
Combines election module + UAT catch-up migrations (0338-0367) with
minor-update changes: soft delete for tellers (0338), share linking
for current account (0339), terminate flag for savings/current (0340).
Resolved changelog conflict by appending minor-update entries after 0367.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 13:43:03 +05:30
Jaanu423andClaude Sonnet 4.6 0e076889e9 feat(db): idempotent catch-up migrations for UAT schema gaps (0358-0367)
Add 10 Liquibase catch-up changeSets covering m_client, m_client_kyc,
m_family_members, m_client_nominee, m_client_msme, m_rbi_feed_item,
m_savings_product, m_current_product, m_clerk_withdrawal_request,
m_daily_token_counter, and misc column additions (0207-0348 range).
All changeSets use preConditions (tableExists/columnExists) so they
MARK_RAN if already applied. Fixes PostgreSQL startup crash in 0360
by replacing MySQL-specific INFORMATION_SCHEMA sqlCheck with Liquibase-
native foreignKeyConstraintExists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 13:35:41 +05:30
Jaanu423andClaude Sonnet 4.6 87b308b527 feat(election): Chairman/VC auto-resolution, member vote list on voting page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 10:59:00 +05:30
Jaanu423andClaude Sonnet 4.6 934a51c9ee feat(election): dynamic designations from m_code, email-based voting, vote remark field
- DesignationService reads designation limits and mandatory flags from DirectorDesignation m_code_value
- DirectorAppointmentServiceImpl: replaced hardcoded DESIGNATION_LIMITS map with dynamic DB fetch
- CommitteeServiceImpl: replaced hardcoded Chairman/Vice Chairman check with dynamic mandatory designation query
- New GET /v1/election/directors/designations endpoint returns all active designations
- Email-based voting module: MeetingPublicVoteApiResource, MeetingVoteAdminApiResource, MeetingVoteService
- MeetingMemberVote: added remark (TEXT) column for member vote remarks
- AgendaVoteRequest: added remark field to carry voice/typed remarks per agenda item
- Liquibase migrations 0354 (vote/resolution tables), 0355 (DirectorDesignation code), 0356 (vote remark column)
- SecurityConfig: permit public voting endpoints without authentication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 18:26:14 +05:30
keshav898966 9980310c9e table changes on Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits 2026-07-06 17:49:39 +05:30
Jaanu423andClaude Sonnet 4.6 cfcd24efad refactor(client): reformat ClientMapper.java
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 16:58:34 +05:30
Jaanu423andClaude Sonnet 4.6 73db4ee114 feat(election): relational agenda items, email redesign, and stale-cache fix
- Add CommitteeMeetingAgenda entity + repository for relational agenda storage
- Liquibase migration 0353: create m_election_committee_meeting_agenda table,
  drop old agenda TEXT column from m_election_committee_meeting
- Update CommitteeMeeting: replace String agenda with List<CommitteeMeetingAgenda>
  (EAGER, no cascade — writes handled explicitly via repository)
- Update CommitteeMeetingRepository: explicit JPQL with SELECT DISTINCT + LEFT JOIN
  FETCH to load agendaItems and prevent NonUniqueResultException on Optional returns
- Fix email sending stale agenda: inject EntityManager, call refresh() after
  agendaRepository.flush() so EclipseLink reloads the collection from DB
- Redesign buildMeetingInviteHtml: table-based email layout, icon+label+value rows,
  amber agenda section with numbered badges, compact two-column footer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 16:53:32 +05:30
Jaanu423andClaude Sonnet 4.6 e72b9359c1 feat(election): committee meeting module — schedule, online/offline mode, soft delete, audit logging
- CommitteeMeeting entity, enums (ONLINE/OFFLINE mode, SCHEDULED/COMPLETED/CANCELLED status), repository with soft delete filter
- CommitteeMeetingService + Impl: schedule, update, patchStatus, soft delete, retrieveByCommittee
- CommitteeMeetingApiResource: REST endpoints at /v1/election/committee-meetings with full audit logging
- ElectionAuditAction: add SCHEDULE_MEETING, UPDATE_MEETING, CANCEL_MEETING, COMPLETE_MEETING, DELETE_MEETING + FAILED variants
- CommitteeService: expose getElectionIdForCommittee() for audit context
- Liquibase 0351: create m_election_committee_meeting table with is_deleted soft delete column
- Liquibase 0352: rename created_date/last_modified_date → created_on_utc/last_modified_on_utc to match EclipseLink base class mapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 10:16:20 +05:30
Jaanu423andClaude Sonnet 4.6 a1a31aff25 feat(election): comprehensive audit log fixes — committee logging, deduplicate service logs, fix director entity type
- Add audit logging for all 5 committee operations (create, update, dissolve, add/remove member)
- Add CREATE_COMMITTEE, UPDATE_COMMITTEE, DISSOLVE_COMMITTEE, ADD_COMMITTEE_MEMBER, REMOVE_COMMITTEE_MEMBER to ElectionAuditAction enum
- Fix double logging: remove all duplicate audit calls from ElectionWritePlatformServiceImpl (API resource logs are authoritative)
- Fix double logging in DirectorAppointmentServiceImpl (keep only autoExpireByTenure scheduler log)
- Fix entity type bug in DirectorAppointmentApiResource: "DIRECTOR" -> "DIRECTOR_APPOINTMENT" to match frontend filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 17:24:03 +05:30
Jaanu423andClaude Sonnet 4.6 2606427203 feat(election): committee management — CRUD, members, image, and director fix
- Add Committee entity, CommitteeMember, repositories, service, and REST API
- Liquibase migrations: 0349 committee tables, 0350 committee image column
- Committee supports profile image (base64 TEXT), name, description, status
- Current directors: fix EclipseLink native query (?1 positional param) and add
  fallback to most recent RESULT_DECLARED election when no active duration found
- DirectorAppointmentRepository: add findByElectionIdAndStatus (non-pageable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 16:39:42 +05:30
keshav898966 b0bfa386ce implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits 2026-07-03 15:15:09 +05:30
Jaanu423andClaude Sonnet 4.6 00d3a2e333 feat(election): candidate modification lock, director member photo, and comprehensive audit logging
- ElectionConfig: add isCandidateModificationLocked() — blocks remove/update/symbol after voting starts
- ElectionWritePlatformServiceImpl: full audit logging on all 16 operations; failures logged in new transaction (REQUIRES_NEW) so they survive main tx rollback
- DirectorAppointmentData: add memberId field for client photo lookup
- DirectorAppointmentServiceImpl: batch-fetch candidate memberId via ElectionCandidateRepository; pass to response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 13:44:49 +05:30
Jaanu423andClaude Sonnet 4.6 048a4ecfde feat(election): election duration blocking, director tenure auto-fill, audit logging, and scheduler
- Add durationYears to ElectionConfig with Liquibase migration (0348)
- Block new election creation when previous election duration is still active (PostgreSQL-compatible native query)
- Auto-override director tenureEndDate from election durationEndDate on appoint and update
- Add ElectionSchedulerConfiguration to enable @Scheduled support
- Fix JPQL EclipseLink enum compatibility in expireByTenureDate query
- Add full audit logging to DirectorAppointmentServiceImpl (appoint, update, remove, auto-expire)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 12:25:43 +05:30
Jaanu423andClaude Sonnet 4.6 7ce7353e7a feat(election): Board of Directors — director appointment management
- DirectorAppointment entity + Liquibase migration (0347)
- CRUD API: POST/GET/PUT/DELETE /v1/election/directors with @Valid Bean Validation
- GET /v1/election/directors/pending — WON candidates not yet appointed
- Duplicate prevention: existsByElectionIdAndCandidateId check on appoint
- Business validation: tenure end must be after start; tenure start cannot be before appointment date
- @Scheduled daily midnight job to auto-expire tenure-ended directors via bulk JPQL UPDATE
- Full audit logging for all director actions (success + failure) via ElectionAuditService
- PendingDirectorData DTO; DirectorAppointmentStatus enum (ACTIVE/RESIGNED/REMOVED/TENURE_ENDED)
- ElectionAuditAction: added APPOINT_DIRECTOR, UPDATE_DIRECTOR_APPOINTMENT, REMOVE_DIRECTOR_APPOINTMENT + FAILED variants
- ElectionResultRepository: findByResult(String) for pending director lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 12:50:04 +05:30
keshav898966 99c153830c pending account terminate feature develop 2026-07-01 17:25:04 +05:30
Jaanu423andClaude Sonnet 4.6 ba199d78e7 feat(election): date validations, overdue-only NDC/loan checks, audit log gaps fixed
- Added ElectionDateViolationException for proper 4xx on date violations
- createElection: validates date order (nominationStart <= nominationEnd <= electionDate <= resultDate)
- withdrawNomination/resubmitNomination: blocked after nominationEndDate
- closeNominations: requires today >= nominationEndDate
- openVoting/closeVoting: requires today >= electionDate
- declareResults: requires today >= resultDate (or electionDate if no resultDate)
- NDC hasNoDues: checks overdue EMIs only (total_overdue_derived), not outstanding balance
- Director/guarantor/family loan checks: overdue-based only — pass if no missed EMIs
- MemberHasOutstandingDuesException: proper AbstractPlatformDomainRuleException (was IllegalStateException → 500)
- Audit gaps fixed: RESUBMIT_NOMINATION_FAILED added to enum, try/catch added for resubmit,
  createElection, updateElection; saveManualVoteCounts catches all RuntimeException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 16:50:08 +05:30
Jaanu423andClaude Sonnet 4.6 99cc972804 feat(election): comprehensive audit log — rich details for all actions
- Fixed VARCHAR(500) overflow: description column changed to TEXT (Liquibase 0346)
- Fixed CEO pre-check 500 error: fm.firstname/lastname column name fix
- Fixed audit log showing wrong PASS/FAIL for CEO pre-check
- Fixed approval action not showing step name/number (read PENDING step before action)
- submitNomination: details JSON with applicantName, memberId, postId, gender, caste
- runEligibilityCheck: details JSON with all 5 check results + remarks
- runCeoPreCheck: details JSON with all 7 individual check results + remarks
- processApprovalStep: details JSON with action, stepName, stepNumber, remarks, rejectionReason
- activateElection/closeNominations/openVoting/closeVoting/cancelElection: details with electionTitle + newStatus
- updateElection: full request JSON via ObjectMapper
- approveCandidate: details with nominationId + applicantName
- updateCandidate: details with symbolUpdated, profileUpdated, briefProfile
- uploadCandidateSymbol: details with filename + mimeType
- declareResults: winner summary JSON array [{post, winner, votes}]
- saveManualVoteCounts: return type changed void→String, details JSON array with candidate vote data
- Added Liquibase migrations: 0344, 0345 for manual vote count, 0346 for TEXT column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:59:23 +05:30
Jaanu423andClaude Sonnet 4.6 99a89d5d81 feat(election): overall shareholder analysis API + analytics enhancements
- ElectionAnalyticsServiceImpl: fix SQL_FAMILY to count candidate's own family members who are active shareholders (candidate-specific, not system-wide)
- ElectionAnalyticsServiceImpl: SQL_SUB_CASTE now groups by m_client.lastname instead of sub_caste_cv_id
- ElectionAnalyticsServiceImpl: SQL_CANDIDATE_INFO fetches candidate client_id, lastname, religion for match highlighting
- ElectionAnalyticsServiceImpl: add getOverallShareholderAnalysis() — returns all demographic breakdowns with no candidate matching
- ElectionAnalyticsService: add getOverallShareholderAnalysis() interface method
- ElectionCandidateApiResource: add GET /{electionId}/shareholders/analysis endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 18:36:02 +05:30
Jaanu423andClaude Sonnet 4.6 388af0a6a3 feat(election): candidate voter analysis API — multi-dimension breakdown + sub-caste migration
- New ElectionAnalyticsServiceImpl with 14 SQL breakdown queries:
  gender, age, area, social category (caste), sub-caste, religion,
  occupation, share-holding, tenure, loan, education, marital status,
  monthly income, savings account depth
- Cross-dimension match: caste + area simultaneously (core voter base)
- Zero-count row injection for caste/gender/sub-caste when candidate's
  category has no shareholder matches in GROUP BY result
- Liquibase 0343: sub_caste_cv_id column on m_client with 33 pre-seeded
  sub-caste values across General / OBC / SC / ST categories
- Liquibase 0342: election post eligibility columns
- CandidateVoterAnalysisData DTO with education, marital, income,
  savings, activeSavingsHolders, crossMatchCount fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:54:01 +05:30
keshav898966 1bd41cff43 saving, currun, fd and rd account working and tested 2026-06-30 10:06:59 +05:30
Jaanu423andClaude Sonnet 4.6 34f84e1fc6 chore: remove tracked log files and ignore logs directory permanently
- Delete all fineract-provider/logs/*.log from version control
- Add fineract-provider/logs/ to .gitignore so runtime logs are
  never tracked again

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 13:06:54 +05:30
Jaanu423andClaude Sonnet 4.6 69513f25f3 feat(election): production-ready audit log with server-side filter + pagination
- Add m_election_audit_log table via Liquibase (0341)
- ElectionAuditLog entity, ElectionAuditAction enum (21 actions)
- ElectionAuditService: log() with double try-catch, getAuditLogPaged()
  with entityType/action/full-text filters, size cap at 100, page safety
- ElectionAuditLogRepository: findWithFilters() JPQL with optional params
- All 4 election API resources wired with audit calls (create/update/
  activate/nominations/voting/candidates/votes/results)
- GET /{electionId}/audit-log accepts ?entityType=&action=&search=&page=&size=

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 12:57:21 +05:30
Jaanu423andClaude Sonnet 4.6 3d30bdb788 feat(election): CBS ContentStore symbol upload, hasSymbol DTO, vote summary API
- ElectionCandidateData: replace String symbol with Boolean hasSymbol + add memberId
- ElectionCandidateApiResource: symbol upload/download via ContentStoreService
  (mimeType|storagePath format, backward-compatible with legacy base64 records)
- ElectionVotingApiResource: add GET /vote-summary endpoint (totalVotes, totalVoters,
  per-candidate breakdown) using ElectionVoteRepository queries
- ElectionVoteRepository: add countByElectionConfigId and voteSummaryByCandidate queries
- ElectionReadPlatformServiceImpl: map hasSymbol and memberId in candidate list response
- ElectionWritePlatformServiceImpl: set photoUrl from memberId on approveCandidate,
  delete old symbol file from ContentStore on removeCandidate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:39:28 +05:30
Jaanu423andClaude Sonnet 4.6 ed139d6e68 feat(election): server-side pagination, family loan CEO pre-check, extended checks
- PagedResponse<T>: generic paginated wrapper with statusCounts and summary fields
- ElectionConfigRepository: pageable query + countGroupByStatus
- ElectionNominationRepository: pageable queries + count-by-status helpers
- ElectionCandidateRepository: pageable queries by election and post
- ElectionReadPlatformService: all list methods return PagedResponse with page/size params
- ElectionReadPlatformServiceImpl: add missing Pageable import; paginated impls with summary data
- API resources: @QueryParam page/size with @DefaultValue on all 3 list endpoints
- ElectionCeoPrecheck: add familyLoanCheckResult + familyLoanCheckRemarks (7th auto-check)
- ElectionCeoPreCheckServiceImpl: fix directorLoan check (overdue only); add family loan check covering manual members (unverifiable→FAIL) and linked CBS clients (overdue→FAIL)
- Liquibase 0338/0339: extended CEO pre-check columns + family loan check columns
- changelog-tenant.xml: include both new changesets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 12:08:29 +05:30
keshav898966 db41c2b606 feat: add share linking enabled to current and savings accounts 2026-06-26 11:12:00 +05:30
Jaanu423andClaude Sonnet 4.6 fc80869d4b feat(guarantor): enrich guarantor accounts API with loan health and borrower details
- Add borrower name, savings account, guaranteed/released/remaining amounts, loan timeline, and overdue info to GuarantorAccountSummaryData
- Fix duplicate rows in GuarantorLoanAccountSummaryDataMapper by joining savings account via guarantor funding details instead of client ID
- Fix NPE in GuarantorDomainServiceImpl when loan product guarantee details are null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:15:25 +05:30
keshav898966 8e55b6747c Merge branch 'feature/minor-update' after resolving conflict in logs 2026-06-24 12:50:35 +05:30
keshav898966 21878939b5 Merge branch 'feature/minor-update' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/minor-update 2026-06-24 12:47:53 +05:30
keshav898966 b5c9101fa4 feat: implement teller creation limit and soft delete with cashier check bypass 2026-06-24 12:47:03 +05:30
keshav898966 1ff3f9c0c9 shares account link to new saving account 2026-06-23 15:23:30 +05:30
Jaanu423andClaude Sonnet 4.6 1eba2c50ed fix(election): move additional permissions to new changeset to avoid Liquibase checksum error
The 5 missing permissions were added to the already-executed changeset
causing checksum mismatch on restart. Move them to a new changeset
0337_seed_election_permissions_v2 with a preCondition guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 11:13:39 +05:30
Jaanu423andClaude Sonnet 4.6 3367b7d935 fix(election): seed missing permissions in Liquibase migration 0337
Add 5 permissions that the write service enforces but were absent from
the DB seed: UPDATE_ELECTION, ELIGIBILITY_CHECK_ELECTION,
CEO_PRECHECK_ELECTION, APPROVE_ELECTION_NOMINATION,
APPROVE_ELECTION_CANDIDATE — without these, role assignments for those
operations would fail at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:48:46 +05:30
Jaanu423andClaude Sonnet 4.6 5899d2ca07 feat(election): add election module — config, nominations, candidates, voting, approval flow
- REST API resources: ElectionConfig, ElectionNomination, ElectionCandidate, ElectionVoting
- Domain entities, repositories, services (read + write) with maker-checker approval steps
- Eligibility checks, no-due-certificate, CEO pre-check services
- Liquibase migration 0337 creates all election module tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:39:43 +05:30
Jaanu423andClaude Sonnet 4.6 c1e23080fc fix(share-account+clerk): remove cashier collection validation, add NPE guards
ShareAccountWritePlatformServiceJpaRepositoryImpl:
- Remove m_share_cashier_collection validation from activateShareAccount() —
  cashier now enters GL directly via transaction entry workstation
- Mark all share account charges as paid on activation instead of posting
  duplicate GL (cashier already posted DR Cash -> CR Charges GL directly)
- Remove unused imports: java.sql.Date, EmptyResultDataAccessException, Collectors
- Update createShareAccount() comment to reflect new cashier GL flow

ClerkDisbursementRequestApiResource:
- Add null guard for clerk_id before cast in approve() — returns 500 with
  clear message if DB record is missing the field
- Add null/blank guard for credit_entries in approve() — returns 400 with
  clear message instead of NPE on gson.fromJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:46:17 +05:30
Jaanu423andClaude Sonnet 4.6 7354da8c1e feat(bank-transfer): add maker-checker support for bank transfers
- Added request_type column to m_clerk_disbursement_request via migration 0336
- submit() extracts requestType from body, stores in DB (defaults to DISBURSEMENT)
- approve() detects BANK_TRANSFER mode via request_type field:
    - DR entries: directDebitSavingsAccount / directDebitCurrentAccount
    - CR entries: directCreditSavingsAccount / directCreditCurrentAccount
    - GL: DR SavingsControl(per DR) / CR SavingsControl(per CR)
    - Audit INSERT into m_bank_transfer_log with unique BT voucher number
    - Existing disbursement/collection logic unchanged in else branch
- list(), myRequests(), loadRecord() all return request_type
- Migration 0335 (m_bank_transfer_log) also included

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:47:21 +05:30
Jaanu423andClaude Sonnet 4.6 bf5ce421df feat(clerk+gl): implement real CBS book transfer for savings and current account disbursements
- Replace Fineract cash deposit API with direct INSERT for both savings and current accounts
  → Zero CashVault entries created anywhere in DB for disbursements
- Savings: directCreditSavingsAccount now sets running_balance_derived and updates
  total_deposits_derived alongside account_balance_derived (parity with current account)
- Current: directCreditCurrentAccount — direct INSERT into m_current_account_transaction
  with running_balance_derived, total_deposits_derived update, and remarks audit field
- getCurrentControlGlId: looks up SAVINGS_CONTROL GL for current account products
  via acc_product_mapping (product_type=2, financial_account_type=2)
- directReverseCurrentCredit: compensation restores both account_balance_derived
  and total_deposits_derived on rollback
- GL: combined DR sourceGL / CR SavingsControl entry posted for both savings and
  current account credits; linkGlEntriesToSavingsAccounts sets entity_type_enum=2
  so ACCOUNT NO column shows in journal entries view
- Hide reversed=true GL entries from all GL views in JournalEntryReadPlatformServiceImpl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:04:17 +05:30
Jaanu423andClaude Sonnet 4.6 d002fc0c9b feat(clerk): add collection flow backend — savings withdrawal + Cash@Vault GL offset
- ClerkDisbursementRequestApiResource: support collection mode (dbCr='Dr' entries); submit() separates DR/CR totals and validates balance; approve() detects mode, calls executeSavingsWithdrawal/executeCurrentAccountWithdrawal for DR entries, then posts GL offset via findCashVaultGlId(txnRefs, type_enum=1) → target GL
- ClerkWithdrawalRequestApiResource: minor updates
- DB migration: changelog-tenant.xml + 0333_rename_disbursed_to_withdrawn.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:27:32 +05:30
Jaanu423andClaude Sonnet 4.6 a6eae3d6e8 feat(clerk): add GL disbursement maker-checker backend
- Add ClerkDisbursementRequestApiResource with submit/list/my/approve/reject endpoints
- Approve posts savings/current deposits then offsets DR source GL / CR Cash at Vault
- Add Liquibase migration for m_clerk_disbursement_request table (IF NOT EXISTS indexes)
- Add VoucherService constants TYPE_DISBURSEMENT and REF_CLERK_DISBURSEMENT_REQUEST

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:44:04 +05:30
Jaanu423andClaude Sonnet 4.6 3e1abaef03 fix(cashier): fix NumberFormatException on GL deposit approval and add GL maker-checker flow
- Use parseTxnIdToLong() for voucherService.linkSavingsTransaction() to
  handle hex GL txnId (e.g. "a2070e542bab") without NumberFormatException
- Add GL deposit maker-checker: submit stores gl_account_id, contra_gl_id,
  office_id, currency_code; approve routes to executeGlJournalEntry()
- Save denomination at submit time (cash physically received); update
  txn_id to real Fineract ID on approve, delete on reject
- Add Liquibase migration 0331 to add GL columns to m_cashier_deposit_request

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 14:59:28 +05:30
Jaanu423andClaude Sonnet 4.6 40449ef0f7 fix(cashier+clerk): route CA deposits and withdrawals to current account commands
CashierDepositRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountDeposit() instead of executeSavingsDeposit()
- Add executeCurrentAccountDeposit() using CommandWrapperBuilder.currentAccountDeposit()
- Add isCurrentAccountById() helper querying m_current_account table
- Add POST /{id}/reprocess endpoint to fix already-approved CA records that
  were wrongly posted to savings account

ClerkWithdrawalRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountWithdrawal() instead of executeSavingsWithdrawal()
- Add executeCurrentAccountWithdrawal() using CommandWrapperBuilder.currentAccountWithdrawal()
- Add isCurrentAccountById() helper querying m_current_account table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:52:09 +05:30
Jaanu423andClaude Sonnet 4.6 33ea10689f feat(cashier+voucher): add cashier deposit maker-checker flow and central voucher system
- VoucherService: central voucher register with globally unique voucher numbers
- CashierDepositRequestApiResource: full submit/approve/reject/list flow
  - Cashier submits deposit request; balance credited only on manager approval
  - Self-approval blocked (maker cannot be checker)
  - Role guard: only manager/head-cashier/ALL_FUNCTIONS can approve/reject
  - Race condition guard: WHERE status='PENDING' on approve/reject UPDATE
  - Clean JSON error responses (no 500 stack traces) via apiError() helper
  - Denomination total validation at submit and approve
- ClerkWithdrawalRequestApiResource: refactored to use m_voucher
  - Voucher created at submit, linked to savings txn at approve
  - Same role guard and self-approval block as deposit flow
  - Disburse: denomination validation, null denoms for non-cash payments
- Migrations 0327-0330:
  - 0327: add voucher_no to m_clerk_withdrawal_request
  - 0328: add voucher_no to m_savings_account_transaction
  - 0329: create m_voucher central table (globally unique voucher_no)
  - 0330: create m_cashier_deposit_request table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:53:47 +05:30
Jaanu423andClaude Sonnet 4.6 8d5b4d1ce9 feat(clerk-withdrawal): add maker-checker withdrawal disbursement API
- ClerkWithdrawalRequestApiResource: full CRUD for withdrawal requests
- Endpoints: submit, list, myRequests, getById, getByToken, approve, reject, disburse, update
- Token uniqueness validated per day (validateTokenUniqueToday)
- Cashier disburse: amount match, race condition guard (WHERE status=APPROVED)
- Disbursement denomination saved to m_savings_txn_denomination for vault position tracking
- Liquibase migrations: 0321-0326 (create table, tok_no, manager fields, disbursement fields, drop unused counter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:00:57 +05:30
Jaanu423andClaude Sonnet 4.6 db94056144 feat(cashier): add server-side pagination and date ordering to GL entries endpoint
- Add offset/limit query params with defaults (limit=20, offset=0)
- Return totalFilteredRecords + pageItems response structure
- Compute totalDr/totalCr from all records (not just current page)
- Fix ordering: ORDER BY MAX(created_at) for correct chronological sort

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:53:49 +05:30
Jaanu423andClaude Sonnet 4.6 19639c9e02 feat(cashier): add GL denomination tracking, live GL balance, and enhanced closing report
- Add CashierGlDenominationApiResource: new POST /v1/cashier/gl-denomination endpoint
  to record denomination breakdown for GL journal entries in m_savings_txn_denomination
- Fix GL account real-time balance in GLAccountReadPlatformServiceImpl using live SUM query
- Add Liquibase migrations 0318/0319/0320: make savings_account_id and txn_id nullable,
  add payment_type column to m_savings_txn_denomination
- Add validCheckSum to 0317b changeset to resolve checksum mismatch on existing installs
- Enhance GL entries closing report SQL with date, time, teller, payment type, txn source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:31:12 +05:30
Jaanu423andClaude Sonnet 4.6 53b6a94bf8 fix(cashier): fix GL account search SQL bug, running balance, and add CASHIER role permissions
- Fix searchParam SQL syntax: parameterized LIKE with %value% instead of literal %?%
- Fix organizationRunningBalance: correlated subquery for latest journal entry (ORDER BY id DESC LIMIT 1)
- Migration 0317: grant READ_GLACCOUNT, READ_LOAN, READ_SHAREACCOUNT to CASHIER role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 11:17:32 +05:30
Jaanu423andClaude Sonnet 4.6 6d0c1fe7f6 feat(clerk): add clerk role with cashier teller workstation permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:27:04 +05:30
Jaanu423andClaude Sonnet 4.6 edaeb674d5 feat(cashier): fix current account transactions search and withdrawal validations
CurrentAccountTransactionsApiResource:
- Fixed: search endpoint now queries m_current_account_transaction
  instead of m_savings_account_transaction (wrong table)
- Added remarks column to search query (column exists in CA table)
- Charge Deduction (type=7) mapped in transactionType value
- Removed sat.remarks that caused 500 (column doesn't exist in savings table)

CurrentAccountTransactionWritePlatformServiceImpl:
- Added min required balance validation in withdrawal()
- Throws GeneralPlatformDomainRuleException when balance after withdrawal
  would fall below minRequiredBalance (if enforceMinRequiredBalance=true)

StandingInstructionReadPlatformServiceImpl:
- Fixed SQL WHERE clause: fromAccountType + fromAccount condition
  now correctly adds WHERE when fromAccountId is set

Liquibase 0315:
- Fix standing instruction permission code (trim trailing space on id=449)
- Grant READ permissions to CASHIER role for transaction entry screen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 22:27:59 +05:30
Jaanu423andClaude Sonnet 4.6 2371ad98b1 feat(cashier): add role-based data filtering and pagination for teller workstation
- Add CashierRoleHelper service: detects CASHIER role via DB permission
  check, resolves cashier ID from staff/user linkage
- Role-based filtering on GET list endpoints (cashtransfer, cashsettlement,
  cashintercashier, cashexchange, cashreports): CASHIER sees only own data,
  ADMIN/HEAD sees all
- Pagination (offset/limit) added to all cashier list APIs returning
  standard Fineract Page format {totalFilteredRecords, pageItems}
- ShareCollectionApiResource: collect cash before account activation,
  post GL entries (Dr Cash Vault / Cr Share Sundry / Cr Income from Fees),
  mark charges paid to prevent duplicate GL on activation,
  GET status endpoint for collection check
- Migration 0315: grant READ_CLIENTKYC, READ_CLIENTNOMINEE, READ_ADDRESS,
  READ_FAMILYMEMBERS, READ_SAVINGSACCOUNT to CASHIER role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:00:55 +05:30
Jaanu423andClaude Sonnet 4.6 9d944d0196 feat(share): add cashier share collection endpoint with GL posting and activation validation
- New POST /cashier/sharecollection endpoint: collects cash for share purchase,
  posts Dr Cash Vault / Cr Share Sundry / Cr Income GL entries, marks charges
  as paid and sets cashier_collected flag on the account
- GET /cashier/sharecollection/:id endpoint for collection status check
- Block share account activation if cashier has not collected cash first
  (validates m_share_cashier_collection record within last 30 days)
- Skip duplicate charge GL on activation when charge already collected upfront
- Suppress automatic GL posting at account submission time to prevent
  double entries; GL is now posted only via cashier collection endpoint
- Fix JdbcTemplate injection in ShareAccountWritePlatformServiceJpaRepositoryImpl
  via ShareAccountsConfiguration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 13:22:33 +05:30
Jaanu423andClaude Sonnet 4.6 6e31ac9acd fix(savings): allow modifyApplication for rejected Savings/FD/RD accounts
SavingsAccount.modifyApplication() previously blocked modification unless
the account was in SUBMITTED_AND_PENDING_APPROVAL state. Extended to also
allow REJECTED state so bank users can amend and re-submit rejected applications.

When a rejected account is modified, status resets to
SUBMITTED_AND_PENDING_APPROVAL (rejectedOnDate and rejectedBy cleared)
so the account re-enters the approval workflow.

Applies to Savings, FD, and RD accounts (all extend SavingsAccount).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:55:39 +05:30
Jaanu423andClaude Sonnet 4.6 a051faba5d feat(accounts): backend fixes for Modify Application feature (Current/Savings/FD/RD)
Current Account:
- CurrentAccountReadPlatformServiceImpl: add retrieveJointHolders() and retrievePartners()
- CurrentAccountWritePlatformServiceImpl: full modifyApplication() - updates all CBS fields,
  joint holders, partners, nominees, facility config, dates, introducer
- Fix cbsAccountType save: parse string account type from 'accountType' JSON field

Nominees:
- ClientNomineeWritePlatformService: add replaceNomineesForAccount() for atomic replace
- Savings/FD/RD modifyApplication: replace nominees on modify

FD/RD Read (DepositAccountReadPlatformServiceImpl):
- Add isSeniorCitizen, seniorCitizenInterestRate to BASE SQL SELECT
- Add setCbsAccountFields() call in FD/RD mappers reading accountStatus, modeOfOperation,
  introducerClientId, guardianFields, linkedAccountId, isSeniorCitizen from ResultSet
- Fix retrieveOne() to also fetch joint holders from m_savings_account_joint_holders
- Use explicit LinkedHashMap in joint holder query (camelCase keys for Angular)

FD/RD Data Classes:
- FixedDepositAccountData: add accountStatus, modeOfOperation, introducerClientId,
  linkedAccountId, jointHolders, isSeniorCitizen, seniorCitizenInterestRate fields
- RecurringDepositAccountData: same fields added
- Fix all factory methods (withInterestChartAndRecurringDetails, withClosureTemplateDetails,
  withTemplateOptions, associationsAndTemplate) to call copyCbsFieldsFrom()
- Fix SavingsAccountReadPlatformServiceImpl joint holders: use camelCase keys

FD Calculator:
- FixedDepositAccountInterestCalculationServiceImpl: add calculateExact() with actual
  calendar-based calculation matching PostingPeriod engine (exact same as account creation)
- Add submittedOnDate, daysInYear, interestPostingPeriodInMonths optional params
- Support interestCompoundingPeriodInMonths=0 (daily compounding) in both validator and calc
- DepositsApiConstants: add submittedOnDate/dateFormat/locale/daysInYear to allowed params
- DepositAccountDataValidator: allow 0 (daily) in interestCompoundingPeriodInMonths

Misc:
- SavingsProductData: null-check in isCashBased/isAccrualBased methods (NPE fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:32:59 +05:30
Jaanu423andClaude Sonnet 4.6 7c4cf6260c feat(cashier): GL entries endpoint + remove vault-cashier GL postings
- GET /cashreports/gl-entries: returns voucher_no, ac_no, ac_name
  (customer name), dr (withdrawal), cr (deposit) for cashier transactions
  with optional cashierId/tellerId filter
- TellerWritePlatformServiceJpaImpl: removed ALLOCATE/SETTLE GL entries
- OrganisationTellerConfiguration: removed obsolete GL constructor args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:47:22 +05:30
Jaanu423andClaude Sonnet 4.6 6bdcac7b77 feat(cashier): remove GL entries for vault-to-cashier transfers (Option A)
ALLOCATE (vault→cashier) and SETTLE (cashier→vault) no longer post
DR CASH_AT_TELLER / CR CASH_AT_MAINVAULT journal entries.

Rationale: Cash transfer between vault and teller drawer is an internal
physical movement — the denomination/amount is already tracked operationally
via m_cash_transfer and m_cash_settlement tables. A single Cash in Hand GL
(CASH_AT_MAINVAULT) covers all bank cash. GL entries now only occur on
actual customer deposit/withdrawal transactions (SAVINGS_REFERENCE ↔
SAVINGS_CONTROL), keeping the GL clean and RBI-compliant.

Removed unused imports and field injections:
  JournalEntryRepository, FinancialActivityAccountRepositoryWrapper,
  FinancialActivity, FinancialActivityAccount, GLAccount,
  JournalEntry, JournalEntryType

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 07:13:48 +05:30
Jaanu423andClaude Sonnet 4.6 8bf28a8033 fix(cashier): prevent duplicate cashier lookup crash + add missing migrations
- DenominationBreakdownWriter: add ORDER BY mc.id DESC LIMIT 1 to all cashier
  lookup queries so multiple cashier records per staff do not crash queryForObject
- CashReportApiResource: same LIMIT 1 fix for cashier-day-status and position endpoints
- 0311: fix m_client and m_savings_account NOT NULL columns (created_by, last_modified_by,
  created_on_utc, last_modified_on_utc, cif_number, is_lien_allowed, etc.)
- 0312: create CASHIER role if missing + grant all 34 cashier permissions (incl FD/RD)
- 0313: add missing m_cash_exchange columns (cashier_id, is_reversed, reversed_by,
  reversed_at, reverse_reason, customer_id_proof)
- 0314: unique constraint on m_cashiers.staff_id to prevent duplicate allocations
- application.properties: restore local postgres password after merge
- ExecuteOverdueAndCurrentStandingInstructionsTest: fix constructor call (5 args)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:23:31 +05:30
Jaanu423 d4262ebf6f Merge remote-tracking branch 'origin/feature/switch-integration-backend' into feature/account-charges 2026-06-01 13:23:22 +05:30
Jaanu423andClaude Sonnet 4.6 656a0b4f13 feat(cashier): add morning transfer validation & fix denomination SQL
- validateCashierHasMorningTransfer(): blocks all txns when no today's cash transfer in m_cash_transfer
- validatePreviousDayClosed(): blocks when previous business day unsettled
- Both validations called on deposit AND withdrawal in Savings, Current, FD, RD services
- cashier-day-status endpoint now returns morningTransferReceived field
- getCashierTotalCash SQL fixed: dm.denomination_value -> dm.denomination

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:15:52 +05:30
Aditya ae7017ca81 @
feat(fundtransfer+imps+mfa): fund transfer module, IMPS switch client, MFA log API

- Fund transfer: NEFT/RTGS/IMPS/internal strategies, limit resolution, bulk
  transfer, verification tokens, suspense GL, recall/approval flow, jobs
- IMPS CBS switch client with crypto (JWT/envelope) and transaction domain
- MFA log read API (audit of MFA events)
- Liquibase migrations 0300-0311 for fund transfer / IMPS tables, permissions,
  roles, and suspense account
- JobName, SecurityConfig, dependencies.gradle wiring

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-01 12:18:30 +05:30
Aditya 6413befe07 Merge self-hosted/feature/account-charges into feature/Backend-DSC-MFA 2026-06-01 12:14:49 +05:30
Jaanu423andClaude Sonnet 4.6 eae5b03d7e feat(cashier): denomination withdrawal validation, exchange detail fix, cash exchange in excess report
- Savings/Current/FD/RD withdrawal services: call validateWithdrawalStock() before
  validateBreakdown() to block withdrawal when cashier lacks the requested denomination
- CashExchangeApiResource: validate cashier denomination stock before giving to customer;
  fix saveAndFlush (EclipseLink does not flush on save() — getId() returned null causing
  exchange_id NOT NULL violation in m_cash_exchange_detail)
- CashReportApiResource excess(): include cash exchanges >= threshold in result,
  add type field (SAVINGS_TXN/EXCHANGE) and idProof for RBI/AML compliance;
  combined list sorted by amount DESC
- CashReportApiResource closing(): add denomination-wise exchange rows (already present)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:11:44 +05:30
Jaanu423andClaude Sonnet 4.6 49d64004cc feat(cashier): add cashier-morning balance endpoint and expose fromCashierId in settlement/transfer APIs
- GET /cashreports/cashier-morning: returns denomination-wise cash received by cashier from vault
  for a given date (only vault→cashier transfers, non-reversed)
- CashSettlementApiResource: include fromCashierId in list and detail responses
- CashTransferApiResource: include toCashierId in list and detail responses for traceability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 08:55:09 +05:30
Jaanu423 ada24d7497 feat(cashier): implement expected balance for logged-in cashier, settlement checks, and reconciliation rules 2026-05-28 22:28:30 +05:30
Jaanu423andClaude Sonnet 4.6 b89e653bcb feat: cash management backend - transfer, settlement, inter-cashier, reconciliation + cashier /me position endpoint
OVERVIEW
--------
Added complete cash management backend covering the full cashier workflow:
vault transfers, inter-cashier transfers, end-of-day settlement, GL
reconciliation, and a new authenticated /me endpoint for cashier position
lookup without needing to pass a cashier ID.

NEW API ENDPOINT
----------------
GET /cashreports/cashier-position/me
- Auto-detects the authenticated user via PlatformSecurityContext
- Resolves appUser -> staff -> cashier via m_cashiers JOIN chain
- Accepts optional ?asOfDate=YYYY-MM-DD query param (defaults to today)
- Reuses existing buildPositionForCashier(cashierId, dateFilter) internally
- Returns: { cashierId, asOfDate, totalAmount, denominations[] }
- Non-cashier users get empty denominations array (no error thrown)
- Used by Angular UI to show opening/closing denomination panels

CASH TRANSFER (vault -> cashier)
---------------------------------
CashTransferApiResource.java   — REST endpoints (POST /cashtransfer, GET list/detail)
CashTransfer.java              — JPA entity (m_cash_transfer)
CashTransferDetail.java        — JPA entity (denomination breakdown, m_cash_transfer_detail)
CashTransferRepository.java    — Spring Data JPA repository
CashTransferDetailRepository.java

DB migrations:
  0014_cash_transfer.xml       — creates m_cash_transfer + m_cash_transfer_detail tables
  0015_cash_transfer_add_ids.xml — adds missing ID columns to transfer detail

CASH SETTLEMENT (end-of-day cashier close)
------------------------------------------
CashSettlementApiResource.java — REST endpoints (POST /cashsettlement, GET list/detail)
CashSettlement.java            — JPA entity (m_cash_settlement)
CashSettlementDetail.java      — JPA entity (denomination breakdown)
CashSettlementRepository.java
CashSettlementDetailRepository.java

DB migration:
  0016_cash_settlement.xml     — creates m_cash_settlement + m_cash_settlement_detail

INTER-CASHIER TRANSFER
-----------------------
CashInterCashierApiResource.java — REST endpoints (POST /cashintercashier, GET list/detail)
CashInterCashier.java            — JPA entity (m_cash_inter_cashier)
CashInterCashierDetail.java      — JPA entity (denomination breakdown)
CashInterCashierRepository.java
CashInterCashierDetailRepository.java

DB migration:
  0017_cash_inter_cashier.xml  — creates m_cash_inter_cashier + detail table

CASH RECONCILIATION
--------------------
CashReconciliationApiResource.java — REST endpoints (POST /cashreconciliation, GET list)
CashReconciliation.java            — JPA entity (m_cash_reconciliation)
CashReconciliationDetail.java      — JPA entity (denomination breakdown)
CashReconciliationRepository.java
CashReconciliationDetailRepository.java

DB migrations:
  0018_cash_reconciliation.xml — creates m_cash_reconciliation + detail table
  0019_cash_reversal_columns.xml — adds reversal tracking columns
  0020_cash_permissions.xml    — adds permissions for all cash management APIs

GL DISPATCH SERVICE
-------------------
GlDispatchService.java (portfolio/teller/service/)
- Shared service for posting GL journal entries for cash movements
- Used by settlement and reconciliation flows

CHANGELOG
---------
changelog-tenant.xml updated to include migrations 0014 through 0020.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:40:57 +05:30
Jaanu423andClaude Opus 4.7 9a30ede278 feat: denomination save+validation for all deposit types + cash exchange backend
- DenominationBreakdownWriter helper: validates breakdown total == amount and
  persists per-denomination rows; wired into current, FD and RD deposit flows
  (savings already had inline save) so all 4 deposit types now record the
  cash denomination breakdown.
- CIF on denomination rows now sourced from the client (getCifNumber) instead
  of the account external id.
- Allow denominationBreakdown param in the FD/RD transaction validator.
- Cash Exchange backend: m_cash_exchange + m_cash_exchange_detail tables,
  entities, repositories and JAX-RS API with received==given validation;
  registered the 0013 migration in changelog-tenant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:36:00 +05:30
Jaanu423andClaude Opus 4.7 ee915e395a feat: Denomination Master backend - master, audit, soft delete, txn breakdown
- m_denomination_master CRUD API with duplicate (denomination+kind)
  validation, soft delete + restore, and a full audit trail
  (m_denomination_master_audit logs CREATE/UPDATE/ACTIVATE/DEACTIVATE/
  DELETE/RESTORE with snapshot, changes, user and timestamp)
- m_savings_txn_denomination: persist per-transaction cash denomination
  breakdown on savings deposit; allow denominationBreakdown request param
- Liquibase 0012_denomination_master registered in tenant changelog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:16:52 +05:30
Jaanu423andClaude Sonnet 4.6 7d4f409202 feat: fix standing instructions GL accounting for current account transfers
- AccountTransfersWritePlatformServiceImpl:
  * Direct GL posting for current↔savings/current transfers:
    DR debit-side account GL (with correct account entity_id)
    CR credit-side account GL (with correct account entity_id)
  * Prefix current account txn_id with CA (not C) so
    JournalEntryReadPlatformServiceImpl joins m_current_account
    and shows correct current account number on GL entries
  * Suppress redundant savings deposit/withdrawal GL entries
    (skipAccountingPostings=true) when current account handles
    the full double-entry directly
  * Separate performCurrentWithdrawalForLoan for Current→Loan
    transfers (loan domain service handles the other GL leg)
  * Reuse existing AccountTransferDetails for SI recurring runs
    so transactions appear in list-account-transactions history

- SavingsAccountDomainService / SavingsAccountDomainServiceJpa:
  * New handleDeposit overload with skipAccountingPostings flag
  * handleWithdrawal respects SavingsTransactionBooleanValues
    .isSkipAccountingPostings() to suppress redundant GL entries

- SavingsTransactionBooleanValues:
  * Added skipAccountingPostings field + overloaded constructor

- AccountingProcessorHelper:
  * New createDirectTransferJournalEntriesForSavings() method
    creates DR and CR entries with separate productId/entityId
    so each GL line shows the correct account number

- JournalEntryReadPlatformServiceImpl:
  * Join both m_savings_account and m_current_account always
  * COALESCE(ca.account_no, sa.account_no) to resolve correct
    account number regardless of transaction_id prefix

- ExecuteStandingInstructionsTasklet:
  * Load existing AccountTransferDetails via repository so
    subsequent SI runs link to the same transfer detail record

- ExecuteStandingInstructionsConfig / AccountConfiguration:
  * Wire AccountTransferDetailRepository and
    AccountingProcessorHelper into the relevant beans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:30:04 +05:30
Jaanu423andClaude Sonnet 4.6 2251fd2259 feat: current account standing instructions - full backend support
- StandingInstructionData: rename conflicting Lombok getters (getInstructionType,
  getRecurrenceType, getRecurrenceFrequency, getFromAccountType, getToAccountType,
  getTransferType) to domain enum methods so Jackson serializes EnumOptionData fields
  as {id, code, value} objects instead of plain strings
- AccountTransfersWritePlatformServiceImpl: add current account transfer support for
  all 4 missing cases (Current->Savings, Savings->Current, Current->Current,
  Current->Loan) with performCurrentWithdrawal/performCurrentDeposit helpers
- AccountConfiguration: wire CurrentAccountRepository and CurrentAccountTransactionRepository
  into AccountTransfersWritePlatformService bean
- StandingInstructionApiResource: fix getValue() type mismatch after getter rename
- ExecuteStandingInstructionsTasklet: update to use renamed enum accessor methods
- PortfolioAccountType: add CURRENT(3) type support
- DB migration: 0301_add_current_account_to_transfer_details.xml adds
  from_current_account_id and to_current_account_id columns to transfer details table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:55:35 +05:30
AdityaandClaude Opus 4.7 45585ecc10 Fix family-member PUT 500; CORS POST 403; Hikari local password default
- ClientFamilyMembersWritePlatformServiceImpl#updateFamilyMember: guard
  maritalStatusId/genderId/professionId with != null instead of != 0 so a
  payload that omits these keys doesn't NPE when the boxed Long is unboxed.
- application.properties: fix the broken property name
  `fineract.security.cors.allowed-metho pas` -> `allowed-methods`. The typo
  left CORS allowed-methods empty, which made Spring reject every browser
  POST with "Invalid CORS request" 403 once the SPA sent an Origin header.
- application.properties: change the Hikari default password from 313863
  to root so the local devRun matches the tenant-DB default at line 49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:07:24 +05:30
AdityaandClaude Opus 4.7 7dea88d30f Fix family-member PUT 500; CORS POST 403; Hikari local password default
- ClientFamilyMembersWritePlatformServiceImpl#updateFamilyMember: guard
  maritalStatusId/genderId/professionId with != null instead of != 0 so a
  payload that omits these keys doesn't NPE when the boxed Long is unboxed.
- application.properties: fix the broken property name
  `fineract.security.cors.allowed-metho pas` -> `allowed-methods`. The typo
  left CORS allowed-methods empty, which made Spring reject every browser
  POST with "Invalid CORS request" 403 once the SPA sent an Origin header.
- application.properties: change the Hikari default password from 313863
  to root so the local devRun matches the tenant-DB default at line 49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:06:38 +05:30
Jaanu423 d32953ed76 Merge branch 'feature/account-charges' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/account-charges 2026-05-21 21:00:05 +05:30
AdityaandClaude Opus 4.7 5a98a89839 KYC: PAN entity-branch, defensive LincPay parse, family/nominee rules for non-individuals
PAN verify for non-individual clients was sending name="" + dob=null to LincPay because
the code path always read firstname/middlename/lastname (NULL on entities) and
client.dateOfBirth (also NULL on entities). LincPay then returned plain-text "Invalid
DOB format. Required format is dd/MM/yyyy", which Gson can't parse (MalformedJsonException
at line 1 col 10), surfacing as a useless 503. Now ClientKycWritePlatformServiceJpa-
RepositoryImpl.verifyPan branches on LegalForm:
  - PERSON  → name = firstname + middlename + lastname,    dob = client.dateOfBirth
  - ENTITY  → name = client.fullname,                       dob = clientNonPerson.dateOfRegistration
Injected ClientNonPersonRepositoryWrapper for the entity branch.

ClientNonPerson got two missing getters (getConstitution, getDateOfRegistration) needed
by the above plus the family/nominee rules below.

Defensive JSON parse in LincPayKycVerificationServiceImpl: parseJson now returns null
when the body doesn't even start with '{' or '[' (i.e. vendor sent plain text or HTML),
logs the literal body at WARN, and every verify method (PAN, GSTIN, Aadhaar OTP gen,
Aadhaar OTP verify, CIN, DIN) checks for null and returns
  "<X> verification failed — vendor returned non-JSON: <first 200 chars>"
So future vendor outages surface the actual response text instead of a MalformedJson-
Exception with no context.

Non-individual client rules:
- ClientFamilyMembersWritePlatformService: addFamilyMember / addClientFamilyMember /
  updateFamilyMember now refuse with PlatformApiDataValidationException when the client
  is an entity. Family members are an individual-only concept. Delete is intentionally
  unguarded so legacy rows can be cleaned up.
- ClientNomineeWritePlatformService: addNominee / updateNominee refuse for non-individual
  clients UNLESS the constitution normalises to PROPRIETORSHIP (matches both
  "PROPRIETOR SHIP" and "PROPRIETORSHIP"). Proprietorships are operationally individuals
  with a separate trade name, so they keep nominee support; companies / LLPs / trusts /
  societies / HUFs / partnerships don't carry a nomination concept. Delete unguarded.

Verified the LincPay endpoints directly:
  /validate-pan   AQRPV8345E + "Aditya Vyas" + 19/12/1988      → valid (individual)
  /validate-pan   AAMCG5406B + "GIZMOGLADE..." + 01/12/2025    → valid (company)
  /validate-pan   AAMCG5406B + ""             + ""             → "Invalid DOB format" plain text (the failing path)
  /validate-gstn  23AADCL9984A1ZH                              → 200 with full company data
  /validate-gstn  21AAMCG5406BIZ6                              → 400 "Invalid GSTIN pattern" (bad checksum, not our bug)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:46:34 +05:30
Aditya d5157ef701 Merge remote-tracking branch 'self-hosted/feature/account-charges' into feature/Backend-DSC-MFA 2026-05-21 16:15:28 +05:30
AdityaandClaude Opus 4.7 c6f130e116 Fix BadSqlGrammarException on GET /clients?status=pending — missing SELECT column
The second schema() builder used by the clients-list query had the JOIN
  left join m_client gc on gc.id = c.guardian_client_id
and the SELECT
  c.guardian_client_id as guardianClientId
but was missing
  gc.display_name as guardianClientDisplayName
which the RowMapper still reads (rs.getString("guardianClientDisplayName")).

Result: PSQLException "The column name guardianClientDisplayName was not found
in this ResultSet" → BadSqlGrammarException → 500 on any client-list pull.

The matching first builder (~line 327) had both columns correctly. Added the
missing SELECT line to keep the pair consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:35:59 +05:30
610 changed files with 125699 additions and 383667 deletions
+1 -1
View File
@@ -1 +1 @@
{"sessionId":"0fda6e48-a79d-4980-a9ee-31a6d5f735e1","pid":15548,"acquiredAt":1776168814856} {"sessionId":"b5222d29-c337-4c5d-afde-70cde98cdf46","pid":426448,"acquiredAt":1780052566378}
+26 -1
View File
@@ -52,7 +52,32 @@
"Bash(git -c commit.gpgsign=false commit -m ' *)", "Bash(git -c commit.gpgsign=false commit -m ' *)",
"Bash(git pull *)", "Bash(git pull *)",
"Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)", "Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)",
"Bash(git ls-remote *)" "Bash(git ls-remote *)",
"Bash(PGPASSWORD=root psql -h localhost -p 5432 -U postgres -d fineract_default -c \"SELECT cv.id, cv.code_value FROM m_code_value cv JOIN m_code c ON cv.code_id=c.id WHERE c.code_name='Constitution' ORDER BY cv.id;\")",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AQRPV8345E\",\"name\":\"Aditya Vyas\",\"dob\":\"19/12/1988\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"GIZMOGLADE PRIVATE LIMITED\",\"dob\":\"01/12/2025\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"\",\"dob\":\"\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"21AAMCG5406BIZ6\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"23AADCL9984A1ZH\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"PowerShell(Get-Process | Where-Object { $_.ProcessName -match '^\\(java|gradle\\)' } | Select-Object Id, ProcessName, StartTime, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}} | Format-Table -AutoSize)",
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -ErrorAction SilentlyContinue)",
"PowerShell(Stop-Process -Id 3468,3864,39964 -Force)",
"PowerShell(Get-Process -Id 3468,3864,39964 -ErrorAction SilentlyContinue)",
"PowerShell(foreach \\($id in 3468,3864,39964\\) { try { Stop-Process -Id $id -Force -ErrorAction Stop; \"killed $id\" } catch { \"skip $id - $\\($_.Exception.Message\\)\" } })",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue)",
"PowerShell(if \\(-not \\(Test-Path .\\\\fineract-provider\\\\logs\\)\\) { New-Item -ItemType Directory -Force .\\\\fineract-provider\\\\logs | Out-Null }; Remove-Item .\\\\fineract-provider\\\\logs\\\\devrun.out -ErrorAction SilentlyContinue; \"ready\")",
"PowerShell($j = Get-Process java -ErrorAction SilentlyContinue | Select-Object Id, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}}, StartTime; $j | Format-Table -AutoSize; \"---\"; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, OwningProcess)",
"PowerShell(if \\(Test-Path .\\\\fineract-provider\\\\logs\\\\devrun.out\\) { $f = Get-Item .\\\\fineract-provider\\\\logs\\\\devrun.out; \"size=$\\($f.Length\\) bytes mtime=$\\($f.LastWriteTime\\)\" } else { \"devrun.out missing\" })",
"PowerShell(netstat -ano)",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 800; $left = Get-Process java -ErrorAction SilentlyContinue; if \\($left\\) { $left | Select-Object Id, ProcessName } else { \"no java processes\" }; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Measure-Object | ForEach-Object { \"8443 listeners: $\\($_.Count\\)\" })",
"Bash(curl -sk -o /tmp/resp.txt -w \"STATUS=%{http_code}\\\\nCONTENT_TYPE=%{content_type}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -sk -o /tmp/r1.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r2.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r3.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Origin: http://localhost:4200' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r4.txt -w \"STATUS=%{http_code}\\\\n\" -X OPTIONS 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Origin: http://localhost:4200' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,fineract-platform-tenantid' -i)",
"Bash(git cherry-pick *)",
"PowerShell($parent = 'C:\\\\Users\\\\adity\\\\Music\\\\CBS'; if \\(Test-Path $parent\\) { \"parent exists: $parent\"; Get-ChildItem $parent -Directory | Select-Object -ExpandProperty Name } else { \"MISSING parent: $parent\" }; \"---\"; $target = Join-Path $parent 'LincpayEKYC'; if \\(Test-Path $target\\) { \"TARGET ALREADY EXISTS: $target\" } else { \"target free: $target\" })",
"Bash(git clone *)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api", "c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api",
+40 -37
View File
@@ -1,37 +1,40 @@
*.class *.class
*.jar *.jar
*.war *.war
*.ear *.ear
.classpath .classpath
.project .project
.settings .settings
.gradle .gradle
build build
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
*.swp *.swp
*.DS_Store *.DS_Store
.idea .idea
.vscode .vscode
catalina.base_IS_UNDEFINED/ catalina.base_IS_UNDEFINED/
keystore.jks keystore.jks
bin/ bin/
.lock .lock
fineract-provider/out/ fineract-provider/out/
fineract-provider/config/swagger/config.json fineract-provider/config/swagger/config.json
fineract-provider/config/swagger/fineract-input.yaml fineract-provider/config/swagger/fineract-input.yaml
licenses licenses
**/.asciidoctor/images/* **/.asciidoctor/images/*
**/.asciidoctor/diagram/* **/.asciidoctor/diagram/*
**/images/diag-*.svg **/images/diag-*.svg
fineract-provider/src/main/generated/ fineract-provider/src/main/generated/
**/out/ **/out/
gradleExp/ gradleExp/
.run/ .run/
.java-version .java-version
.windsurf/ .windsurf/
# Application logs
fineract-provider/logs/
@@ -1,272 +1,284 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.accounting.glaccount.service; package org.apache.fineract.accounting.glaccount.service;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingEnumerations; import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup; import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage; import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException; import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException; import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData; import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService { public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService {
private static final String NAME_DECORATED_BASE_ON_HIERARCHY = "concat(substring('........................................', 1, ((LENGTH(hierarchy) - LENGTH(REPLACE(hierarchy, '.', '')) - 1) * 4)), name)"; private static final String NAME_DECORATED_BASE_ON_HIERARCHY = "concat(substring('........................................', 1, ((LENGTH(hierarchy) - LENGTH(REPLACE(hierarchy, '.', '')) - 1) * 4)), name)";
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private static final String SQL_AND = " and "; private static final String SQL_AND = " and ";
private static final class GLAccountMapper implements RowMapper<GLAccountData> { private static final class GLAccountMapper implements RowMapper<GLAccountData> {
private final JournalEntryAssociationParametersData associationParametersData; private final JournalEntryAssociationParametersData associationParametersData;
GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) { GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) {
if (associationParametersData == null) { if (associationParametersData == null) {
this.associationParametersData = new JournalEntryAssociationParametersData(); this.associationParametersData = new JournalEntryAssociationParametersData();
} else { } else {
this.associationParametersData = associationParametersData; this.associationParametersData = associationParametersData;
} }
} }
public String schema() { public String schema() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append( sb.append(
" gl.id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, ") " gl.id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, ")
.append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ") .append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ")
.append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ") .append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ")
.append("cv.id as codeId, cv.code_value as codeValue "); .append("cv.id as codeId, cv.code_value as codeValue ");
if (this.associationParametersData.isRunningBalanceRequired()) { if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append(",gl_j.organization_running_balance as organizationRunningBalance "); sb.append(",gl_j.organization_running_balance as organizationRunningBalance ");
} }
sb.append("from acc_gl_account gl left join m_code_value cv on tag_id=cv.id "); sb.append("from acc_gl_account gl left join m_code_value cv on tag_id=cv.id ");
if (this.associationParametersData.isRunningBalanceRequired()) { if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append("left outer Join acc_gl_journal_entry gl_j on gl_j.account_id = gl.id"); sb.append("left outer join acc_gl_journal_entry gl_j on gl_j.id = (")
} .append("select id from acc_gl_journal_entry ")
return sb.toString(); .append("where account_id = gl.id ")
} .append("order by id desc limit 1) ");
}
@Override return sb.toString();
public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { }
final Long id = rs.getLong("id"); @Override
final String name = rs.getString("name"); public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long parentId = JdbcSupport.getLong(rs, "parentId");
final String glCode = rs.getString("glCode"); final Long id = rs.getLong("id");
final boolean disabled = rs.getBoolean("disabled"); final String name = rs.getString("name");
final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed"); final Long parentId = JdbcSupport.getLong(rs, "parentId");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); final String glCode = rs.getString("glCode");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); final boolean disabled = rs.getBoolean("disabled");
final int usageId = JdbcSupport.getInteger(rs, "accountUsage"); final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed");
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId); final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final String description = rs.getString("description"); final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final String nameDecorated = rs.getString("nameDecorated"); final int usageId = JdbcSupport.getInteger(rs, "accountUsage");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId"); final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId);
final String codeValue = rs.getString("codeValue"); final String description = rs.getString("description");
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue); final String nameDecorated = rs.getString("nameDecorated");
Long organizationRunningBalance = null; final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
if (associationParametersData.isRunningBalanceRequired()) { final String codeValue = rs.getString("codeValue");
organizationRunningBalance = rs.getLong("organizationRunningBalance"); final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
} Long organizationRunningBalance = null;
return new GLAccountData().setId(id).setName(name).setParentId(parentId).setGlCode(glCode).setDisabled(disabled) if (associationParametersData.isRunningBalanceRequired()) {
.setManualEntriesAllowed(manualEntriesAllowed).setType(accountType).setUsage(usage).setDescription(description) organizationRunningBalance = rs.getLong("organizationRunningBalance");
.setNameDecorated(nameDecorated).setTagId(tagId).setOrganizationRunningBalance(organizationRunningBalance); }
} return new GLAccountData().setId(id).setName(name).setParentId(parentId).setGlCode(glCode).setDisabled(disabled)
} .setManualEntriesAllowed(manualEntriesAllowed).setType(accountType).setUsage(usage).setDescription(description)
.setNameDecorated(nameDecorated).setTagId(tagId).setOrganizationRunningBalance(organizationRunningBalance);
@Override }
public List<GLAccountData> retrieveAllGLAccounts(final Integer accountClassification, final String searchParam, final Integer usage, }
final Boolean manualTransactionsAllowed, final Boolean disabled,
JournalEntryAssociationParametersData associationParametersData) { @Override
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) { public List<GLAccountData> retrieveAllGLAccounts(final Integer accountClassification, final String searchParam, final Integer usage,
throw new GLAccountInvalidClassificationException(accountClassification); final Boolean manualTransactionsAllowed, final Boolean disabled,
} JournalEntryAssociationParametersData associationParametersData) {
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) {
if (usage != null && !checkValidGLAccountUsage(usage)) { throw new GLAccountInvalidClassificationException(accountClassification);
throw new GLAccountInvalidClassificationException(accountClassification); }
}
if (usage != null && !checkValidGLAccountUsage(usage)) {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData); throw new GLAccountInvalidClassificationException(accountClassification);
String sql = "select " + rm.schema(); }
// append SQL statement for fetching account totals
if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) { final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
sql = sql + " and gl_j.id in (select t1.id from (select t2.account_id, max(t2.id) as id from " String sql = "select " + rm.schema();
+ "(select id, max(entry_date) as entry_date, account_id from acc_gl_journal_entry where is_running_balance_calculated = true " // append SQL statement for fetching account totals
+ "group by account_id desc, id) t3 inner join acc_gl_journal_entry t2 on t2.account_id = t3.account_id and t2.entry_date = t3.entry_date " if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) {
+ "group by t2.account_id desc) t1)"; sql = sql + " and gl_j.id in (select t1.id from (select t2.account_id, max(t2.id) as id from "
} + "(select id, max(entry_date) as entry_date, account_id from acc_gl_journal_entry where is_running_balance_calculated = true "
final Object[] parameterArray = new Object[3]; + "group by account_id desc, id) t3 inner join acc_gl_journal_entry t2 on t2.account_id = t3.account_id and t2.entry_date = t3.entry_date "
int arrayPos = 0; + "group by t2.account_id desc) t1)";
boolean filtersPresent = false; }
if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null) final Object[] parameterArray = new Object[3];
|| (disabled != null)) { int arrayPos = 0;
filtersPresent = true; boolean filtersPresent = false;
sql += " where"; if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null)
} || (disabled != null)) {
filtersPresent = true;
if (filtersPresent) { sql += " where";
boolean firstWhereConditionAdded = false; }
if (accountClassification != null) {
sql += " classification_enum = ?"; if (filtersPresent) {
parameterArray[arrayPos] = accountClassification.shortValue(); boolean firstWhereConditionAdded = false;
arrayPos = arrayPos + 1; if (accountClassification != null) {
firstWhereConditionAdded = true; sql += " classification_enum = ?";
} parameterArray[arrayPos] = accountClassification.shortValue();
if (StringUtils.isNotBlank(searchParam)) { arrayPos = arrayPos + 1;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (StringUtils.isNotBlank(searchParam)) {
sql += " ( name like %?% or gl_code like %?% )"; if (firstWhereConditionAdded) {
parameterArray[arrayPos] = searchParam; sql += SQL_AND;
arrayPos = arrayPos + 1; }
parameterArray[arrayPos] = searchParam; sql += " ( name like ? or gl_code like ? )";
arrayPos = arrayPos + 1; parameterArray[arrayPos] = "%" + searchParam + "%";
firstWhereConditionAdded = true; arrayPos = arrayPos + 1;
} parameterArray[arrayPos] = "%" + searchParam + "%";
if (usage != null) { arrayPos = arrayPos + 1;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (usage != null) {
if (GLAccountUsage.HEADER.getValue().equals(usage)) { if (firstWhereConditionAdded) {
sql += " account_usage = 2 "; sql += SQL_AND;
} else if (GLAccountUsage.DETAIL.getValue().equals(usage)) { }
sql += " account_usage = 1 "; if (GLAccountUsage.HEADER.getValue().equals(usage)) {
} sql += " account_usage = 2 ";
firstWhereConditionAdded = true; } else if (GLAccountUsage.DETAIL.getValue().equals(usage)) {
} sql += " account_usage = 1 ";
if (manualTransactionsAllowed != null) { }
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (manualTransactionsAllowed != null) {
if (firstWhereConditionAdded) {
sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed; sql += SQL_AND;
firstWhereConditionAdded = true; }
}
if (disabled != null) { sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (disabled != null) {
if (firstWhereConditionAdded) {
sql += " disabled = " + disabled; sql += SQL_AND;
} }
}
sql += " disabled = " + disabled;
sql += " ORDER BY gl_code ASC"; }
}
final Object[] finalObjectArray = Arrays.copyOf(parameterArray, arrayPos);
return this.jdbcTemplate.query(sql, rm, (Object[]) finalObjectArray);// NOSONAR sql += " ORDER BY gl_code ASC";
}
final Object[] finalObjectArray = Arrays.copyOf(parameterArray, arrayPos);
@Override return this.jdbcTemplate.query(sql, rm, (Object[]) finalObjectArray);// NOSONAR
public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) { }
try {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData); @Override
final StringBuilder sql = new StringBuilder(); public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) {
sql.append("select ").append(rm.schema()); try {
sql.append(" where gl.id = ?"); final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
if (associationParametersData.isRunningBalanceRequired()) { final StringBuilder sql = new StringBuilder();
sql.append(" and gl_j.is_running_balance_calculated = true ") sql.append("select ").append(rm.schema());
.append(" ORDER BY gl_j.entry_date DESC,gl_j.id DESC LIMIT 1"); sql.append(" where gl.id = ?");
}
final GLAccountData glAccount = this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
return this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
} catch (final EmptyResultDataAccessException e) { // Live balance: sum all non-reversed journal entries so cashier sees real-time balance
throw new GLAccountNotFoundException(glAccountId, e); // without waiting for the "Update Accounting Running Balances" scheduled job.
} if (associationParametersData != null && associationParametersData.isRunningBalanceRequired() && glAccount != null) {
} final String liveBalanceSql = "SELECT COALESCE(ABS(SUM(CASE WHEN type_enum = 2 THEN amount ELSE -amount END)), 0) "
+ "FROM acc_gl_journal_entry "
@Override + "WHERE account_id = ? AND (reversed = false OR reversed IS NULL)";
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) { final java.math.BigDecimal liveBalance = this.jdbcTemplate.queryForObject(liveBalanceSql,
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false, java.math.BigDecimal.class, glAccountId);
new JournalEntryAssociationParametersData()); glAccount.setOrganizationRunningBalance(liveBalance != null ? liveBalance.longValue() : 0L);
} }
@Override return glAccount;
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() { } catch (final EmptyResultDataAccessException e) {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false, throw new GLAccountNotFoundException(glAccountId, e);
new JournalEntryAssociationParametersData()); }
} }
private static boolean checkValidGLAccountType(final int type) { @Override
for (final GLAccountType accountType : GLAccountType.values()) { public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) {
if (accountType.getValue().equals(type)) { return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false,
return true; new JournalEntryAssociationParametersData());
} }
}
return false; @Override
} public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false,
private static boolean checkValidGLAccountUsage(final int type) { new JournalEntryAssociationParametersData());
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) { }
if (accountUsage.getValue().equals(type)) {
return true; private static boolean checkValidGLAccountType(final int type) {
} for (final GLAccountType accountType : GLAccountType.values()) {
} if (accountType.getValue().equals(type)) {
return false; return true;
} }
}
@Override return false;
public GLAccountData retrieveNewGLAccountDetails(final Integer type) { }
return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
} private static boolean checkValidGLAccountUsage(final int type) {
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) {
@Override if (accountUsage.getValue().equals(type)) {
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) { return true;
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false, }
new JournalEntryAssociationParametersData()); }
} return false;
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) { @Override
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper(); public GLAccountData retrieveNewGLAccountDetails(final Integer type) {
final String sql = "Select " + GLAccountDataLookUpMapper.LOOKUP_SCHEMA + " where rule.id=? and tags.acc_type_enum=?"; return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
return this.jdbcTemplate.query(sql, mapper, (Object[]) new Object[] { ruleId, transactionType });// NOSONAR }
}
@Override
private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> { public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false,
private static final String LOOKUP_SCHEMA = " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; new JournalEntryAssociationParametersData());
}
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { @Override
final Long id = JdbcSupport.getLong(rs, "id"); public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final String name = rs.getString("name"); final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
final String glCode = rs.getString("glCode"); final String sql = "Select " + GLAccountDataLookUpMapper.LOOKUP_SCHEMA + " where rule.id=? and tags.acc_type_enum=?";
return this.jdbcTemplate.query(sql, mapper, (Object[]) new Object[] { ruleId, transactionType });// NOSONAR
return new GLAccountDataForLookup().setId(id).setName(name).setGlCode(glCode); }
}
} private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> {
}
private static final String LOOKUP_SCHEMA = " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id";
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final String name = rs.getString("name");
final String glCode = rs.getString("glCode");
return new GLAccountDataForLookup().setId(id).setName(name).setGlCode(glCode);
}
}
}
@@ -76,6 +76,9 @@ public class Teller extends AbstractPersistableCustom<Long> {
@Column(name = "state", nullable = false) @Column(name = "state", nullable = false)
private Integer status; private Integer status;
@Column(name = "is_deleted", nullable = false)
private boolean deleted = false;
@OneToMany(mappedBy = "teller", fetch = FetchType.LAZY) @OneToMany(mappedBy = "teller", fetch = FetchType.LAZY)
private Set<Cashier> cashiers; private Set<Cashier> cashiers;
@@ -161,4 +164,12 @@ public class Teller extends AbstractPersistableCustom<Long> {
this.office.getId(); this.office.getId();
this.cashiers.size(); this.cashiers.size();
} }
public void delete() {
this.deleted = true;
}
public boolean isDeleted() {
return this.deleted;
}
} }
@@ -31,4 +31,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecificationExecutor<Teller> { public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecificationExecutor<Teller> {
Collection<Teller> findTellerByOfficeId(Long officeId); Collection<Teller> findTellerByOfficeId(Long officeId);
long countByDeletedFalse();
} }
@@ -36,6 +36,9 @@ public class TellerRepositoryWrapper {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Teller findOneWithNotFoundDetection(final Long id) { public Teller findOneWithNotFoundDetection(final Long id) {
final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id)); final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id));
if (teller.isDeleted()) {
throw new TellerNotFoundException(id);
}
teller.initializeLazyCollections(); teller.initializeLazyCollections();
return teller; return teller;
} }
@@ -51,4 +54,8 @@ public class TellerRepositoryWrapper {
public void delete(final Teller teller) { public void delete(final Teller teller) {
this.repository.delete(teller); this.repository.delete(teller);
} }
public boolean existsAny() {
return this.repository.countByDeletedFalse() > 0;
}
} }
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.teller.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleException {
private static final String ERROR_MESSAGE_CODE = "error.msg.teller.already.exists";
private static final String DEFAULT_ERROR_MESSAGE = "Cannot create teller as a teller is already present.";
public TellerAlreadyExistsException() {
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
}
}
@@ -34,7 +34,8 @@ public enum CommandProcessingResultType {
AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), // AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
REJECTED(3, "commandProcessingResultType.rejected"), // REJECTED(3, "commandProcessingResultType.rejected"), //
UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), // UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), //
ERROR(5, "commandProcessingResultType.error"); ERROR(5, "commandProcessingResultType.error"), //
TERMINATE(6, "commandProcessingResultType.terminate");
private static final Map<Integer, CommandProcessingResultType> BY_ID = Arrays.stream(values()) private static final Map<Integer, CommandProcessingResultType> BY_ID = Arrays.stream(values())
.collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v)); .collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v));
@@ -59,4 +60,8 @@ public enum CommandProcessingResultType {
public boolean isRejected() { public boolean isRejected() {
return this == REJECTED; return this == REJECTED;
} }
public boolean isTerminate() {
return this == TERMINATE;
}
} }
@@ -71,6 +71,166 @@ public class CommandWrapperBuilder {
this.sanitizeJsonKeys); this.sanitizeJsonKeys);
} }
public CommandWrapperBuilder createChequeSeries() {
this.actionName = "CREATE";
this.entityName = "CHEQUESERIES";
this.entityId = null;
this.href = "/chequeseries";
return this;
}
public CommandWrapperBuilder updateChequeSeries(final Long seriesId) {
this.actionName = "UPDATE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder deleteChequeSeries(final Long seriesId) {
this.actionName = "DELETE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = null;
this.href = "/chequestocklots";
return this;
}
public CommandWrapperBuilder reportLostChequeStockLot(final Long lotId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = lotId;
this.href = "/chequestocklots/" + lotId;
return this;
}
public CommandWrapperBuilder createChequeStockTransfer() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = null;
this.href = "/chequestocktransfers";
return this;
}
public CommandWrapperBuilder receiveChequeStockTransfer(final Long transferId) {
this.actionName = "RECEIVE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder rejectChequeStockTransfer(final Long transferId) {
this.actionName = "REJECT";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder createChequeBook() {
this.actionName = "CREATE";
this.entityName = "CHEQUEBOOK";
this.entityId = null;
this.href = "/chequebooks";
return this;
}
public CommandWrapperBuilder issueChequeBook(final Long chequeBookId) {
this.actionName = "ISSUE";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder rejectChequeBook(final Long chequeBookId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder withdrawChequeBook(final Long chequeBookId) {
this.actionName = "WITHDRAW";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder stopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "STOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder revokeStopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "REVOKESTOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder reportLostChequeBook(final Long chequeBookId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder cancelLeafChequeBook(final Long chequeBookId) {
this.actionName = "CANCELLEAF";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder markUsedChequeBook(final Long chequeBookId) {
this.actionName = "MARKUSED";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder returnChequeBook(final Long chequeBookId) {
this.actionName = "RETURN";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder approveChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "APPROVE";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
public CommandWrapperBuilder rejectChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
public CommandWrapperBuilder updateCreditBureau() { public CommandWrapperBuilder updateCreditBureau() {
this.actionName = "UPDATE"; this.actionName = "UPDATE";
this.entityName = "ORGANISATIONCREDITBUREAU"; this.entityName = "ORGANISATIONCREDITBUREAU";
@@ -1611,6 +1771,14 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder terminateAccount(final Long accountId) {
this.actionName = "TERMINATE";
this.entityName = "ACCOUNT";
this.entityId = accountId;
this.href = "/accounts/" + accountId + "/terminate";
return this;
}
public CommandWrapperBuilder approveGSIMAccountApplication(final Long accountId) { public CommandWrapperBuilder approveGSIMAccountApplication(final Long accountId) {
this.actionName = "APPROVE"; this.actionName = "APPROVE";
this.entityName = "GSIMACCOUNT"; this.entityName = "GSIMACCOUNT";
@@ -3565,7 +3733,7 @@ public class CommandWrapperBuilder {
this.actionName = "ACTIVATE"; this.actionName = "ACTIVATE";
this.entityName = "CURRENTACCOUNT"; this.entityName = "CURRENTACCOUNT";
this.savingsId = accountId; this.savingsId = accountId;
this.entityId = null; this.entityId = accountId;
this.href = "/currentaccounts/" + accountId + "?command=activate"; this.href = "/currentaccounts/" + accountId + "?command=activate";
return this; return this;
} }
@@ -20,6 +20,7 @@ package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR; import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED; import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.TERMINATE;
import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson; import com.google.gson.Gson;
@@ -181,7 +182,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
currentSource.setResultStatusCode(SC_OK); currentSource.setResultStatusCode(SC_OK);
currentSource.updateForAudit(result); currentSource.updateForAudit(result);
currentSource.setResult(toApiResultJsonSerializer.serializeResult(result)); currentSource.setResult(toApiResultJsonSerializer.serializeResult(result));
currentSource.setStatus(PROCESSED); currentSource.setStatus("TERMINATE".equals(wrapper.getActionName()) ? TERMINATE : PROCESSED);
// Return saved command source // Return saved command source
return commandSourceService.saveResultSameTransaction(currentSource); return commandSourceService.saveResultSameTransaction(currentSource);
@@ -1,116 +1,150 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.infrastructure.core.serialization; package org.apache.fineract.infrastructure.core.serialization;
import com.google.gson.ExclusionStrategy; import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import java.time.LocalDate; import com.google.gson.TypeAdapter;
import java.time.LocalDateTime; import com.google.gson.TypeAdapterFactory;
import java.time.LocalTime; import com.google.gson.reflect.TypeToken;
import java.time.MonthDay; import com.google.gson.stream.JsonReader;
import java.time.OffsetDateTime; import com.google.gson.stream.JsonWriter;
import java.time.ZonedDateTime; import java.io.IOException;
import java.util.ArrayList; import java.time.LocalDate;
import java.util.HashSet; import java.time.LocalDateTime;
import java.util.Set; import java.time.LocalTime;
import org.apache.fineract.infrastructure.core.api.DateAdapter; import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter; import java.time.ZonedDateTime;
import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter; import java.util.ArrayList;
import org.apache.fineract.infrastructure.core.api.LocalDateAdapter; import java.util.HashSet;
import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter; import java.util.Set;
import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter; import org.apache.fineract.infrastructure.core.api.DateAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter; import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy; import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy; import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.api.LocalDateAdapter;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter;
import org.springframework.stereotype.Service; import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter;
/** import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy;
* Helper class for serialization of Java objects into JSON using Google's GSON. import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy;
*/ import org.apache.fineract.infrastructure.core.domain.ExternalId;
@Service import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
public final class GoogleGsonSerializerHelper { import org.springframework.stereotype.Service;
public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) { /**
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters); * Helper class for serialization of Java objects into JSON using Google's GSON.
*/
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy); @Service
registerTypeAdapters(builder); public final class GoogleGsonSerializerHelper {
return builder.create();
} public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) {
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters);
public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
final Set<String> responseParameters) { final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
final Set<String> parameterNamesToSkip = new HashSet<>(); return builder.create();
}
if (!responseParameters.isEmpty()) {
// strip out all known support parameters from expected response to public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
// see if unsupported parameters requested for response. final Set<String> responseParameters) {
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
differentParametersDetectedSet.removeAll(supportedParameters); final Set<String> parameterNamesToSkip = new HashSet<>();
if (!differentParametersDetectedSet.isEmpty()) { if (!responseParameters.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet)); // strip out all known support parameters from expected response to
} // see if unsupported parameters requested for response.
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
parameterNamesToSkip.addAll(supportedParameters); differentParametersDetectedSet.removeAll(supportedParameters);
parameterNamesToSkip.removeAll(responseParameters);
} if (!differentParametersDetectedSet.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet));
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip); }
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy); parameterNamesToSkip.addAll(supportedParameters);
registerTypeAdapters(builder); parameterNamesToSkip.removeAll(responseParameters);
return builder.create(); }
}
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip);
public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects); final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
} registerTypeAdapters(builder);
return builder.create();
public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) { }
return serializer.toJson(singleDataObject);
} public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects);
public static Gson createSimpleGson() { }
return createGsonBuilder().create();
} public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) {
return serializer.toJson(singleDataObject);
public static GsonBuilder createGsonBuilder() { }
final GsonBuilder builder = new GsonBuilder();
registerTypeAdapters(builder); public static Gson createSimpleGson() {
return builder; return createGsonBuilder().create();
} }
public static void registerTypeAdapters(final GsonBuilder builder) { public static GsonBuilder createGsonBuilder() {
builder.registerTypeAdapter(java.util.Date.class, new DateAdapter()); final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter()); registerTypeAdapters(builder);
// NOTE: was missing, necessary for GSON serialization with JDK 21's restrictive module access return builder;
builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()); }
builder.registerTypeAdapter(ZonedDateTime.class, new JodaDateTimeAdapter());
builder.registerTypeAdapter(MonthDay.class, new JodaMonthDayAdapter()); public static void registerTypeAdapters(final GsonBuilder builder) {
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()); builder.registerTypeAdapter(java.util.Date.class, new DateAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()); builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter());
builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter()); // NOTE: was missing, necessary for GSON serialization with JDK 21's restrictive module access
} builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter());
} builder.registerTypeAdapter(ZonedDateTime.class, new JodaDateTimeAdapter());
builder.registerTypeAdapter(MonthDay.class, new JodaMonthDayAdapter());
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter());
builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter());
// Use runtime type when serializing interface-typed fields (e.g. Collection<? extends IGuarantor>)
builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
if (!type.getRawType().isInterface()) {
return null;
}
final TypeAdapterFactory self = this;
return new TypeAdapter<T>() {
@Override
public void write(final JsonWriter out, final T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
TypeAdapter<T> delegate = (TypeAdapter<T>) gson.getDelegateAdapter(self,
TypeToken.get((Class<T>) value.getClass()));
delegate.write(out, value);
}
@Override
public T read(final JsonReader in) throws IOException {
return null;
}
};
}
});
}
}
@@ -61,6 +61,9 @@ public enum JobName {
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), // ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), // JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), // FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), //
IMPS_DEEMED_RECONCILE("IMPS DEEMED Reconciliation"), //
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
; // ; //
private final String name; private final String name;
@@ -1,71 +1,79 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account; package org.apache.fineract.portfolio.account;
public enum PortfolioAccountType { public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), // INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), // LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), // SAVINGS(2, "accountType.savings"), //
; CURRENT(3, "accountType.current"), //
;
private final Integer value;
private final String code; private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value; PortfolioAccountType(final Integer value, final String code) {
this.code = code; this.value = value;
} this.code = code;
}
public Integer getValue() {
return this.value; public Integer getValue() {
} return this.value;
}
public String getCode() {
return this.code; public String getCode() {
} return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) { PortfolioAccountType enumType = INVALID;
switch (type) { if (type != null) {
case 1: switch (type) {
enumType = LOAN; case 1:
break; enumType = LOAN;
case 2: break;
enumType = SAVINGS; case 2:
break; enumType = SAVINGS;
default: break;
enumType = INVALID; case 3:
} enumType = CURRENT;
} break;
return enumType; default:
} enumType = INVALID;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! }
public boolean isSavingsAccount() { return enumType;
return this.equals(SAVINGS); }
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! public boolean isSavingsAccount() {
public boolean isLoanAccount() { return this.equals(SAVINGS);
return this.equals(LOAN); }
}
} // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isLoanAccount() {
return this.equals(LOAN);
}
public boolean isCurrentAccount() {
return this.equals(CURRENT);
}
}
@@ -224,7 +224,7 @@ public final class DepositsApiConstants {
* Deposit Product Parameters * Deposit Product Parameters
*/ */
private static final Set<String> DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, private static final Set<String> DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, dateFormatParamName, monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName,
digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName,
interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName,
@@ -512,6 +512,11 @@ public final class DepositsApiConstants {
fixedDepositInterestCalculationData.add(tenureInMonthsParamName); fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
// Exact calendar-based calculation params
fixedDepositInterestCalculationData.add("submittedOnDate");
fixedDepositInterestCalculationData.add("dateFormat");
fixedDepositInterestCalculationData.add("locale");
fixedDepositInterestCalculationData.add("daysInYear");
return fixedDepositInterestCalculationData; return fixedDepositInterestCalculationData;
@@ -170,6 +170,7 @@ public final class SavingsAccountData implements Serializable {
private Boolean balanceSwapOption; private Boolean balanceSwapOption;
private Boolean msme; private Boolean msme;
private Boolean autoDebitFacility; private Boolean autoDebitFacility;
private Boolean terminateTrue;
private String avgBalanceCalcPeriod; private String avgBalanceCalcPeriod;
private String validForPeriod; private String validForPeriod;
private Boolean odAccountClosedZeroBalanceInDayClosing; private Boolean odAccountClosedZeroBalanceInDayClosing;
@@ -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.savings.data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
/**
* Read-side view of a single pending Savings Product edit row (m_savings_product_temp), for the Approvals ->
* Pending Savings Product Edits screen.
*/
@Getter
@Setter
public class SavingsProductTempData implements Serializable {
private Long id;
private Long savingsProductId;
private String savingsProductName;
private Long makerId;
private String makerUsername;
private Long checkerId;
private String checkerUsername;
private LocalDateTime createdOn;
private LocalDateTime checkedOn;
private String status;
private String rejectReason;
private Map<String, Object> proposedChanges;
public SavingsProductTempData() {}
}
@@ -36,7 +36,8 @@ public enum SavingsAccountStatusType {
REJECTED(500, "savingsAccountStatusType.rejected"), // REJECTED(500, "savingsAccountStatusType.rejected"), //
CLOSED(600, "savingsAccountStatusType.closed"), // CLOSED(600, "savingsAccountStatusType.closed"), //
PRE_MATURE_CLOSURE(700, "savingsAccountStatusType.pre.mature.closure"), // PRE_MATURE_CLOSURE(700, "savingsAccountStatusType.pre.mature.closure"), //
MATURED(800, "savingsAccountStatusType.matured"); // MATURED(800, "savingsAccountStatusType.matured"), //
TERMINATED(900, "savingsAccountStatusType.terminated"); //
private final Integer value; private final Integer value;
private final String code; private final String code;
@@ -53,6 +54,7 @@ public enum SavingsAccountStatusType {
case 600 -> SavingsAccountStatusType.CLOSED; case 600 -> SavingsAccountStatusType.CLOSED;
case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE; case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE;
case 800 -> SavingsAccountStatusType.MATURED; case 800 -> SavingsAccountStatusType.MATURED;
case 900 -> SavingsAccountStatusType.TERMINATED;
default -> SavingsAccountStatusType.INVALID; default -> SavingsAccountStatusType.INVALID;
}; };
} }
@@ -113,4 +115,8 @@ public enum SavingsAccountStatusType {
public boolean isPreMatureClosure() { public boolean isPreMatureClosure() {
return this.value.equals(SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue()); return this.value.equals(SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue());
} }
public boolean isTerminated() {
return this.value.equals(SavingsAccountStatusType.TERMINATED.getValue());
}
} }
@@ -285,6 +285,11 @@ public final class SavingsEnumerations {
SavingsAccountStatusType.MATURED.getCode(), "Matured", submittedAndPendingApproval, isApproved, isRejected, SavingsAccountStatusType.MATURED.getCode(), "Matured", submittedAndPendingApproval, isApproved, isRejected,
isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured); isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured);
break; break;
case TERMINATED:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.TERMINATED.getValue().longValue(),
SavingsAccountStatusType.TERMINATED.getCode(), "Terminated", submittedAndPendingApproval, isApproved, isRejected,
isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured);
break;
} }
return optionData; return optionData;
} }
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity @Entity
@Table(name = "m_current_account", uniqueConstraints = { @Table(name = "m_current_account", uniqueConstraints = {
@@ -102,6 +103,30 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "closedon_date") @Column(name = "closedon_date")
private LocalDate closedOnDate; private LocalDate closedOnDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "submittedon_userid")
private AppUser submittedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "approvedon_userid")
private AppUser approvedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "activatedon_userid")
private AppUser activatedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rejectedon_userid")
private AppUser rejectedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "withdrawnon_userid")
private AppUser withdrawnOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "closedon_userid")
private AppUser closedOnByUser;
// Currency (copied from product at account creation) // Currency (copied from product at account creation)
@Column(name = "currency_code", nullable = false, length = 3) @Column(name = "currency_code", nullable = false, length = 3)
private String currencyCode; private String currencyCode;
@@ -304,6 +329,12 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "od_account_closed_zero_balance_in_day_closing") @Column(name = "od_account_closed_zero_balance_in_day_closing")
private boolean odAccountClosedZeroBalanceInDayClosing = false; private boolean odAccountClosedZeroBalanceInDayClosing = false;
@Column(name = "is_linking_enable", nullable = false)
private boolean shareLinkingEnabled = false;
@Column(name = "terminate_true", nullable = false, columnDefinition = "boolean default false")
private boolean terminateTrue = false;
// Relationships // Relationships
@OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@@ -373,29 +404,34 @@ public class CurrentAccount extends AbstractAuditableCustom {
public void updateAccountNo(String accountNo) { this.accountNo = accountNo; } public void updateAccountNo(String accountNo) { this.accountNo = accountNo; }
public void approve(LocalDate approvedOnDate) { public void approve(LocalDate approvedOnDate, AppUser approvedOnByUser) {
this.statusEnum = 200; this.statusEnum = 200;
this.approvedOnDate = approvedOnDate; this.approvedOnDate = approvedOnDate;
this.approvedOnByUser = approvedOnByUser;
} }
public void activate(LocalDate activatedOnDate) { public void activate(LocalDate activatedOnDate, AppUser activatedOnByUser) {
this.statusEnum = 300; this.statusEnum = 300;
this.activatedOnDate = activatedOnDate; this.activatedOnDate = activatedOnDate;
this.activatedOnByUser = activatedOnByUser;
} }
public void reject(LocalDate rejectedOnDate) { public void reject(LocalDate rejectedOnDate, AppUser rejectedOnByUser) {
this.statusEnum = 500; this.statusEnum = 500;
this.rejectedOnDate = rejectedOnDate; this.rejectedOnDate = rejectedOnDate;
this.rejectedOnByUser = rejectedOnByUser;
} }
public void withdraw(LocalDate withdrawnOnDate) { public void withdraw(LocalDate withdrawnOnDate, AppUser withdrawnOnByUser) {
this.statusEnum = 400; this.statusEnum = 400;
this.withdrawnOnDate = withdrawnOnDate; this.withdrawnOnDate = withdrawnOnDate;
this.withdrawnOnByUser = withdrawnOnByUser;
} }
public void close(LocalDate closedOnDate) { public void close(LocalDate closedOnDate, AppUser closedOnByUser) {
this.statusEnum = 600; this.statusEnum = 600;
this.closedOnDate = closedOnDate; this.closedOnDate = closedOnDate;
this.closedOnByUser = closedOnByUser;
} }
public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; } public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; }
@@ -406,4 +442,15 @@ public class CurrentAccount extends AbstractAuditableCustom {
public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; } public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; }
public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; } public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; }
public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; } public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; }
public boolean isShareLinkingEnabled() { return shareLinkingEnabled; }
public void setShareLinkingEnabled(boolean shareLinkingEnabled) { this.shareLinkingEnabled = shareLinkingEnabled; }
public boolean isTerminateTrue() { return terminateTrue; }
public void setTerminateTrue(boolean terminateTrue) { this.terminateTrue = terminateTrue; }
public void terminate() {
this.terminateTrue = true;
this.statusEnum = 900;
}
} }
@@ -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.currentaccount.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* Wrapper for {@link CurrentAccountRepository} that is responsible for checking if {@link CurrentAccount} is
* returned when using <code>findOne</code> repository method and throwing an appropriate not found exception.
* </p>
*
* <p>
* This is to avoid need for checking and throwing in multiple areas of code base where
* {@link CurrentAccountRepository} is required.
* </p>
*/
@Service
@RequiredArgsConstructor
public class CurrentAccountRepositoryWrapper {
private final CurrentAccountRepository repository;
@Transactional(readOnly = true)
public CurrentAccount findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new CurrentAccountNotFoundException(id));
}
}
@@ -37,6 +37,31 @@ public class CurrentAccountNumberGenerator {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private String getNextSequence() {
final java.util.List<String> accountNos = this.jdbcTemplate.query(
"SELECT account_no FROM m_current_account WHERE account_no IS NOT NULL",
(rs, rowNum) -> rs.getString(1));
long maxVal = 0;
for (String accNo : accountNos) {
if (accNo != null && accNo.length() >= 5) {
String suffix = accNo.substring(accNo.length() - 5);
try {
long val = Long.parseLong(suffix);
if (val > maxVal) {
maxVal = val;
}
} catch (NumberFormatException ignored) {}
}
}
String nextSeq = StringUtils.leftPad(String.valueOf(maxVal + 1), 5, '0');
if (nextSeq.length() > 5) {
nextSeq = nextSeq.substring(nextSeq.length() - 5);
}
return nextSeq;
}
public String generate(CurrentAccount account) { public String generate(CurrentAccount account) {
Long productId = account.getProductId(); Long productId = account.getProductId();
@@ -59,11 +84,10 @@ public class CurrentAccountNumberGenerator {
String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0'); String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0');
if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2); if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2);
// Account ID — 5 digits // Next sequence from existing account numbers
String accountId = StringUtils.leftPad(account.getId().toString(), 5, '0'); String nextSeq = getNextSequence();
if (accountId.length() > 5) accountId = accountId.substring(accountId.length() - 5);
return CBS_PREFIX + glCode + officeId + accountId; return CBS_PREFIX + glCode + officeId + nextSeq;
} }
public String preview(Long productId, Long officeId) { public String preview(Long productId, Long officeId) {
@@ -80,13 +104,10 @@ public class CurrentAccountNumberGenerator {
String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0'); String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0');
if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2); if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2);
// Next account ID estimate from m_current_account // Next sequence from existing account numbers
Long maxId = jdbcTemplate.query( String nextSeq = getNextSequence();
"SELECT MAX(id) FROM m_current_account",
rs -> rs.next() ? rs.getLong(1) : 0L);
String nextSeq = StringUtils.leftPad(String.valueOf((maxId != null ? maxId : 0L) + 1), 5, '0');
if (nextSeq.length() > 5) nextSeq = nextSeq.substring(nextSeq.length() - 5);
return CBS_PREFIX + glCode + officeStr + nextSeq; return CBS_PREFIX + glCode + officeStr + nextSeq;
} }
} }
@@ -26,6 +26,8 @@ public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId); Map<String, Object> retrieveOne(Long accountId);
Map<String, Object> retrieveOneTransaction(Long accountId, Long transactionId);
List<Map<String, Object>> retrieveAll(); List<Map<String, Object>> retrieveAll();
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName); List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
@@ -40,4 +40,6 @@ public interface CurrentAccountWritePlatformService {
CommandProcessingResult modifyApplication(Long accountId, JsonCommand command); CommandProcessingResult modifyApplication(Long accountId, JsonCommand command);
CommandProcessingResult deleteApplication(Long accountId); CommandProcessingResult deleteApplication(Long accountId);
CommandProcessingResult terminateAccount(Long accountId, JsonCommand command);
} }
@@ -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.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductEditPendingApprovalException extends AbstractPlatformDomainRuleException {
public CurrentProductEditPendingApprovalException(final Long productId) {
super("error.msg.currentproduct.edit.pending.approval",
"Current product with identifier " + productId + " already has an edit pending approval", productId);
}
}
@@ -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.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductNoPendingEditException extends AbstractPlatformDomainRuleException {
public CurrentProductNoPendingEditException(final Long productId) {
super("error.msg.currentproduct.no.pending.edit",
"Current product with identifier " + productId + " has no edit pending approval", productId);
}
}
@@ -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.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
public CurrentProductSelfApprovalNotAllowedException(final Long productId) {
super("error.msg.currentproduct.self.approval.not.allowed",
"The maker of the pending edit for current product " + productId + " cannot also approve or reject it", productId);
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.currentproduct.service;
import java.util.Collection; import java.util.Collection;
import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
public interface CurrentProductReadPlatformService { public interface CurrentProductReadPlatformService {
@@ -28,4 +29,8 @@ public interface CurrentProductReadPlatformService {
Collection<SavingsProductData> retrieveAllForLookup(); Collection<SavingsProductData> retrieveAllForLookup();
SavingsProductData retrieveOne(Long productId); SavingsProductData retrieveOne(Long productId);
Collection<SavingsProductTempData> retrieveAllPendingEdits();
SavingsProductTempData retrievePendingEdit(Long productId);
} }
@@ -42,13 +42,23 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException;
import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService { public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService {
@@ -58,6 +68,11 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper(); private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper();
private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper(); private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper();
@@ -147,6 +162,64 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
} }
} }
private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, "
+ "cp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, "
+ "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, "
+ "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt "
+ "join m_current_product cp on cp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.createdby_id "
+ "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = 'CURRENT' ";
private RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository,
this.pendingEditFacilityLimitRepository, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
public SavingsProductTempData retrievePendingEdit(final Long productId) {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?";
try {
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new CurrentProductNoPendingEditException(productId);
}
}
private static final class CurrentProductMapper implements RowMapper<SavingsProductData> { private static final class CurrentProductMapper implements RowMapper<SavingsProductData> {
private final String schemaSql; private final String schemaSql;
@@ -197,7 +270,9 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
sqlBuilder.append("cp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); sqlBuilder.append("cp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, "); sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, ");
sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, "); sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, ");
sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing "); sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id "
+ "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_current_product cp "); sqlBuilder.append("from m_current_product cp ");
sqlBuilder.append("join m_currency curr on curr.code = cp.currency_code "); sqlBuilder.append("join m_currency curr on curr.code = cp.currency_code ");
@@ -321,6 +396,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod")); productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod"));
productData.setValidForPeriod(rs.getString("validForPeriod")); productData.setValidForPeriod(rs.getString("validForPeriod"));
productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing")); productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing"));
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
return productData; return productData;
} }
@@ -28,4 +28,21 @@ public interface CurrentProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult update(Long productId, JsonCommand command);
CommandProcessingResult delete(Long productId); CommandProcessingResult delete(Long productId);
/**
* Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this
* product (that pending row is itself the lock).
*/
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
/**
* Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving
* user is the same user who submitted the edit.
*/
CommandProcessingResult approveEdit(Long productId);
/**
* Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit.
*/
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
} }
@@ -31,6 +31,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -38,6 +41,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator;
@@ -50,10 +55,26 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductEditPendingApprovalException;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductSelfApprovalNotAllowedException;
import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import com.google.gson.JsonObject;
import org.apache.fineract.portfolio.tax.domain.TaxGroup; import org.apache.fineract.portfolio.tax.domain.TaxGroup;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService { public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final CurrentProductRepository currentProductRepository; private final CurrentProductRepository currentProductRepository;
private final CurrentProductDataValidator fromApiJsonDataValidator; private final CurrentProductDataValidator fromApiJsonDataValidator;
@@ -69,6 +91,13 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final FromJsonHelper fromApiJsonHelper;
private final CommandSourceRepository commandSourceRepository;
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) { private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) {
String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME; String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME;
@@ -251,6 +280,222 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
} }
} }
@Transactional
@Override
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
final AppUser maker = this.context.authenticatedUser();
final CurrentProduct product = this.currentProductRepository.findById(productId)
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
if (this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.isPresent()) {
throw new CurrentProductEditPendingApprovalException(productId);
}
this.fromApiJsonDataValidator.validateForUpdate(command.json(), product);
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_CURRENT, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> currentOnlySubmitted = ProductPendingEditFieldMapper.populateCurrentOnlyFields(command, pendingEdit.getCurrentOnlyFields());
final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command, pendingEdit.getFacilityConfigFields());
final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command, pendingEdit.getDepositLimitKycFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (!currentOnlySubmitted.isEmpty()) {
submitted.add("avgBalanceCalcPeriod");
submitted.add("validForPeriod");
submitted.add("odAccountClosedZeroBalanceInDayClosing");
}
if (facilityConfigSubmitted) {
submitted.add("accountFacilityConfig");
}
if (depositLimitKycSubmitted) {
submitted.add("depositLimitKyc");
}
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("CURRENTPRODUCT") //
.resourceGetUrl("/currentproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(command.json()) //
.maker(maker) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
.sanitized(false) //
.build();
this.commandSourceRepository.saveAndFlush(auditEntry);
pendingEdit.setCommandSourceId(auditEntry.getId());
this.productPendingEditRepository.saveAndFlush(pendingEdit);
for (final String fieldName : submitted) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
}
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
if (chargeIds != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
for (final Long chargeId : chargeIds) {
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
}
}
final List<ProductPendingEditFacilityLimit> facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command, pendingEdit);
if (facilityLimits != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits"));
this.pendingEditFacilityLimitRepository.saveAll(facilityLimits);
}
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
if (glMappings != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
this.pendingEditGlMappingRepository.saveAll(glMappings);
}
return new CommandProcessingResultBuilder() //
.withEntityId(product.getId()) //
.build();
}
@Transactional
@Override
public CommandProcessingResult approveEdit(final Long productId) {
final AppUser checker = this.context.authenticatedUser();
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new CurrentProductNoPendingEditException(productId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new CurrentProductSelfApprovalNotAllowedException(productId);
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
pendingEdit.markApproved(checker);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
auditEntry.markAsChecked(checker);
auditEntry.updateForAudit(result);
this.commandSourceRepository.saveAndFlush(auditEntry);
return result;
}
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
if (pendingEdit.getCommandSourceId() != null) {
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("CURRENTPRODUCT") //
.resourceGetUrl("/currentproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
.sanitized(false) //
.build();
this.commandSourceRepository.saveAndFlush(auditEntry);
pendingEdit.setCommandSourceId(auditEntry.getId());
this.productPendingEditRepository.saveAndFlush(pendingEdit);
return auditEntry;
}
@Transactional
@Override
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
final AppUser checker = this.context.authenticatedUser();
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new CurrentProductNoPendingEditException(productId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new CurrentProductSelfApprovalNotAllowedException(productId);
}
pendingEdit.markRejected(checker, rejectReason);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
auditEntry.markAsRejected(checker);
this.commandSourceRepository.saveAndFlush(auditEntry);
return new CommandProcessingResultBuilder() //
.withEntityId(productId) //
.build();
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult delete(final Long productId) { public CommandProcessingResult delete(final Long productId) {
@@ -19,7 +19,14 @@
package org.apache.fineract.portfolio.currentproduct.starter; package org.apache.fineract.portfolio.currentproduct.starter;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository;
@@ -43,9 +50,17 @@ public class CurrentProductConfiguration {
public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context, public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context,
JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository, JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository) {
return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository, return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository); currentFacilityLimitRepository, currentDepositLimitKycRepository,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository,
pendingEditFacilityLimitRepository);
} }
@Bean @Bean
@@ -56,9 +71,18 @@ public class CurrentProductConfiguration {
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
CurrentProductFacilityConfigRepository currentFacilityConfigRepository, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
FromJsonHelper fromApiJsonHelper,
CommandSourceRepository commandSourceRepository) {
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator, return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository, currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository); currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository,
pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository,
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
} }
} }
+4
View File
@@ -99,6 +99,10 @@ dependencies {
// Bundles ZXing transitively for QR PNG output (used by ZxingPngQrGenerator). // Bundles ZXing transitively for QR PNG output (used by ZxingPngQrGenerator).
'dev.samstevens.totp:totp:1.7.1', 'dev.samstevens.totp:totp:1.7.1',
// Nimbus JOSE+JWT — RS256 sign/verify for the IMPS_BANK_API_SPEC_CBS_1.0 envelope
// (see org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil).
'com.nimbusds:nimbus-jose-jwt:9.37',
'org.apache.commons:commons-lang3', 'org.apache.commons:commons-lang3',
'commons-io:commons-io', 'commons-io:commons-io',
'org.apache.poi:poi', 'org.apache.poi:poi',
+975
View File
@@ -0,0 +1,975 @@
> Task :buildSrc:extractPluginRequests UP-TO-DATE
> Task :buildSrc:generatePluginAdapters UP-TO-DATE
> Task :buildSrc:compileJava UP-TO-DATE
> Task :buildSrc:compileGroovy UP-TO-DATE
> Task :buildSrc:compileGroovyPlugins UP-TO-DATE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Configure project :
matching ref: COMMIT - 7dea88d30f72365945bed2160d2ba72dccdc80b7
ref configuration: COMMIT - pattern: null
version: ${describe.tag.version.major}.${describe.tag.version.minor.next}.0-SNAPSHOT
describeTagPattern: .*(\d+\.\d+\.\d+).*
describeTagFirstParent: false
project version: 1.15.0-SNAPSHOT
> Task :spotlessInternalRegisterDependencies UP-TO-DATE
> Task :fineract-current-product:processResources NO-SOURCE
> Task :fineract-mix:processResources NO-SOURCE
> Task :fineract-rates:processResources UP-TO-DATE
> Task :fineract-core:processResources UP-TO-DATE
> Task :fineract-cob:processResources UP-TO-DATE
> Task :fineract-branch:processResources UP-TO-DATE
> Task :fineract-security:processResources NO-SOURCE
> Task :fineract-loan-origination:processResources UP-TO-DATE
> Task :fineract-savings:processResources UP-TO-DATE
> Task :fineract-accounting:processResources UP-TO-DATE
> Task :fineract-report:processResources UP-TO-DATE
> Task :fineract-charge:processResources UP-TO-DATE
> Task :fineract-investor:processResources UP-TO-DATE
> Task :fineract-loan:processResources UP-TO-DATE
> Task :fineract-client:processResources UP-TO-DATE
> Task :fineract-progressive-loan:processResources UP-TO-DATE
> Task :fineract-working-capital-loan:processResources UP-TO-DATE
> Task :fineract-document:processResources NO-SOURCE
> Task :fineract-tax:processResources UP-TO-DATE
> Task :fineract-avro-schemas:preprocessAvroSchemas UP-TO-DATE
> Task :fineract-validation:compileJava UP-TO-DATE
> Task :fineract-command:compileJava UP-TO-DATE
> Task :fineract-validation:processResources UP-TO-DATE
> Task :fineract-validation:classes UP-TO-DATE
> Task :fineract-command:processResources UP-TO-DATE
> Task :fineract-command:classes UP-TO-DATE
> Task :fineract-command:jar UP-TO-DATE
> Task :fineract-validation:jar UP-TO-DATE
> Task :fineract-avro-schemas:spotlessJson UP-TO-DATE
> Task :fineract-avro-schemas:spotlessJsonApply UP-TO-DATE
> Task :fineract-avro-schemas:buildJavaSdk UP-TO-DATE
> Task :fineract-avro-schemas:compileJava UP-TO-DATE
> Task :fineract-avro-schemas:processResources UP-TO-DATE
> Task :fineract-avro-schemas:classes UP-TO-DATE
> Task :fineract-avro-schemas:jar UP-TO-DATE
> Task :fineract-working-capital-loan:compileJava UP-TO-DATE
> Task :fineract-working-capital-loan:classes UP-TO-DATE
> Task :fineract-working-capital-loan:jar UP-TO-DATE
> Task :fineract-core:compileJava UP-TO-DATE
> Task :fineract-core:classes UP-TO-DATE
> Task :fineract-core:jar UP-TO-DATE
> Task :fineract-mix:compileJava UP-TO-DATE
> Task :fineract-tax:compileJava UP-TO-DATE
> Task :fineract-mix:classes UP-TO-DATE
> Task :fineract-report:compileJava UP-TO-DATE
> Task :fineract-tax:classes UP-TO-DATE
> Task :fineract-mix:jar UP-TO-DATE
> Task :fineract-rates:compileJava UP-TO-DATE
> Task :fineract-report:classes UP-TO-DATE
> Task :fineract-cob:compileJava UP-TO-DATE
> Task :fineract-rates:classes UP-TO-DATE
> Task :fineract-cob:classes UP-TO-DATE
> Task :fineract-tax:jar UP-TO-DATE
> Task :fineract-report:jar UP-TO-DATE
> Task :fineract-cob:jar UP-TO-DATE
> Task :fineract-rates:jar UP-TO-DATE
> Task :fineract-document:compileJava UP-TO-DATE
> Task :fineract-document:classes UP-TO-DATE
> Task :fineract-security:compileJava UP-TO-DATE
> Task :fineract-document:jar UP-TO-DATE
> Task :fineract-security:classes UP-TO-DATE
> Task :fineract-security:jar UP-TO-DATE
> Task :fineract-charge:compileJava UP-TO-DATE
> Task :fineract-charge:classes UP-TO-DATE
> Task :fineract-charge:jar UP-TO-DATE
> Task :fineract-accounting:compileJava UP-TO-DATE
> Task :fineract-accounting:classes UP-TO-DATE
> Task :fineract-accounting:jar UP-TO-DATE
> Task :fineract-branch:compileJava UP-TO-DATE
> Task :fineract-branch:classes UP-TO-DATE
> Task :fineract-loan:compileJava UP-TO-DATE
> Task :fineract-loan:classes UP-TO-DATE
> Task :fineract-branch:jar UP-TO-DATE
> Task :fineract-loan:jar UP-TO-DATE
> Task :fineract-savings:compileJava UP-TO-DATE
> Task :fineract-savings:classes UP-TO-DATE
> Task :fineract-savings:jar UP-TO-DATE
> Task :fineract-loan-origination:compileJava UP-TO-DATE
> Task :fineract-investor:compileJava UP-TO-DATE
> Task :fineract-progressive-loan:compileJava UP-TO-DATE
> Task :fineract-loan-origination:classes UP-TO-DATE
> Task :fineract-progressive-loan:classes UP-TO-DATE
> Task :fineract-investor:classes UP-TO-DATE
> Task :fineract-progressive-loan:jar UP-TO-DATE
> Task :fineract-loan-origination:jar UP-TO-DATE
> Task :fineract-investor:jar UP-TO-DATE
> Task :fineract-current-product:compileJava UP-TO-DATE
> Task :fineract-current-product:classes UP-TO-DATE
> Task :fineract-current-product:jar UP-TO-DATE
> Task :fineract-provider:generateGitProperties
> Task :fineract-provider:compileJava UP-TO-DATE
> Task :fineract-provider:generateRebelMain UP-TO-DATE
> Task :fineract-provider:processResources
> Task :fineract-provider:classes
> Task :fineract-provider:compileTestJava UP-TO-DATE
> Task :fineract-provider:prepareInputYaml UP-TO-DATE
> Task :fineract-provider:processTestResources UP-TO-DATE
> Task :fineract-provider:testClasses UP-TO-DATE
> Task :fineract-provider:resolve
> Task :fineract-provider:resolveMainClassName
> Task :fineract-provider:bootRun
19:17:05,132 |-WARN in ch.qos.logback.core.joran.action.ConversionRuleAction - [converterClass] attribute is deprecated and replaced by [class]. See element [conversionRule] near line 30
_ _ _____ _ _
/ \ _ __ __ _ ___| |__ ___ | ___(_)_ __ ___ _ __ __ _ ___| |_
/ _ \ | '_ \ / _` |/ __| '_ \ / _ \ | |_ | | '_ \ / _ \ '__/ _` |/ __| __|
/ ___ \| |_) | (_| | (__| | | | __/ | _| | | | | | __/ | | (_| | (__| |_
/_/ \_\ .__/ \__,_|\___|_| |_|\___| |_| |_|_| |_|\___|_| \__,_|\___|\__|
|_|
(c) 2015-2024 Apache Fineract (https://fineract.apache.org)
Powered by Spring Boot 3.5.6
2026-05-29 19:17:05.618 - INFO 7760  [ no-tenant] --- [ main] org.apache.fineract.ServerApplication  : 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)
2026-05-29 19:17:05.622 - INFO 7760  [ no-tenant] --- [ main] org.apache.fineract.ServerApplication  : No active profile set, falling back to 1 default profile: "default"
2026-05-29 19:17:10.836 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-05-29 19:17:10.836 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-05-29 19:17:11.794 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:11.797 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:11.811 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:11.813 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:11.815 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:11.887 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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
2026-05-29 19:17:13.120 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 2267 ms. Found 224 JPA repository interfaces.
2026-05-29 19:17:13.130 - INFO 7760  [ no-tenant] --- [ main] o.s.b.f.s.DefaultListableBeanFactory  : 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]
2026-05-29 19:17:13.204 - INFO 7760  [ no-tenant] --- [ main] o.s.b.f.s.DefaultListableBeanFactory  : 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]]
2026-05-29 19:17:13.206 - INFO 7760  [ no-tenant] --- [ main] o.s.b.c.c.annotation.BatchRegistrar  : Bean jobRepository already defined in the application context, skipping the registration of a jobRepository
2026-05-29 19:17:13.206 - INFO 7760  [ no-tenant] --- [ main] o.s.b.c.c.annotation.BatchRegistrar  : Bean jobExplorer already defined in the application context, skipping the registration of a jobExplorer
2026-05-29 19:17:13.206 - INFO 7760  [ no-tenant] --- [ main] o.s.b.c.c.annotation.BatchRegistrar  : Bean jobLauncher already defined in the application context, skipping the registration of a jobLauncher
2026-05-29 19:17:13.208 - INFO 7760  [ no-tenant] --- [ main] o.s.b.c.c.annotation.BatchRegistrar  : Finished Spring Batch infrastructure beans configuration in 3 ms.
2026-05-29 19:17:14.107 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-05-29 19:17:14.108 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JDBC repositories in DEFAULT mode.
2026-05-29 19:17:14.408 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.409 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.409 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.410 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.412 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.412 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.412 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.413 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.413 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.413 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.414 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.414 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.414 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.414 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.414 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.415 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.415 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.415 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.415 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.415 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.416 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.416 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.416 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.416 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.417 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.417 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.417 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.417 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.417 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.419 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.419 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.420 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.421 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.421 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.421 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.421 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.422 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.422 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.422 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.423 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.423 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.423 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.424 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.424 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.424 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.425 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.425 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.426 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.426 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.426 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.427 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.428 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.428 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.428 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.428 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.429 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.430 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.430 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.431 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.431 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.431 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.432 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.433 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.433 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.433 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.433 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.433 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.434 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.435 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.438 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.438 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.438 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.439 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.440 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.440 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.440 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.440 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.440 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.441 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.441 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.442 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.444 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.444 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.445 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.445 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.446 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.446 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.446 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.446 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.446 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.447 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.447 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.447 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.447 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.448 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.448 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.448 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.449 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.449 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.449 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.450 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.452 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.453 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.453 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.453 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.453 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.453 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.454 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.454 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.454 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.454 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.455 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.456 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.457 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.458 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.459 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.459 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.459 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.459 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.462 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.462 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.462 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.462 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.464 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.466 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.467 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.468 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.468 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.470 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.470 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.471 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.472 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.473 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.473 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.473 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.473 - INFO 7760  [ no-tenant] --- [ main] .RepositoryConfigurationExtensionSupport : 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.
2026-05-29 19:17:14.768 - INFO 7760  [ no-tenant] --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 657 ms. Found 6 JDBC repository interfaces.
2026-05-29 19:17:16.886 - INFO 7760  [ no-tenant] --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.
2026-05-29 19:17:16.895 - INFO 7760  [ no-tenant] --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.
2026-05-29 19:17:18.104 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.117 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.335 - INFO 7760  [ no-tenant] --- [ main] org.reflections.Reflections  : Reflections took 7 ms to scan 1 urls, producing 0 keys and 0 values
2026-05-29 19:17:18.339 - WARN 7760  [ no-tenant] --- [ main] o.a.f.i.core.config.cache.CacheConfig  : The following cache configurations are defined but cache does not exists: [userTFAccessToken]
2026-05-29 19:17:18.343 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.354 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.360 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.364 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.407 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:18.415 - WARN 7760  [ no-tenant] --- [ main] trationDelegate$BeanPostProcessorChecker : 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.
2026-05-29 19:17:19.255 - INFO 7760  [ no-tenant] --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8443 (https)
2026-05-29 19:17:19.278 - INFO 7760  [ no-tenant] --- [ main] o.apache.catalina.core.StandardService  : Starting service [Tomcat]
2026-05-29 19:17:19.279 - INFO 7760  [ no-tenant] --- [ main] o.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/10.1.49]
2026-05-29 19:17:19.328 - INFO 7760  [ no-tenant] --- [ main] o.a.c.c.C.[.[.[/fineract-provider]  : Initializing Spring embedded WebApplicationContext
2026-05-29 19:17:19.329 - INFO 7760  [ no-tenant] --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 13673 ms
2026-05-29 19:17:19.577 - INFO 7760  [ no-tenant] --- [ main] com.zaxxer.hikari.HikariDataSource  : HikariPool-1 - Starting...
2026-05-29 19:17:19.860 - INFO 7760  [ no-tenant] --- [ main] com.zaxxer.hikari.pool.HikariPool  : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@5917b984
2026-05-29 19:17:19.861 - INFO 7760  [ no-tenant] --- [ main] com.zaxxer.hikari.HikariDataSource  : HikariPool-1 - Start completed.
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : Upgrading tenant store DB at localhost:5432
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.username: postgres
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.password: ****
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.parameters:
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.timezone: Asia/Kolkata
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.description: Default Demo Tenant
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.identifier: default
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - fineract.tenant.name: fineract_default
2026-05-29 19:17:20.050 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : - liquibase.analytics.enabled: false
2026-05-29 19:17:25.487 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : Tenant store upgrade finished
2026-05-29 19:17:25.487 - INFO 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : Upgrading all tenants
2026-05-29 19:17:25.501 - INFO 7760  [ default] --- [lTaskExecutor-1] a.f.i.c.s.m.TenantDatabaseUpgradeService : Upgrade for tenant default has started
2026-05-29 19:17:25.842 - INFO 7760  [ default] --- [lTaskExecutor-1] com.zaxxer.hikari.HikariDataSource  : HikariPool-2 - Starting...
2026-05-29 19:17:25.946 - INFO 7760  [ default] --- [lTaskExecutor-1] com.zaxxer.hikari.pool.HikariPool  : HikariPool-2 - Added connection org.postgresql.jdbc.PgConnection@625805c1
2026-05-29 19:17:25.946 - INFO 7760  [ default] --- [lTaskExecutor-1] com.zaxxer.hikari.HikariDataSource  : HikariPool-2 - Start completed.
2026-05-29 19:17:31.700 -ERROR 7760  [ default] --- [lTaskExecutor-1] liquibase.changelog  : 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
2026-05-29 19:17:31.748 - INFO 7760  [ default] --- [lTaskExecutor-1] com.zaxxer.hikari.HikariDataSource  : HikariPool-2 - Shutdown initiated...
2026-05-29 19:17:31.750 - INFO 7760  [ default] --- [lTaskExecutor-1] com.zaxxer.hikari.HikariDataSource  : HikariPool-2 - Shutdown completed.
2026-05-29 19:17:31.752 -ERROR 7760  [ no-tenant] --- [ main] a.f.i.c.s.m.TenantDatabaseUpgradeService : 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
2026-05-29 19:17:31.761 -ERROR 7760  [ no-tenant] --- [ main] o.s.b.web.embedded.tomcat.TomcatStarter  : 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
2026-05-29 19:17:31.812 - INFO 7760  [ no-tenant] --- [ main] o.apache.catalina.core.StandardService  : Stopping service [Tomcat]
2026-05-29 19:17:31.817 - WARN 7760  [ no-tenant] --- [ main] o.a.c.loader.WebappClassLoaderBase  : 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)
2026-05-29 19:17:31.820 - WARN 7760  [ no-tenant] --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
2026-05-29 19:17:31.820 - INFO 7760  [ no-tenant] --- [ main] com.zaxxer.hikari.HikariDataSource  : HikariPool-1 - Shutdown initiated...
2026-05-29 19:17:31.822 - INFO 7760  [ no-tenant] --- [ main] com.zaxxer.hikari.HikariDataSource  : HikariPool-1 - Shutdown completed.
2026-05-29 19:17:31.847 - INFO 7760  [ no-tenant] --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-05-29 19:17:31.875 -ERROR 7760  [ no-tenant] --- [ main] o.s.boot.SpringApplication  : Application run failed
org.springframework.context.ApplicationContextException: Unable to start web server
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:170)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149)
at org.apache.fineract.ServerApplication.main(ServerApplication.java:57)
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:147)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:107)
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:517)
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:219)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:193)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:167)
... 7 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration': Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'jerseyConfig': Invocation of init method failed
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:413)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:230)
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:221)
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:106)
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:95)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:271)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:245)
at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:52)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4452)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:770)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1201)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1191)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81)
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:747)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:410)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:868)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:436)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:128)
... 12 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jerseyConfig': Invocation of init method failed
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:429)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1818)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 61 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'journalEntriesApiResource' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\accounting\journalentry\api\JournalEntriesApiResource.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'springSecurityPlatformSecurityContext' defined in URL [jar:file:/C:/Users/adity/Music/CBS/fineract/fineract-security/build/libs/fineract-security-1.15.0-SNAPSHOT.jar!/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansWithAnnotation(DefaultListableBeanFactory.java:796)
at org.springframework.context.support.AbstractApplicationContext.getBeansWithAnnotation(AbstractApplicationContext.java:1440)
at org.apache.fineract.infrastructure.core.jersey.JerseyConfig.setup(JerseyConfig.java:58)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219)
... 74 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springSecurityPlatformSecurityContext' defined in URL [jar:file:/C:/Users/adity/Music/CBS/fineract/fineract-security/build/libs/fineract-security-1.15.0-SNAPSHOT.jar!/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 91 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'configurationDomainServiceJpa' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\configuration\domain\ConfigurationDomainServiceJpa.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1395)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1760)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 105 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'permissionRepository' defined in org.apache.fineract.useradministration.domain.PermissionRepository defined in @EnableJpaRepositories declared on JPAConfig: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager'
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:377)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1725)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1474)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:606)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1698)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 119 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaSharedEM_entityManagerFactory': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:377)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:691)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:513)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1375)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1205)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
... 132 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tenantDatabaseUpgradeService' defined in file [C:\Users\adity\Music\CBS\fineract\fineract-provider\build\classes\java\main\org\apache\fineract\infrastructure\core\service\migration\TenantDatabaseUpgradeService.class]: Tenant upgrades had exceptions
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1826)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:373)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:365)
... 144 common frames omitted
Caused by: java.lang.RuntimeException: Tenant upgrades had exceptions
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.upgradeIndividualTenants(TenantDatabaseUpgradeService.java:167)
at org.apache.fineract.infrastructure.core.service.migration.TenantDatabaseUpgradeService.afterPropertiesSet(TenantDatabaseUpgradeService.java:93)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1873)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1822)
... 153 common frames omitted
> Task :fineract-provider:bootRun FAILED
[Incubating] Problems report is available at: file:///C:/Users/adity/Music/CBS/fineract/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':fineract-provider:bootRun'.
> Process 'command 'C:\Program Files\Eclipse Adoptium\jdk-21.0.9.10-hotspot\bin\java.exe'' finished with non-zero exit value 1
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.14.3/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 1m 30s
83 actionable tasks: 5 executed, 78 up-to-date
Resource usage collection shutdown not awaited
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -600,6 +600,27 @@ public class AccountingProcessorHelper {
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount);
} }
/**
* Creates a direct transfer journal entry between two savings-type accounts (savings or current),
* each with their own product GL mapping and entity ID so the correct account number appears per entry.
*
* @param debitProductId product ID whose SAVINGS_CONTROL GL is debited
* @param debitEntityId entity (account) ID shown on the debit entry
* @param creditProductId product ID whose SAVINGS_CONTROL GL is credited
* @param creditEntityId entity (account) ID shown on the credit entry
*/
public void createDirectTransferJournalEntriesForSavings(final Office office, final String currencyCode,
final Long debitProductId, final Long debitEntityId,
final Long creditProductId, final Long creditEntityId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(debitProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(creditProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount);
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
}
/** /**
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
* *
@@ -106,8 +106,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ") .append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.append(" eao.external_id as externalAssetOwner, ") .append(" eao.external_id as externalAssetOwner, ")
.append(" CASE ") .append(" CASE ")
.append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' THEN ca.account_no ") .append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
.append(" ELSE NULL ") .append(" ELSE NULL ")
.append(" END as savingsAccountNo "); .append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) { if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" join m_currency curr on curr.code = journalEntry.currency_code ") .append(" join m_currency curr on curr.code = journalEntry.currency_code ")
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ") .append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ") .append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id NOT LIKE 'CA%' ") .append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' "); .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
if (associationParametersData.isTransactionDetailsRequired()) { if (associationParametersData.isTransactionDetailsRequired()) {
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ") sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ") .append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
@@ -265,6 +264,10 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
int arrayPos = 0; int arrayPos = 0;
String whereClose = " where "; String whereClose = " where ";
// Exclude intermediary/reversed entries from all GL views
sqlBuilder.append(whereClose).append("(journalEntry.reversed IS NULL OR journalEntry.reversed = false)");
whereClose = " and ";
if (StringUtils.isNotBlank(transactionId)) { if (StringUtils.isNotBlank(transactionId)) {
sqlBuilder.append(whereClose).append(" journalEntry.transaction_id = ?"); sqlBuilder.append(whereClose).append(" journalEntry.transaction_id = ?");
objectArray[arrayPos] = transactionId; objectArray[arrayPos] = transactionId;
@@ -581,7 +584,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
"SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS ob_debit, " "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS ob_debit, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit " + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit "
+ "FROM acc_gl_journal_entry je " + "FROM acc_gl_journal_entry je "
+ "WHERE je.account_id = ? AND je.entry_date < ?"); + "WHERE je.account_id = ? AND je.entry_date < ? AND (je.reversed IS NULL OR je.reversed = false)");
obParams.add(glAccountId); obParams.add(glAccountId);
obParams.add(fromDate); obParams.add(fromDate);
if (officeId != null && officeId > 0) { if (officeId != null && officeId > 0) {
@@ -608,7 +611,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS dw_credit, " + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS dw_credit, "
+ "COUNT(*) AS dw_count " + "COUNT(*) AS dw_count "
+ "FROM acc_gl_journal_entry je " + "FROM acc_gl_journal_entry je "
+ "WHERE 1=1"); + "WHERE (je.reversed IS NULL OR je.reversed = false)");
if (glAccountId != null && glAccountId > 0) { if (glAccountId != null && glAccountId > 0) {
dwSql.append(" AND je.account_id = ?"); dwSql.append(" AND je.account_id = ?");
dwParams.add(glAccountId); dwParams.add(glAccountId);
@@ -684,7 +687,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
final StringBuilder totSql = new StringBuilder( final StringBuilder totSql = new StringBuilder(
"SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS totalDebit, " "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS totalDebit, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS totalCredit " + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS totalCredit "
+ "FROM acc_gl_journal_entry je WHERE 1=1"); + "FROM acc_gl_journal_entry je WHERE (je.reversed IS NULL OR je.reversed = false)");
if (glAccountId != null && glAccountId > 0) { if (glAccountId != null && glAccountId > 0) {
totSql.append(" AND je.account_id = ?"); totSql.append(" AND je.account_id = ?");
totParams.add(glAccountId); totParams.add(glAccountId);
@@ -723,7 +726,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
{ {
final List<Object> cntParams = new ArrayList<>(); final List<Object> cntParams = new ArrayList<>();
final StringBuilder cntSql = new StringBuilder( final StringBuilder cntSql = new StringBuilder(
"SELECT COUNT(*) FROM acc_gl_journal_entry je WHERE 1=1"); "SELECT COUNT(*) FROM acc_gl_journal_entry je WHERE (je.reversed IS NULL OR je.reversed = false)");
if (glAccountId != null && glAccountId > 0) { if (glAccountId != null && glAccountId > 0) {
cntSql.append(" AND je.account_id = ?"); cntSql.append(" AND je.account_id = ?");
cntParams.add(glAccountId); cntParams.add(glAccountId);
@@ -146,6 +146,14 @@ public class SecurityConfig {
auth.requestMatchers(API_MATCHER.matcher(HttpMethod.OPTIONS, "/api/**")).permitAll() auth.requestMatchers(API_MATCHER.matcher(HttpMethod.OPTIONS, "/api/**")).permitAll()
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/echo")).permitAll() .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/echo")).permitAll()
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/authentication")).permitAll() .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/authentication")).permitAll()
// Meeting vote public endpoints — secured by UUID token in the URL path
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/public/meeting-vote/**")).permitAll()
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/public/meeting-vote/**")).permitAll()
// IMPS receiver — Fineract acts as the bank-side endpoint under
// IMPS_BANK_API_SPEC_CBS_1.0 §6.2.x. LincPay (the switch) calls these without
// a Fineract Bearer token; security is enforced at the JWT envelope
// (signature + encryption) by ImpsJwtUtil / ImpsCryptoUtil inside the handler.
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/imps/**")).permitAll()
// Login OTP — same posture as PAN/GSTIN public verification: provider-credentialed, // Login OTP — same posture as PAN/GSTIN public verification: provider-credentialed,
// no Fineract auth required. // no Fineract auth required.
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/login/otp/generate")).permitAll() .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/login/otp/generate")).permitAll()
@@ -0,0 +1,124 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.imps.crypto.ImpsCryptoUtil;
import org.apache.fineract.infrastructure.imps.crypto.ImpsEnvelope;
import org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil;
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
import org.apache.fineract.infrastructure.imps.service.ImpsCbsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Receiver-side IMPS CBS endpoints — Fineract acting as the bank under
* IMPS_BANK_API_SPEC_CBS_1.0 §6. Each endpoint:
*
* <ol>
* <li>Reads the {@link ImpsEnvelope} ({@code data}, {@code signature}).</li>
* <li>Verifies the inbound JWT via {@link ImpsJwtUtil}.</li>
* <li>Decrypts {@code data} via {@link ImpsCryptoUtil}.</li>
* <li>Hands the typed {@link ImpsRequestData} to {@link ImpsCbsService}.</li>
* <li>Encrypts the response, signs an outbound JWT, returns the envelope.</li>
* </ol>
*/
@Path("/v1/imps")
@Component
@Tag(name = "IMPS CBS (Bank Side)", description = "Receiver-side endpoints for IMPS_BANK_API_SPEC_CBS_1.0 §6.2.")
@RequiredArgsConstructor
public class ImpsCbsApiResource {
private static final Logger log = LoggerFactory.getLogger(ImpsCbsApiResource.class);
private final ImpsCryptoUtil cryptoUtil;
private final ImpsJwtUtil jwtUtil;
private final ImpsCbsService cbsService;
private final ObjectMapper objectMapper = new ObjectMapper();
@POST
@Path("/ReqPay/v1/urn:txnid:{txnid}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "IMPS ReqPay (§6.2.1)")
public Response reqPay(@PathParam("txnid") String txnid, ImpsEnvelope body) {
return handle("ReqPay", txnid, body, cbsService::reqPay);
}
@POST
@Path("/ReqValAdd/v1/urn:txnid:{txnid}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "IMPS ReqValAdd (§6.2.3)")
public Response reqValAdd(@PathParam("txnid") String txnid, ImpsEnvelope body) {
return handle("ReqValAdd", txnid, body, cbsService::reqValAdd);
}
@POST
@Path("/ReqChkTxn/v1/urn:txnid:{txnid}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "IMPS ReqChkTxn (§6.2.2)")
public Response reqChkTxn(@PathParam("txnid") String txnid, ImpsEnvelope body) {
return handle("ReqChkTxn", txnid, body, cbsService::reqChkTxn);
}
private Response handle(String api, String txnid, ImpsEnvelope body, Function<ImpsRequestData, ImpsResponseData> handler) {
log.info("[ImpsCbs] Inbound {} txnid={}", api, txnid);
try {
if (!jwtUtil.verifyJwtSignature(body.getSignature())) {
log.warn("[ImpsCbs] {} txnid={} — JWT verification FAILED", api, txnid);
return Response.status(Response.Status.BAD_REQUEST).entity(new ImpsEnvelope("invalid-signature", "")).build();
}
String decrypted = cryptoUtil.decryptJson(body.getData());
if (decrypted == null) {
log.warn("[ImpsCbs] {} txnid={} — decrypt FAILED", api, txnid);
return Response.status(Response.Status.BAD_REQUEST).entity(new ImpsEnvelope("decrypt-failed", "")).build();
}
ImpsRequestData request = objectMapper.readValue(decrypted, ImpsRequestData.class);
ImpsResponseData response = handler.apply(request);
String respJson = objectMapper.writeValueAsString(response);
log.info("[ImpsCbs] {} txnid={} responding status={} responseCode={}", api, txnid, response.getStatus(),
response.getResponseCode());
String encResp = cryptoUtil.encryptJson(respJson);
String jwt = jwtUtil.signPayloadAsJwt(respJson);
return Response.ok(new ImpsEnvelope(encResp, jwt)).build();
} catch (Exception e) {
log.error("[ImpsCbs] {} txnid={} unhandled error: {}", api, txnid, e.toString(), e);
return Response.serverError().entity(new ImpsEnvelope("error: " + e.getMessage(), "")).build();
}
}
}
@@ -0,0 +1,166 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.fineract.infrastructure.imps.crypto.ImpsCryptoUtil;
import org.apache.fineract.infrastructure.imps.crypto.ImpsEnvelope;
import org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil;
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* Outbound IMPS calls — Fineract acting as the bank initiating ReqPay / ReqValAdd /
* ReqChkTxn against the LincPay switch.
*
* <p>Each call: build {@link ImpsRequestData} → encrypt with LincPay public key →
* sign JWT with CBS private key → POST envelope → verify response JWT → decrypt
* response body → parse to {@link ImpsResponseData}.
*
* <p>The crypto utilities are symmetric across inbound and outbound: encryptJson
* uses the peer's public key (LincPay), decryptJson uses our private key (CBS).
* No additional key configuration is needed beyond what's already set for the
* inbound receiver.
*/
@Service
public class ImpsSwitchClient {
private static final Logger log = LoggerFactory.getLogger(ImpsSwitchClient.class);
private static final MediaType JSON = MediaType.get("application/json");
@Value("${fineract.imps.switch.base-url:}")
private String baseUrl;
@Value("${fineract.imps.switch.timeout-ms:10000}")
private int timeoutMs;
private final ImpsCryptoUtil cryptoUtil;
private final ImpsJwtUtil jwtUtil;
private final ObjectMapper objectMapper = new ObjectMapper();
private OkHttpClient httpClient;
public ImpsSwitchClient(ImpsCryptoUtil cryptoUtil, ImpsJwtUtil jwtUtil) {
this.cryptoUtil = cryptoUtil;
this.jwtUtil = jwtUtil;
}
private OkHttpClient client() {
if (httpClient == null) {
httpClient = new OkHttpClient.Builder().connectTimeout(Duration.ofMillis(timeoutMs))
.readTimeout(timeoutMs, TimeUnit.MILLISECONDS).writeTimeout(timeoutMs, TimeUnit.MILLISECONDS).build();
}
return httpClient;
}
public ImpsResponseData reqPay(String txnId, ImpsRequestData payload) {
return invoke("IMPS", "ReqPay", txnId, payload);
}
public ImpsResponseData reqValAdd(String txnId, ImpsRequestData payload) {
return invoke("IMPS", "ReqValAdd", txnId, payload);
}
public ImpsResponseData reqChkTxn(String txnId, ImpsRequestData payload) {
return invoke("IMPS", "ReqChkTxn", txnId, payload);
}
public ImpsResponseData neftPay(String txnId, ImpsRequestData payload) {
return invoke("NEFT", "ReqPay", txnId, payload);
}
public ImpsResponseData neftChkTxn(String txnId, ImpsRequestData payload) {
return invoke("NEFT", "ReqChkTxn", txnId, payload);
}
public ImpsResponseData rtgsPay(String txnId, ImpsRequestData payload) {
return invoke("RTGS", "ReqPay", txnId, payload);
}
public ImpsResponseData rtgsChkTxn(String txnId, ImpsRequestData payload) {
return invoke("RTGS", "ReqChkTxn", txnId, payload);
}
/**
* Best-effort recall on the switch. Real-world semantics vary by channel:
* IMPS uses a dispute / chargeback flow rather than a synchronous recall;
* NEFT supports {@code ReqDirectRet} for return-on-no-credit; RTGS recall
* is rare and usually manual. Callers should treat exceptions as soft
* failures and proceed with local reversal.
*/
public ImpsResponseData recall(String channel, String txnId, ImpsRequestData payload) {
return invoke(channel, "ReqRecall", txnId, payload);
}
private ImpsResponseData invoke(String channel, String api, String txnId, ImpsRequestData payload) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalStateException("fineract.imps.switch.base-url is not set");
}
try {
String plainBody = objectMapper.writeValueAsString(payload);
String encrypted = cryptoUtil.encryptJson(plainBody);
String signature = jwtUtil.signPayloadAsJwt(plainBody);
ImpsEnvelope env = new ImpsEnvelope(encrypted, signature);
String envJson = objectMapper.writeValueAsString(env);
String url = baseUrl.replaceAll("/+$", "") + "/" + channel + "/" + api + "/v1/urn:txnid:" + txnId;
log.info("[ImpsSwitchClient] {} {} txnId={} → POST {}", channel, api, txnId, url);
Request req = new Request.Builder().url(url).post(RequestBody.create(envJson, JSON)).build();
try (Response resp = client().newCall(req).execute()) {
String respBody = bodyAsString(resp.body());
if (!resp.isSuccessful()) {
log.warn("[ImpsSwitchClient] {} {} txnId={} HTTP {} body={}", channel, api, txnId, resp.code(), respBody);
throw new IOException("Switch returned HTTP " + resp.code());
}
ImpsEnvelope respEnv = objectMapper.readValue(respBody, ImpsEnvelope.class);
if (!jwtUtil.verifyJwtSignature(respEnv.getSignature())) {
throw new IllegalStateException("Response JWT signature verification failed");
}
String decrypted = cryptoUtil.decryptJson(respEnv.getData());
if (decrypted == null) {
throw new IllegalStateException("Response payload decrypt failed");
}
log.info("[ImpsSwitchClient] {} {} txnId={} ← decrypted response", channel, api, txnId);
return objectMapper.readValue(decrypted, ImpsResponseData.class);
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(channel + " " + api + " call failed: " + e.getMessage(), e);
}
}
private static String bodyAsString(ResponseBody body) throws IOException {
return body == null ? "" : body.string();
}
}
@@ -0,0 +1,249 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.crypto;
import jakarta.annotation.PostConstruct;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Base64;
import java.util.Enumeration;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Encryption / decryption matching IMPS_BANK_API_SPEC_CBS_1.0 §4.3.
*
* <p>Wire format: {@code <base64(wrappedAESKey)>:<base64(iv)>:<base64(cipherText)>}.
* AES-256-GCM with a 12-byte IV and a 128-bit tag; the AES key is wrapped via
* RSA-OAEP-SHA256.
*
* <h3>Key directions</h3>
* <ul>
* <li><b>Inbound</b>: {@link #decryptJson(String)} — unwrap the AES key with
* the CBS private key loaded from {@code fineract.imps.cbs.private-key-path}.</li>
* <li><b>Outbound</b>: {@link #encryptJson(String)} — wrap the AES key with
* the LincPay public key loaded from
* {@code fineract.imps.lincpay.public-key-path}.</li>
* </ul>
*
* <p>Keys are loaded once at startup in {@link #init()}.
*/
@Component
public class ImpsCryptoUtil {
private static final Logger log = LoggerFactory.getLogger(ImpsCryptoUtil.class);
private static final String AES = "AES";
private static final String AES_GCM = "AES/GCM/NoPadding";
private static final String RSA = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
private static final int GCM_TAG_LENGTH = 128;
private static final int AES_KEY_SIZE = 256;
private static final int IV_SIZE = 12;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
@Value("${fineract.imps.cbs.private-key-path:}")
private String cbsPrivateKeyPath;
@Value("${fineract.imps.cbs.private-key-password:}")
private String cbsPrivateKeyPassword;
@Value("${fineract.imps.lincpay.public-key-path:}")
private String lincpayPublicKeyPath;
private PrivateKey cbsPrivateKey;
private PublicKey lincpayPublicKey;
@PostConstruct
void init() {
if (cbsPrivateKeyPath == null || cbsPrivateKeyPath.isBlank()) {
log.info("[ImpsCryptoUtil] fineract.imps.cbs.private-key-path not set — CBS private key NOT loaded");
} else {
try {
cbsPrivateKey = loadFirstPrivateKey(cbsPrivateKeyPath, cbsPrivateKeyPassword);
log.info("[ImpsCryptoUtil] CBS private key loaded from {}", cbsPrivateKeyPath);
} catch (Exception e) {
log.error("[ImpsCryptoUtil] Failed to load CBS private key from {}: {}", cbsPrivateKeyPath, e.toString());
}
}
if (lincpayPublicKeyPath == null || lincpayPublicKeyPath.isBlank()) {
log.info("[ImpsCryptoUtil] fineract.imps.lincpay.public-key-path not set — LincPay public key NOT loaded");
} else {
try {
lincpayPublicKey = loadPublicKey(lincpayPublicKeyPath);
log.info("[ImpsCryptoUtil] LincPay public key loaded from {}", lincpayPublicKeyPath);
} catch (Exception e) {
log.error("[ImpsCryptoUtil] Failed to load LincPay public key from {}: {}", lincpayPublicKeyPath, e.toString());
}
}
}
/** Decrypt inbound payload using the CBS private key. */
public String decryptJson(String encryptedPayload) {
if (cbsPrivateKey == null) {
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
}
try {
String[] parts = encryptedPayload.split(":");
if (parts.length != 3) {
log.error("Invalid encrypted payload format — expected 3 colon-separated parts");
return null;
}
byte[] iv = Base64.getDecoder().decode(parts[1]);
SecretKey aesKey = unwrapAESKey(parts[0], cbsPrivateKey);
if (aesKey == null) {
return null;
}
return decryptData(parts[2], aesKey, iv);
} catch (Exception e) {
log.error("Error decrypting inbound payload", e);
return null;
}
}
/** Encrypt outbound payload using the LincPay public key. */
public String encryptJson(String json) {
if (lincpayPublicKey == null) {
throw new IllegalStateException("LincPay public key not loaded — check fineract.imps.lincpay.public-key-path");
}
try {
SecretKey aesKey = generateAESKey();
if (aesKey == null) {
return null;
}
byte[] iv = generateIV();
String cipherText = encryptData(json, aesKey, iv);
String wrappedKey = wrapAESKey(aesKey, lincpayPublicKey);
if (cipherText == null || wrappedKey == null) {
return null;
}
return wrappedKey + ":" + Base64.getEncoder().encodeToString(iv) + ":" + cipherText;
} catch (Exception e) {
log.error("Error encrypting outbound payload", e);
return null;
}
}
/** Exposed for {@link ImpsJwtUtil} — sign outbound JWT with the same CBS private key. */
public PrivateKey getCbsPrivateKey() {
return cbsPrivateKey;
}
/** Exposed for {@link ImpsJwtUtil} — verify inbound JWT with the LincPay public key. */
public PublicKey getLincpayPublicKey() {
return lincpayPublicKey;
}
private static SecretKey generateAESKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(AES_KEY_SIZE);
return keyGen.generateKey();
}
private static byte[] generateIV() {
byte[] iv = new byte[IV_SIZE];
SECURE_RANDOM.nextBytes(iv);
return iv;
}
private static String wrapAESKey(SecretKey aesKey, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.WRAP_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.wrap(aesKey));
} catch (Exception e) {
log.error("Error wrapping AES key", e);
return null;
}
}
private static SecretKey unwrapAESKey(String wrappedBase64, PrivateKey privateKey) {
try {
byte[] wrapped = Base64.getDecoder().decode(wrappedBase64);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.UNWRAP_MODE, privateKey);
return (SecretKey) cipher.unwrap(wrapped, AES, Cipher.SECRET_KEY);
} catch (Exception e) {
log.error("Error unwrapping AES key", e);
return null;
}
}
private static String encryptData(String plain, SecretKey aesKey, byte[] iv) {
try {
Cipher cipher = Cipher.getInstance(AES_GCM);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(enc);
} catch (Exception e) {
log.error("Error encrypting data", e);
return null;
}
}
private static String decryptData(String encBase64, SecretKey aesKey, byte[] iv) {
try {
Cipher cipher = Cipher.getInstance(AES_GCM);
cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] dec = cipher.doFinal(Base64.getDecoder().decode(encBase64));
return new String(dec, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Error decrypting data", e);
return null;
}
}
private static PrivateKey loadFirstPrivateKey(String p12Path, String password) throws Exception {
char[] pwd = password != null ? password.toCharArray() : null;
try (FileInputStream fis = new FileInputStream(p12Path)) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(fis, pwd);
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (ks.isKeyEntry(alias)) {
return (PrivateKey) ks.getKey(alias, pwd);
}
}
throw new IllegalStateException("No private-key entry in keystore at " + p12Path);
}
}
private static PublicKey loadPublicKey(String crtPath) throws Exception {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
try (InputStream is = new BufferedInputStream(new FileInputStream(crtPath))) {
Certificate cert = cf.generateCertificate(is);
return cert.getPublicKey();
}
}
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.crypto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Wire envelope shared with the LincPay IMPS gateway:
* {@code { "data": "<encrypted JSON>", "signature": "<RS256 JWT>" } }.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ImpsEnvelope {
private String data;
private String signature;
}
@@ -0,0 +1,109 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.crypto;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* JWT helpers for IMPS_BANK_API_SPEC_CBS_1.0 §4.2.
*
* <ul>
* <li><b>Verify</b> inbound LincPay-signed JWT using the LincPay public key
* loaded by {@link ImpsCryptoUtil}.</li>
* <li><b>Sign</b> outbound JWT (RS256, 1h TTL, payload in {@code "data"}
* claim) using the CBS private key loaded by {@link ImpsCryptoUtil}.</li>
* </ul>
*
* <p>{@link #verifyJwtSignature(String)} returns {@code true} when the LincPay
* public key is not configured — useful for smoke testing the encrypt/decrypt
* path before both certs are wired up.
*/
@Component
public class ImpsJwtUtil {
private static final Logger log = LoggerFactory.getLogger(ImpsJwtUtil.class);
private static final long JWT_TTL_MILLIS = 3600_000L;
private final ImpsCryptoUtil cryptoUtil;
public ImpsJwtUtil(ImpsCryptoUtil cryptoUtil) {
this.cryptoUtil = cryptoUtil;
}
/** Sign a JSON payload — payload carried in the {@code "data"} claim. */
public String signPayloadAsJwt(String jsonPayload) {
PrivateKey privateKey = cryptoUtil.getCbsPrivateKey();
if (privateKey == null) {
log.warn("[ImpsJwtUtil] CBS private key not loaded — outbound JWT signing SKIPPED");
return "";
}
try {
JWSSigner signer = new RSASSASigner(privateKey);
JWTClaimsSet claims = new JWTClaimsSet.Builder().claim("data", jsonPayload).issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + JWT_TTL_MILLIS)).build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(), claims);
signedJWT.sign(signer);
return signedJWT.serialize();
} catch (Exception e) {
log.error("[ImpsJwtUtil] Error signing outbound JWT: {}", e.toString(), e);
return "";
}
}
/** Verify an inbound JWT against the LincPay public key. */
public boolean verifyJwtSignature(String jwtToken) {
if (jwtToken == null || jwtToken.isBlank()) {
log.warn("[ImpsJwtUtil] Inbound JWT is blank — verification skipped (signature missing)");
return true;
}
PublicKey pub = cryptoUtil.getLincpayPublicKey();
if (pub == null) {
log.warn("[ImpsJwtUtil] LincPay public key not loaded — verification skipped (DEV MODE)");
return true;
}
if (!(pub instanceof RSAPublicKey rsaPub)) {
log.error("[ImpsJwtUtil] LincPay public key is not RSA (type={}) — verification FAILED", pub.getAlgorithm());
return false;
}
try {
SignedJWT signedJWT = SignedJWT.parse(jwtToken);
JWSVerifier verifier = new RSASSAVerifier(rsaPub);
return signedJWT.verify(verifier);
} catch (Exception e) {
log.error("[ImpsJwtUtil] Verification error: {}", e.toString());
return false;
}
}
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Account block under {@code payer.account} / {@code payee.account}. */
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class AccountData {
private String accountNumber;
private String ifsc;
private String accountType;
}
@@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Union of the decrypted inbound request fields across the three IMPS APIs
* (ReqPay §6.2.1, ReqValAdd §6.2.3, ReqChkTxn §6.2.2). Fields not present in a
* given API stay {@code null}.
*
* <p>Covers the fields the simulator extracted via {@code JsonNode} dotted paths.
* Extend with additional spec fields (e.g. {@code remarks}, {@code merchantCode},
* {@code signature}) as the IMPS_BANK_API_SPEC_CBS_1.0 §6 schemas are consulted.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ImpsRequestData {
private String txnId;
private String orgTxnId;
private String refId;
private String utr;
private String amount;
private PartyData payer;
private PartyData payee;
}
@@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Union of CBS response fields across RespPay / RespValAdd / RespChkTxn per
* IMPS_BANK_API_SPEC_CBS_1.0 §6.2.x. {@code @JsonInclude(NON_NULL)} so each API
* only emits the fields its spec section requires.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ImpsResponseData {
private String txnId;
private String orgTxnId;
private String refId;
private String timeStamp;
private String utr;
private String amount;
private String status;
private String responseCode;
private String accountNumber;
private String ifsc;
private String maskName;
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Payer / payee block on an IMPS request. */
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class PartyData {
private String name;
private String mobile;
private AccountData account;
}
@@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* One row per inbound IMPS request received by Fineract acting as the CBS.
* Records audit + idempotency state — a retried {@code txnId} should resolve
* back to the same {@code status} / {@code responseCode} via {@code unique(txn_id, api)}.
*/
@Entity
@Table(name = "m_imps_transaction")
@Getter
@Setter
@NoArgsConstructor
public class ImpsTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** Which spec endpoint produced this row — REQ_PAY / REQ_VAL_ADD / REQ_CHK_TXN. */
@Column(name = "api", nullable = false, length = 32)
private String api;
@Column(name = "txn_id", nullable = false, length = 64)
private String txnId;
@Column(name = "org_txn_id", length = 64)
private String orgTxnId;
@Column(name = "ref_id", length = 64)
private String refId;
@Column(name = "utr", length = 64)
private String utr;
@Column(name = "amount", length = 32)
private String amount;
@Column(name = "payee_account_number", length = 64)
private String payeeAccountNumber;
@Column(name = "payee_ifsc", length = 32)
private String payeeIfsc;
/** Resolved scenario — SUCCESS / DEEMED / FAILED. */
@Column(name = "status", length = 16)
private String status;
/** IMPS response code — 00 / 91 / XC / 8 / etc. */
@Column(name = "response_code", length = 8)
private String responseCode;
/** Decrypted request JSON, kept for audit / replay. */
@Lob
@Column(name = "request_json")
private String requestJson;
/** Outbound response JSON before encryption, kept for audit. */
@Lob
@Column(name = "response_json")
private String responseJson;
public static ImpsTransaction newRow(String api, String txnId) {
ImpsTransaction t = new ImpsTransaction();
t.api = api;
t.txnId = txnId;
return t;
}
}
@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ImpsTransactionRepository extends JpaRepository<ImpsTransaction, Long> {
Optional<ImpsTransaction> findFirstByApiAndTxnId(String api, String txnId);
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.service;
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
/**
* Receiver-side IMPS handler. Each method takes the decrypted request and
* returns the unencrypted response body — the controller layer is responsible
* for envelope encryption / signing around these calls.
*/
public interface ImpsCbsService {
ImpsResponseData reqPay(ImpsRequestData request);
ImpsResponseData reqValAdd(ImpsRequestData request);
ImpsResponseData reqChkTxn(ImpsRequestData request);
}
@@ -0,0 +1,144 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.imps.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.imps.data.AccountData;
import org.apache.fineract.infrastructure.imps.data.ImpsRequestData;
import org.apache.fineract.infrastructure.imps.data.ImpsResponseData;
import org.apache.fineract.infrastructure.imps.data.PartyData;
import org.apache.fineract.infrastructure.imps.domain.ImpsTransaction;
import org.apache.fineract.infrastructure.imps.domain.ImpsTransactionRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* STUB implementation — returns hard-coded SUCCESS for every request and
* persists an audit row per call. Replace the {@link #buildResponse} body
* with real Fineract account lookup / debit-credit logic when that scope is
* agreed.
*
* <p>Idempotency: a retried {@code (api, txnId)} short-circuits to the
* previously-recorded response instead of running the handler twice.
*/
@Service
@RequiredArgsConstructor
public class ImpsCbsServiceImpl implements ImpsCbsService {
private static final Logger log = LoggerFactory.getLogger(ImpsCbsServiceImpl.class);
private static final DateTimeFormatter ISO_OFFSET = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
private final ImpsTransactionRepository repository;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@Transactional
public ImpsResponseData reqPay(ImpsRequestData request) {
return handle("REQ_PAY", request, true);
}
@Override
@Transactional
public ImpsResponseData reqValAdd(ImpsRequestData request) {
return handle("REQ_VAL_ADD", request, false);
}
@Override
@Transactional
public ImpsResponseData reqChkTxn(ImpsRequestData request) {
return handle("REQ_CHK_TXN", request, false);
}
private ImpsResponseData handle(String api, ImpsRequestData request, boolean echoIfsc) {
Optional<ImpsTransaction> prior = repository.findFirstByApiAndTxnId(api, request.getTxnId());
if (prior.isPresent()) {
log.info("[ImpsCbsService] {} txnId={} idempotent replay — returning prior response", api, request.getTxnId());
return readResponse(prior.get());
}
ImpsResponseData response = buildResponse(api, request, echoIfsc);
ImpsTransaction row = ImpsTransaction.newRow(api, request.getTxnId());
row.setOrgTxnId(request.getOrgTxnId());
row.setRefId(request.getRefId());
row.setUtr(request.getUtr());
row.setAmount(request.getAmount());
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
if (payeeAcc != null) {
row.setPayeeAccountNumber(payeeAcc.getAccountNumber());
row.setPayeeIfsc(payeeAcc.getIfsc());
}
row.setStatus(response.getStatus());
row.setResponseCode(response.getResponseCode());
row.setRequestJson(writeJson(request));
row.setResponseJson(writeJson(response));
repository.save(row);
return response;
}
/** STUB — replace with real account lookup + debit/credit. */
private ImpsResponseData buildResponse(String api, ImpsRequestData request, boolean echoIfsc) {
ImpsResponseData resp = new ImpsResponseData();
resp.setTxnId(request.getTxnId());
resp.setOrgTxnId(request.getOrgTxnId());
resp.setRefId(request.getRefId());
resp.setUtr(request.getUtr());
resp.setAmount(request.getAmount());
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET));
resp.setStatus("SUCCESS");
resp.setResponseCode("00");
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
if (payeeAcc != null) {
resp.setAccountNumber(payeeAcc.getAccountNumber());
if (echoIfsc) {
resp.setIfsc(payeeAcc.getIfsc());
}
}
return resp;
}
private String writeJson(Object o) {
try {
return objectMapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
log.warn("[ImpsCbsService] failed to serialize {} for audit: {}", o.getClass().getSimpleName(), e.toString());
return null;
}
}
private ImpsResponseData readResponse(ImpsTransaction row) {
if (row.getResponseJson() == null) {
return null;
}
try {
return objectMapper.readValue(row.getResponseJson(), ImpsResponseData.class);
} catch (JsonProcessingException e) {
log.warn("[ImpsCbsService] failed to deserialize stored response for txnId={}: {}", row.getTxnId(), e.toString());
return null;
}
}
}
@@ -120,7 +120,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
try { try {
final TellerMapper tm = new TellerMapper(); final TellerMapper tm = new TellerMapper();
final String sql = "select " + tm.schema() + " where t.id = ?"; final String sql = "select " + tm.schema() + " where t.id = ? and t.is_deleted = false";
return this.jdbcTemplate.queryForObject(sql, tm, new Object[] { tellerId }); // NOSONAR return this.jdbcTemplate.queryForObject(sql, tm, new Object[] { tellerId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) { } catch (final EmptyResultDataAccessException e) {
@@ -198,7 +198,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
hierarchySearchString = hierarchy + "%"; hierarchySearchString = hierarchy + "%";
} }
final TellerMapper tm = new TellerMapper(); final TellerMapper tm = new TellerMapper();
final String sql = "select " + tm.schema() + "where o.hierarchy like ? order by o.hierarchy"; final String sql = "select " + tm.schema() + "where o.hierarchy like ? and t.is_deleted = false order by o.hierarchy";
return this.jdbcTemplate.query(sql, tm, new Object[] { hierarchySearchString }); // NOSONAR return this.jdbcTemplate.query(sql, tm, new Object[] { hierarchySearchString }); // NOSONAR
} }
@@ -1,451 +1,394 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.organisation.teller.service; package org.apache.fineract.organisation.teller.service;
import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceException;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.organisation.staff.exception.StaffNotFoundException;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.teller.domain.Cashier;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; import org.apache.fineract.organisation.teller.domain.CashierTransaction;
import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.staff.domain.StaffRepository; import org.apache.fineract.organisation.teller.domain.CashierTxnType;
import org.apache.fineract.organisation.staff.exception.StaffNotFoundException; import org.apache.fineract.organisation.teller.domain.Teller;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.domain.Cashier; import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
import org.apache.fineract.organisation.teller.domain.CashierRepository; import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
import org.apache.fineract.organisation.teller.domain.CashierTransaction; import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository; import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.organisation.teller.domain.CashierTxnType; import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.organisation.teller.domain.Teller; import org.springframework.dao.DataIntegrityViolationException;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper; import org.springframework.orm.jpa.JpaSystemException;
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer; @AllArgsConstructor
import org.apache.fineract.useradministration.domain.AppUser; @Slf4j
import org.springframework.dao.DataIntegrityViolationException; public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformService {
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional; private final PlatformSecurityContext context;
private final TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer;
@AllArgsConstructor private final TellerRepositoryWrapper tellerRepositoryWrapper;
@Slf4j private final OfficeRepositoryWrapper officeRepositoryWrapper;
public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformService { private final StaffRepository staffRepository;
private final CashierRepository cashierRepository;
private final PlatformSecurityContext context; private final CashierTransactionRepository cashierTxnRepository;
private final TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final CashierTransactionDataValidator cashierTransactionDataValidator;
private final TellerRepositoryWrapper tellerRepositoryWrapper;
private final OfficeRepositoryWrapper officeRepositoryWrapper; @Override
private final StaffRepository staffRepository; @Transactional
private final CashierRepository cashierRepository; public CommandProcessingResult createTeller(JsonCommand command) {
private final CashierTransactionRepository cashierTxnRepository; try {
private final JournalEntryRepository glJournalEntryRepository; this.context.authenticatedUser();
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper;
private final CashierTransactionDataValidator cashierTransactionDataValidator; if (this.tellerRepositoryWrapper.existsAny()) {
throw new TellerAlreadyExistsException();
@Override }
@Transactional
public CommandProcessingResult createTeller(JsonCommand command) { final Long officeId = command.longValueOfParameterNamed("officeId");
try {
this.context.authenticatedUser(); this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
final Long officeId = command.longValueOfParameterNamed("officeId"); // final Office parent =
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json()); final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final Teller teller = Teller.fromJson(tellerOffice, command);
// final Office parent =
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId); // pre save to generate id for use in office hierarchy
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); this.tellerRepositoryWrapper.saveAndFlush(teller);
final Teller teller = Teller.fromJson(tellerOffice, command);
return new CommandProcessingResultBuilder() //
// pre save to generate id for use in office hierarchy .withCommandId(command.commandId()) //
this.tellerRepositoryWrapper.saveAndFlush(teller); .withEntityId(teller.getId()) //
.withOfficeId(teller.getOffice().getId()) //
return new CommandProcessingResultBuilder() // .build();
.withCommandId(command.commandId()) // } catch (final JpaSystemException | DataIntegrityViolationException dve) {
.withEntityId(teller.getId()) // handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
.withOfficeId(teller.getOffice().getId()) // return CommandProcessingResult.empty();
.build(); } catch (final PersistenceException dve) {
} catch (final JpaSystemException | DataIntegrityViolationException dve) { Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty(); return CommandProcessingResult.empty();
} catch (final PersistenceException dve) { }
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); }
handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty(); @Override
} @Transactional
} public CommandProcessingResult modifyTeller(Long tellerId, JsonCommand command) {
try {
@Override
@Transactional final Long officeId = command.longValueOfParameterNamed("officeId");
public CommandProcessingResult modifyTeller(Long tellerId, JsonCommand command) { final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
try { final AppUser currentUser = this.context.authenticatedUser();
final Long officeId = command.longValueOfParameterNamed("officeId"); this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final AppUser currentUser = this.context.authenticatedUser(); final Teller teller = validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json()); final Map<String, Object> changes = teller.update(tellerOffice, command);
final Teller teller = validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId); if (!changes.isEmpty()) {
this.tellerRepositoryWrapper.saveAndFlush(teller);
final Map<String, Object> changes = teller.update(tellerOffice, command); }
if (!changes.isEmpty()) { return new CommandProcessingResultBuilder() //
this.tellerRepositoryWrapper.saveAndFlush(teller); .withCommandId(command.commandId()) //
} .withEntityId(teller.getId()) //
.withOfficeId(teller.officeId()) //
return new CommandProcessingResultBuilder() // .with(changes) //
.withCommandId(command.commandId()) // .build();
.withEntityId(teller.getId()) // } catch (final JpaSystemException | DataIntegrityViolationException dve) {
.withOfficeId(teller.officeId()) // handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
.with(changes) // return CommandProcessingResult.empty();
.build(); } catch (final PersistenceException dve) {
} catch (final JpaSystemException | DataIntegrityViolationException dve) { Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty(); return CommandProcessingResult.empty();
} catch (final PersistenceException dve) { }
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); }
handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty(); /*
} * used to restrict modifying operations to office that are either the users office or lower (child) in the office
} * hierarchy
*/
/* private Teller validateUserPriviledgeOnTellerAndRetrieve(final AppUser currentUser, final Long tellerId) {
* used to restrict modifying operations to office that are either the users office or lower (child) in the office
* hierarchy final Long userOfficeId = currentUser.getOffice().getId();
*/ final Office userOffice = this.officeRepositoryWrapper.findOfficeHierarchy(userOfficeId);
private Teller validateUserPriviledgeOnTellerAndRetrieve(final AppUser currentUser, final Long tellerId) { final Teller tellerToReturn = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
final Long tellerOfficeId = tellerToReturn.officeId();
final Long userOfficeId = currentUser.getOffice().getId(); if (userOffice.doesNotHaveAnOfficeInHierarchyWithId(tellerOfficeId)) {
final Office userOffice = this.officeRepositoryWrapper.findOfficeHierarchy(userOfficeId); throw new NoAuthorizationException("User does not have sufficient priviledges to act on the provided office.");
final Teller tellerToReturn = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId); }
final Long tellerOfficeId = tellerToReturn.officeId(); return tellerToReturn;
if (userOffice.doesNotHaveAnOfficeInHierarchyWithId(tellerOfficeId)) { }
throw new NoAuthorizationException("User does not have sufficient priviledges to act on the provided office.");
} @Override
return tellerToReturn; @Transactional
} public CommandProcessingResult deleteTeller(Long tellerId) {
Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
@Override teller.delete();
@Transactional tellerRepositoryWrapper.saveAndFlush(teller);
public CommandProcessingResult deleteTeller(Long tellerId) { return new CommandProcessingResultBuilder() //
// TODO Auto-generated method stub .withEntityId(teller.getId()) //
.build();
Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId); }
Set<Cashier> isTellerIdPresentInCashier = teller.getCashiers();
/*
for (final Cashier tellerIdInCashier : isTellerIdPresentInCashier) { * Guaranteed to throw an exception no matter what the data integrity issue is.
if (tellerIdInCashier.getTeller().getId().toString().equalsIgnoreCase(tellerId.toString())) { */
throw new CashierExistForTellerException(tellerId); private void handleTellerDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) {
} if (realCause.getMessage().contains("m_tellers_name_unq")) {
final String name = command.stringValueOfParameterNamed("name");
} throw new PlatformDataIntegrityException("error.msg.teller.duplicate.name", "Teller with name `" + name + "` already exists",
tellerRepositoryWrapper.delete(teller); "name", name);
return new CommandProcessingResultBuilder() // }
.withEntityId(teller.getId()) //
.build(); log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.teller.unknown.data.integrity.issue",
} "Unknown data integrity issue with resource: " + realCause.getMessage());
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is. @Override
*/ public CommandProcessingResult allocateCashierToTeller(final Long tellerId, JsonCommand command) {
private void handleTellerDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) { try {
if (realCause.getMessage().contains("m_tellers_name_unq")) { this.context.authenticatedUser();
final String name = command.stringValueOfParameterNamed("name"); Long hourStartTime;
throw new PlatformDataIntegrityException("error.msg.teller.duplicate.name", "Teller with name `" + name + "` already exists", Long minStartTime;
"name", name); Long hourEndTime;
} Long minEndTime;
String startTime = " ";
log.error("Error occured.", dve); String endTime = " ";
throw ErrorHandler.getMappable(dve, "error.msg.teller.unknown.data.integrity.issue", final Teller teller = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
"Unknown data integrity issue with resource: " + realCause.getMessage()); final Office tellerOffice = teller.getOffice();
}
final Long staffId = command.longValueOfParameterNamed("staffId");
@Override
public CommandProcessingResult allocateCashierToTeller(final Long tellerId, JsonCommand command) { this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
try {
this.context.authenticatedUser(); final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
Long hourStartTime; final Boolean isFullDay = command.booleanObjectValueOfParameterNamed("isFullDay");
Long minStartTime; if (!isFullDay) {
Long hourEndTime; hourStartTime = command.longValueOfParameterNamed("hourStartTime");
Long minEndTime; minStartTime = command.longValueOfParameterNamed("minStartTime");
String startTime = " ";
String endTime = " "; if (minStartTime == 0) {
final Teller teller = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId); startTime = hourStartTime.toString() + ":" + minStartTime.toString() + "0";
final Office tellerOffice = teller.getOffice(); } else {
startTime = hourStartTime.toString() + ":" + minStartTime.toString();
final Long staffId = command.longValueOfParameterNamed("staffId"); }
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json()); hourEndTime = command.longValueOfParameterNamed("hourEndTime");
minEndTime = command.longValueOfParameterNamed("minEndTime");
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId)); if (minEndTime == 0) {
final Boolean isFullDay = command.booleanObjectValueOfParameterNamed("isFullDay"); endTime = hourEndTime.toString() + ":" + minEndTime.toString() + "0";
if (!isFullDay) { } else {
hourStartTime = command.longValueOfParameterNamed("hourStartTime"); endTime = hourEndTime.toString() + ":" + minEndTime.toString();
minStartTime = command.longValueOfParameterNamed("minStartTime"); }
if (minStartTime == 0) { }
startTime = hourStartTime.toString() + ":" + minStartTime.toString() + "0"; final Cashier cashier = Cashier.fromJson(tellerOffice, teller, staff, startTime, endTime, command);
} else { this.cashierTransactionDataValidator.validateCashierAllowedDateAndTime(cashier, teller);
startTime = hourStartTime.toString() + ":" + minStartTime.toString();
} this.cashierRepository.save(cashier);
hourEndTime = command.longValueOfParameterNamed("hourEndTime"); return new CommandProcessingResultBuilder() //
minEndTime = command.longValueOfParameterNamed("minEndTime"); .withCommandId(command.commandId()) //
if (minEndTime == 0) { .withEntityId(teller.getId()) //
endTime = hourEndTime.toString() + ":" + minEndTime.toString() + "0"; .withSubEntityId(cashier.getId()) //
} else { .build();
endTime = hourEndTime.toString() + ":" + minEndTime.toString(); } catch (final JpaSystemException | DataIntegrityViolationException dve) {
} handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} } catch (final PersistenceException dve) {
final Cashier cashier = Cashier.fromJson(tellerOffice, teller, staff, startTime, endTime, command); Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
this.cashierTransactionDataValidator.validateCashierAllowedDateAndTime(cashier, teller); handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
this.cashierRepository.save(cashier); }
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // @Override
.withEntityId(teller.getId()) // @Transactional
.withSubEntityId(cashier.getId()) // public CommandProcessingResult updateCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
.build(); try {
} catch (final JpaSystemException | DataIntegrityViolationException dve) { final AppUser currentUser = this.context.authenticatedUser();
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty(); this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); final Long staffId = command.longValueOfParameterNamed("staffId");
handleTellerDataIntegrityIssues(command, throwable, dve); final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
return CommandProcessingResult.empty();
} final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
}
cashier.setStaff(staff);
@Override
@Transactional // TODO - check if staff office and teller office match
public CommandProcessingResult updateCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try { final Map<String, Object> changes = cashier.update(command);
final AppUser currentUser = this.context.authenticatedUser();
if (!changes.isEmpty()) {
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json()); this.cashierRepository.saveAndFlush(cashier);
}
final Long staffId = command.longValueOfParameterNamed("staffId");
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId)); return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId); .withEntityId(cashier.getTeller().getId()) //
.withSubEntityId(cashier.getId()) //
cashier.setStaff(staff); .with(changes) //
.build();
// TODO - check if staff office and teller office match } catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
final Map<String, Object> changes = cashier.update(command); return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
if (!changes.isEmpty()) { Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
this.cashierRepository.saveAndFlush(cashier); handleTellerDataIntegrityIssues(command, throwable, dve);
} return CommandProcessingResult.empty();
}
return new CommandProcessingResultBuilder() // }
.withCommandId(command.commandId()) //
.withEntityId(cashier.getTeller().getId()) // private Cashier validateUserPriviledgeOnCashierAndRetrieve(final AppUser currentUser, final Long tellerId, final Long cashierId) {
.withSubEntityId(cashier.getId()) //
.with(changes) // validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) { return this.cashierRepository.findById(cashierId).orElse(null);
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); }
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) { @Override
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); @Transactional
handleTellerDataIntegrityIssues(command, throwable, dve); public CommandProcessingResult deleteCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
return CommandProcessingResult.empty(); try {
} final AppUser currentUser = this.context.authenticatedUser();
} final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
this.cashierRepository.delete(cashier);
private Cashier validateUserPriviledgeOnCashierAndRetrieve(final AppUser currentUser, final Long tellerId, final Long cashierId) {
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId); handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
return this.cashierRepository.findById(cashierId).orElse(null); } catch (final PersistenceException dve) {
} Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleTellerDataIntegrityIssues(command, throwable, dve);
@Override return CommandProcessingResult.empty();
@Transactional }
public CommandProcessingResult deleteCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try { return new CommandProcessingResultBuilder() //
final AppUser currentUser = this.context.authenticatedUser(); .withEntityId(cashierId) //
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId); .build();
this.cashierRepository.delete(cashier); }
} catch (final JpaSystemException | DataIntegrityViolationException dve) { /*
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); * @Override public CommandProcessingResult inwardCashToCashier (final Long cashierId, final CashierTransaction
return CommandProcessingResult.empty(); * cashierTxn) { CashierTxnType txnType = CashierTxnType.INWARD_CASH_TXN; // pre save to generate id for use in
} catch (final PersistenceException dve) { * office hierarchy this.cashierTxnRepository.save(cashierTxn); }
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); */
handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty(); @Override
} public CommandProcessingResult allocateCashToCashier(final Long cashierId, JsonCommand command) {
return doTransactionForCashier(cashierId, CashierTxnType.ALLOCATE, command); // For
return new CommandProcessingResultBuilder() // // fund
.withEntityId(cashierId) // // allocation
.build(); // to
} // cashier
}
/*
* @Override public CommandProcessingResult inwardCashToCashier (final Long cashierId, final CashierTransaction @Override
* cashierTxn) { CashierTxnType txnType = CashierTxnType.INWARD_CASH_TXN; // pre save to generate id for use in public CommandProcessingResult settleCashFromCashier(final Long cashierId, JsonCommand command) {
* office hierarchy this.cashierTxnRepository.save(cashierTxn); }
*/ this.cashierTransactionDataValidator.validateSettleCashAndCashOutTransactions(cashierId, command);
@Override return doTransactionForCashier(cashierId, CashierTxnType.SETTLE, command); // For
public CommandProcessingResult allocateCashToCashier(final Long cashierId, JsonCommand command) { // fund
return doTransactionForCashier(cashierId, CashierTxnType.ALLOCATE, command); // For // settlement
// fund // from
// allocation // cashier
// to }
// cashier
} private CommandProcessingResult doTransactionForCashier(final Long cashierId, final CashierTxnType txnType, JsonCommand command) {
try {
@Override final AppUser currentUser = this.context.authenticatedUser();
public CommandProcessingResult settleCashFromCashier(final Long cashierId, JsonCommand command) {
final Cashier cashier = this.cashierRepository.findById(cashierId).orElseThrow(() -> new CashierNotFoundException(cashierId));
this.cashierTransactionDataValidator.validateSettleCashAndCashOutTransactions(cashierId, command);
this.fromApiJsonDeserializer.validateForCashTxnForCashier(command.json());
return doTransactionForCashier(cashierId, CashierTxnType.SETTLE, command); // For
// fund // TODO: can we please remove this whole block?!? this is 20 lines of dead code!!!
// settlement final String entityType = command.stringValueOfParameterNamed("entityType");
// from if (entityType != null) {
// cashier if (entityType.equals("loan account")) {
} // TODO : Check if loan account exists
// LoanAccount loan = null;
private CommandProcessingResult doTransactionForCashier(final Long cashierId, final CashierTxnType txnType, JsonCommand command) { // if (loan == null) { throw new
try { // LoanAccountFoundException(entityId); }
final AppUser currentUser = this.context.authenticatedUser(); } else if (entityType.equals("savings account")) {
// TODO : Check if loan account exists
final Cashier cashier = this.cashierRepository.findById(cashierId).orElseThrow(() -> new CashierNotFoundException(cashierId)); // SavingsAccount savingsaccount = null;
// if (savingsaccount == null) { throw new
this.fromApiJsonDeserializer.validateForCashTxnForCashier(command.json()); // SavingsAccountNotFoundException(entityId); }
// TODO: can we please remove this whole block?!? this is 20 lines of dead code!!! }
final String entityType = command.stringValueOfParameterNamed("entityType"); if (entityType.equals("client")) {
if (entityType != null) { // TODO: Check if client exists
if (entityType.equals("loan account")) { // Client client = null;
// TODO : Check if loan account exists // if (client == null) { throw new
// LoanAccount loan = null; // ClientNotFoundException(entityId); }
// if (loan == null) { throw new } else {
// LoanAccountFoundException(entityId); } // TODO : Invalid type handling
} else if (entityType.equals("savings account")) { }
// TODO : Check if loan account exists }
// SavingsAccount savingsaccount = null;
// if (savingsaccount == null) { throw new final CashierTransaction cashierTxn = CashierTransaction.fromJson(cashier, command);
// SavingsAccountNotFoundException(entityId); } cashierTxn.setTxnType(txnType.getId());
} this.cashierTxnRepository.save(cashierTxn);
if (entityType.equals("client")) {
// TODO: Check if client exists // GL entries skipped for vault-to-cashier (ALLOCATE) and cashier-to-vault (SETTLE)
// Client client = null; // transfers. Cash is internal bank physical movement — denomination tracking is
// if (client == null) { throw new // handled operationally via m_cash_transfer / m_cash_settlement tables.
// ClientNotFoundException(entityId); } // GL impact happens only on customer deposit/withdrawal transactions.
} else {
// TODO : Invalid type handling return new CommandProcessingResultBuilder() //
} .withCommandId(command.commandId()) //
} .withEntityId(cashier.getId()) //
.withSubEntityId(cashierTxn.getId()) //
final CashierTransaction cashierTxn = CashierTransaction.fromJson(cashier, command); .build();
cashierTxn.setTxnType(txnType.getId()); } catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
this.cashierTxnRepository.save(cashierTxn); return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
// Pass the journal entries Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
FinancialActivityAccount mainVaultFinancialActivityAccount = this.financialActivityAccountRepositoryWrapper handleTellerDataIntegrityIssues(command, throwable, dve);
.findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.CASH_AT_MAINVAULT.getValue()); return CommandProcessingResult.empty();
FinancialActivityAccount tellerCashFinancialActivityAccount = this.financialActivityAccountRepositoryWrapper }
.findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.CASH_AT_TELLER.getValue()); }
GLAccount creditAccount = null;
GLAccount debitAccount = null; }
if (txnType.equals(CashierTxnType.ALLOCATE)) {
debitAccount = tellerCashFinancialActivityAccount.getGlAccount();
creditAccount = mainVaultFinancialActivityAccount.getGlAccount();
} else if (txnType.equals(CashierTxnType.SETTLE)) {
debitAccount = mainVaultFinancialActivityAccount.getGlAccount();
creditAccount = tellerCashFinancialActivityAccount.getGlAccount();
}
final Office cashierOffice = cashier.getTeller().getOffice();
final Long time = System.currentTimeMillis();
final String uniqueVal = String.valueOf(time) + currentUser.getId() + cashierOffice.getId();
final String transactionId = Long.toHexString(Long.parseLong(uniqueVal));
final JournalEntry debitJournalEntry = JournalEntry.createNew(cashierOffice, null, // payment
// detail
debitAccount, cashierTxn.getCurrencyCode(),
transactionId, false, // manual entry
cashierTxn.getTxnDate(), JournalEntryType.DEBIT, cashierTxn.getTxnAmount(), cashierTxn.getTxnNote(), // Description
null, null, null, // entity Type, entityId, reference number
null, null, null, null); // Loan
// and
// Savings
// Txn
final JournalEntry creditJournalEntry = JournalEntry.createNew(cashierOffice, null, // payment
// detail
creditAccount, cashierTxn.getCurrencyCode(),
transactionId, false, // manual entry
cashierTxn.getTxnDate(), JournalEntryType.CREDIT, cashierTxn.getTxnAmount(), cashierTxn.getTxnNote(), // Description
null, null, null, // entity Type, entityId, reference number
null, null, null, null); // Loan
// and
// Savings
// Txn
this.glJournalEntryRepository.saveAndFlush(debitJournalEntry);
this.glJournalEntryRepository.saveAndFlush(creditJournalEntry);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(cashier.getId()) //
.withSubEntityId(cashierTxn.getId()) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleTellerDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleTellerDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
}
@@ -1,71 +1,67 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.organisation.teller.starter; package org.apache.fineract.organisation.teller.starter;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; import org.apache.fineract.organisation.staff.service.StaffReadPlatformService;
import org.apache.fineract.organisation.staff.domain.StaffRepository; import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.teller.domain.CashierRepository; import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository; import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper; import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformService;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer; import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformServiceImpl;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformService; import org.apache.fineract.organisation.teller.service.TellerWritePlatformService;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformServiceImpl; import org.apache.fineract.organisation.teller.service.TellerWritePlatformServiceJpaImpl;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformServiceJpaImpl; import org.springframework.context.annotation.Bean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate; @Configuration
public class OrganisationTellerConfiguration {
@Configuration
public class OrganisationTellerConfiguration { @Bean
@ConditionalOnMissingBean(TellerManagementReadPlatformService.class)
@Bean public TellerManagementReadPlatformService tellerManagementReadPlatformService(JdbcTemplate jdbcTemplate,
@ConditionalOnMissingBean(TellerManagementReadPlatformService.class) PlatformSecurityContext context, OfficeReadPlatformService officeReadPlatformService,
public TellerManagementReadPlatformService tellerManagementReadPlatformService(JdbcTemplate jdbcTemplate, StaffReadPlatformService staffReadPlatformService, CurrencyReadPlatformService currencyReadPlatformService,
PlatformSecurityContext context, OfficeReadPlatformService officeReadPlatformService, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper, SqlValidator sqlValidator) {
StaffReadPlatformService staffReadPlatformService, CurrencyReadPlatformService currencyReadPlatformService, return new TellerManagementReadPlatformServiceImpl(jdbcTemplate, context, officeReadPlatformService, staffReadPlatformService,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper, SqlValidator sqlValidator) { currencyReadPlatformService, sqlGenerator, paginationHelper, sqlValidator);
return new TellerManagementReadPlatformServiceImpl(jdbcTemplate, context, officeReadPlatformService, staffReadPlatformService, }
currencyReadPlatformService, sqlGenerator, paginationHelper, sqlValidator);
} @Bean
@ConditionalOnMissingBean(TellerWritePlatformService.class)
@Bean public TellerWritePlatformService tellerWritePlatformService(PlatformSecurityContext context,
@ConditionalOnMissingBean(TellerWritePlatformService.class) TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer, TellerRepositoryWrapper tellerRepositoryWrapper,
public TellerWritePlatformService tellerWritePlatformService(PlatformSecurityContext context, OfficeRepositoryWrapper officeRepositoryWrapper, StaffRepository staffRepository, CashierRepository cashierRepository,
TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer, TellerRepositoryWrapper tellerRepositoryWrapper, CashierTransactionRepository cashierTxnRepository,
OfficeRepositoryWrapper officeRepositoryWrapper, StaffRepository staffRepository, CashierRepository cashierRepository, CashierTransactionDataValidator cashierTransactionDataValidator) {
CashierTransactionRepository cashierTxnRepository, JournalEntryRepository glJournalEntryRepository, return new TellerWritePlatformServiceJpaImpl(context, fromApiJsonDeserializer, tellerRepositoryWrapper, officeRepositoryWrapper,
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper, staffRepository, cashierRepository, cashierTxnRepository, cashierTransactionDataValidator);
CashierTransactionDataValidator cashierTransactionDataValidator) { }
return new TellerWritePlatformServiceJpaImpl(context, fromApiJsonDeserializer, tellerRepositoryWrapper, officeRepositoryWrapper, }
staffRepository, cashierRepository, cashierTxnRepository, glJournalEntryRepository,
financialActivityAccountRepositoryWrapper, cashierTransactionDataValidator);
}
}
@@ -0,0 +1,80 @@
/**
* 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.account.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import com.google.gson.JsonElement;
import org.springframework.stereotype.Component;
@Path("/v1/accounts/{accountId}/terminate")
@Component
@Tag(name = "Account Terminate", description = "Unified terminate endpoint for Savings, Current, FD and RD accounts.")
@RequiredArgsConstructor
public class AccountTerminateApiResource {
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
private final FromJsonHelper fromJsonHelper;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Terminate an account", description = "Terminates a Savings, Current, Fixed Deposit or Recurring Deposit account. Pass accountType in the request body: SAVINGS | CURRENT | FD | RD.")
@ApiResponse(responseCode = "200", description = "OK")
public String terminate(@PathParam("accountId") final Long accountId,
final String apiRequestBodyAsJson) {
final String jsonBody = StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson;
final JsonElement root = this.fromJsonHelper.parse(jsonBody);
final String accountType = this.fromJsonHelper.extractStringNamed("accountType", root);
String entityName = "SAVINGSACCOUNT";
if ("CURRENT".equalsIgnoreCase(accountType)) {
entityName = "CURRENTACCOUNT";
} else if ("FD".equalsIgnoreCase(accountType)) {
entityName = "FIXEDDEPOSITACCOUNT";
} else if ("RD".equalsIgnoreCase(accountType)) {
entityName = "RECURRINGDEPOSITACCOUNT";
}
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.withJson(jsonBody)
.terminateAccount(accountId)
.withEntityName(entityName)
.build();
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
}
@@ -1,220 +1,220 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.api; package org.apache.fineract.portfolio.account.api;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry; import org.apache.fineract.batch.command.CommandHandlerRegistry;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData; import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest; import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam; import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest; import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions") @Path("/v1/standinginstructions")
@Component @Component
@Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n" @Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n"
+ "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.") + "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor @RequiredArgsConstructor
public class StandingInstructionApiResource { public class StandingInstructionApiResource {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer; private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator; private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>( private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE, Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(), (id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE, CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build())); (id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET @GET
@Path("template") @Path("template")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. " @Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. "
+ "It can be useful when building maintenance user interface screens for client applications. " + "It can be useful when building maintenance user interface screens for client applications. "
+ "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n" + "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n"
+ "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n" + "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n"
+ "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1") + "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) { public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(), return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(), instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(), instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType()); instructionParam.getToAccountType(), instructionParam.getTransferType());
} }
@POST @POST
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another") @Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class))) @RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) { public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction() final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build(); .withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest); return commandsSourceWritePlatformService.logCommandSource(commandRequest);
} }
@PUT @PUT
@Path("{standingInstructionId}") @Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" @Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n"
+ "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n" + "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n"
+ "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n" + "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n"
+ "PUT https://DomainName/api/v1/standinginstructions/1?command=delete") + "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class))) @RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update( public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest, @Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) { @QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest); final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId, final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam)); serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest); return commandsSourceWritePlatformService.logCommandSource(commandRequest);
} }
@GET @GET
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions") @Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll( public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType, @QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName, @QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, @QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount, @QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) { @QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId); sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build(); .orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null; final LocalDate startDateRange = null;
final LocalDate endDateRange = null; final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId, StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange); fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO); return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
} }
@GET @GET
@Path("{standingInstructionId}") @Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1") @Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class)))
public StandingInstructionData retrieveOne( public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, @Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId); StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build(); .orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) { if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) { if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template")); associationParameters.addAll(Arrays.asList("transactions", "template"));
} }
if (associationParameters.contains("transactions")) { if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters); .retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers); standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
} }
if (associationParameters.contains("template")) { if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate( final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(), standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(), standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(), standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(), standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.getTransferType().getValue()); standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData); standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
} }
} }
return standingInstructionData; return standingInstructionData;
} }
} }
@@ -1,79 +1,79 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class AccountTransfersDetailDataValidator { public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@Autowired @Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) { public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
} }
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) { public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element); final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull() baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2)); .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element); final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1), baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2)); Integer.valueOf(2), Integer.valueOf(3));
} }
} }
@@ -1,329 +1,359 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData; import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData; import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/** /**
* Immutable data object representing a savings account. * Immutable data object representing a savings account.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class StandingInstructionData { public final class StandingInstructionData {
@Getter @Getter
private final Long id; private final Long id;
@Getter @Getter
private final Long accountDetailId; private final Long accountDetailId;
@Getter @Getter
private final String name; private final String name;
private final OfficeData fromOffice; @Getter
@Getter private final OfficeData fromOffice;
private final ClientData fromClient; @Getter
private final EnumOptionData fromAccountType; private final ClientData fromClient;
@Getter @Getter
private final PortfolioAccountData fromAccount; private final EnumOptionData fromAccountType;
private final OfficeData toOffice; @Getter
@Getter private final PortfolioAccountData fromAccount;
private final ClientData toClient; @Getter
private final EnumOptionData toAccountType; private final OfficeData toOffice;
@Getter @Getter
private final PortfolioAccountData toAccount; private final ClientData toClient;
private final EnumOptionData transferType; @Getter
private final EnumOptionData priority; private final EnumOptionData toAccountType;
private final EnumOptionData instructionType; @Getter
@Getter private final PortfolioAccountData toAccount;
private final EnumOptionData status; @Getter
@Getter private final EnumOptionData transferType;
private final BigDecimal amount; @Getter
@Getter private final EnumOptionData priority;
private final LocalDate validFrom; @Getter
private final LocalDate validTill; private final EnumOptionData instructionType;
private final EnumOptionData recurrenceType; @Getter
private final EnumOptionData recurrenceFrequency; private final EnumOptionData status;
@Getter @Getter
private final Integer recurrenceInterval; private final BigDecimal amount;
private final MonthDay recurrenceOnMonthDay; @Getter
private final Page<AccountTransferData> transactions; private final LocalDate validFrom;
@Getter
private final Collection<OfficeData> fromOfficeOptions; private final LocalDate validTill;
private final Collection<ClientData> fromClientOptions; @Getter
private final Collection<EnumOptionData> fromAccountTypeOptions; private final EnumOptionData recurrenceType;
private final Collection<PortfolioAccountData> fromAccountOptions; @Getter
private final Collection<OfficeData> toOfficeOptions; private final EnumOptionData recurrenceFrequency;
private final Collection<ClientData> toClientOptions; @Getter
private final Collection<EnumOptionData> toAccountTypeOptions; private final Integer recurrenceInterval;
private final Collection<PortfolioAccountData> toAccountOptions; @Getter
private final Collection<EnumOptionData> transferTypeOptions; private final MonthDay recurrenceOnMonthDay;
private final Collection<EnumOptionData> statusOptions; @Getter
private final Collection<EnumOptionData> instructionTypeOptions; private final Page<AccountTransferData> transactions;
private final Collection<EnumOptionData> priorityOptions;
private final Collection<EnumOptionData> recurrenceTypeOptions; @Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions; private final Collection<OfficeData> fromOfficeOptions;
@Getter
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions, private final Collection<ClientData> fromClientOptions;
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions, @Getter
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions, private final Collection<EnumOptionData> fromAccountTypeOptions;
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions, @Getter
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions, private final Collection<PortfolioAccountData> fromAccountOptions;
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions, @Getter
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions, private final Collection<OfficeData> toOfficeOptions;
final Collection<EnumOptionData> recurrenceFrequencyOptions) { @Getter
private final Collection<ClientData> toClientOptions;
final Long id = null; @Getter
final Long accountDetailId = null; private final Collection<EnumOptionData> toAccountTypeOptions;
final String name = null; @Getter
final OfficeData fromOffice = null; private final Collection<PortfolioAccountData> toAccountOptions;
final OfficeData toOffice = null; @Getter
final ClientData fromClient = null; private final Collection<EnumOptionData> transferTypeOptions;
final ClientData toClient = null; @Getter
final EnumOptionData fromAccountType = null; private final Collection<EnumOptionData> statusOptions;
final PortfolioAccountData fromAccount = null; @Getter
final EnumOptionData toAccountType = null; private final Collection<EnumOptionData> instructionTypeOptions;
final PortfolioAccountData toAccount = null; @Getter
final EnumOptionData transferType = null; private final Collection<EnumOptionData> priorityOptions;
final EnumOptionData priority = null; @Getter
final EnumOptionData instructionType = null; private final Collection<EnumOptionData> recurrenceTypeOptions;
final EnumOptionData status = null; @Getter
final BigDecimal amount = null; private final Collection<EnumOptionData> recurrenceFrequencyOptions;
final LocalDate validFrom = null;
final LocalDate validTill = null; public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final EnumOptionData recurrenceType = null; final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final EnumOptionData recurrenceFrequency = null; final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Integer recurrenceInterval = null; final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final MonthDay recurrenceOnMonthDay = null; final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Page<AccountTransferData> transactions = null; final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, final Long id = null;
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, final Long accountDetailId = null;
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final String name = null;
recurrenceFrequencyOptions); final OfficeData fromOffice = null;
} final OfficeData toOffice = null;
final ClientData fromClient = null;
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name, final ClientData toClient = null;
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient, final EnumOptionData fromAccountType = null;
final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType, final PortfolioAccountData fromAccount = null;
final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData toAccountType = null;
final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, final PortfolioAccountData toAccount = null;
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final EnumOptionData transferType = null;
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) { final EnumOptionData priority = null;
final Page<AccountTransferData> transactions = null; final EnumOptionData instructionType = null;
final Collection<OfficeData> fromOfficeOptions = null; final EnumOptionData status = null;
final Collection<ClientData> fromClientOptions = null; final BigDecimal amount = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null; final LocalDate validFrom = null;
final Collection<PortfolioAccountData> fromAccountOptions = null; final LocalDate validTill = null;
final Collection<OfficeData> toOfficeOptions = null; final EnumOptionData recurrenceType = null;
final Collection<ClientData> toClientOptions = null; final EnumOptionData recurrenceFrequency = null;
final Collection<EnumOptionData> toAccountTypeOptions = null; final Integer recurrenceInterval = null;
final Collection<PortfolioAccountData> toAccountOptions = null; final MonthDay recurrenceOnMonthDay = null;
final Collection<EnumOptionData> transferTypeOptions = null; final Page<AccountTransferData> transactions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null; return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
final Collection<EnumOptionData> priorityOptions = null; toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
final Collection<EnumOptionData> recurrenceTypeOptions = null; recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions = null; fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, recurrenceFrequencyOptions);
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, }
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
recurrenceFrequencyOptions); final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType,
} final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority,
final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom,
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) { final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, final Page<AccountTransferData> transactions = null;
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, final Collection<OfficeData> fromOfficeOptions = null;
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, final Collection<ClientData> fromClientOptions = null;
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, final Collection<EnumOptionData> fromAccountTypeOptions = null;
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, final Collection<PortfolioAccountData> fromAccountOptions = null;
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions, final Collection<OfficeData> toOfficeOptions = null;
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions, final Collection<ClientData> toClientOptions = null;
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions, final Collection<EnumOptionData> toAccountTypeOptions = null;
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions, final Collection<PortfolioAccountData> toAccountOptions = null;
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions); final Collection<EnumOptionData> transferTypeOptions = null;
} final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice, final Collection<EnumOptionData> priorityOptions = null;
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final Collection<EnumOptionData> recurrenceTypeOptions = null;
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount, final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill, return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval, toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions, toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions, recurrenceFrequencyOptions);
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions, }
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions, public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) { return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
this.id = id; instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
this.accountDetailId = accountDetailId; instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
this.name = name; instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
this.fromOffice = fromOffice; instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
this.fromClient = fromClient; instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
this.fromAccountType = fromAccountType; instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
this.fromAccount = fromAccount; templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
this.toOffice = toOffice; templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
this.toClient = toClient; templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
this.toAccountType = toAccountType; templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
this.toAccount = toAccount; }
this.transferType = transferType;
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
this.priority = priority; final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
this.instructionType = instructionType; final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
this.status = status; final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
this.amount = amount; final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
this.validFrom = validFrom; final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
this.validTill = validTill; final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
this.recurrenceType = recurrenceType; final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
this.recurrenceFrequency = recurrenceFrequency; final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
this.recurrenceInterval = recurrenceInterval; final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
this.recurrenceOnMonthDay = recurrenceOnMonthDay; final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
this.fromOfficeOptions = fromOfficeOptions; final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
this.fromClientOptions = fromClientOptions; final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.fromAccountTypeOptions = fromAccountTypeOptions; this.id = id;
this.fromAccountOptions = fromAccountOptions; this.accountDetailId = accountDetailId;
this.toOfficeOptions = toOfficeOptions; this.name = name;
this.toClientOptions = toClientOptions; this.fromOffice = fromOffice;
this.toAccountTypeOptions = toAccountTypeOptions; this.fromClient = fromClient;
this.toAccountOptions = toAccountOptions; this.fromAccountType = fromAccountType;
this.transferTypeOptions = transferTypeOptions; this.fromAccount = fromAccount;
this.statusOptions = statusOptions; this.toOffice = toOffice;
this.instructionTypeOptions = instructionTypeOptions; this.toClient = toClient;
this.priorityOptions = priorityOptions; this.toAccountType = toAccountType;
this.recurrenceTypeOptions = recurrenceTypeOptions; this.toAccount = toAccount;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions; this.transferType = transferType;
this.transactions = transactions;
} this.priority = priority;
this.instructionType = instructionType;
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType, this.status = status;
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient, this.amount = amount;
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions, this.validFrom = validFrom;
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions, this.validTill = validTill;
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions, this.recurrenceType = recurrenceType;
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions, this.recurrenceFrequency = recurrenceFrequency;
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions, this.recurrenceInterval = recurrenceInterval;
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions, this.recurrenceOnMonthDay = recurrenceOnMonthDay;
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) { this.fromOfficeOptions = fromOfficeOptions;
final Long id = null; this.fromClientOptions = fromClientOptions;
final Long accountDetailId = null; this.fromAccountTypeOptions = fromAccountTypeOptions;
final String name = null; this.fromAccountOptions = fromAccountOptions;
final EnumOptionData transferType = null; this.toOfficeOptions = toOfficeOptions;
final EnumOptionData priority = null; this.toClientOptions = toClientOptions;
final EnumOptionData instructionType = null; this.toAccountTypeOptions = toAccountTypeOptions;
final EnumOptionData status = null; this.toAccountOptions = toAccountOptions;
final BigDecimal amount = null; this.transferTypeOptions = transferTypeOptions;
final LocalDate validFrom = null; this.statusOptions = statusOptions;
final LocalDate validTill = null; this.instructionTypeOptions = instructionTypeOptions;
final EnumOptionData recurrenceType = null; this.priorityOptions = priorityOptions;
final EnumOptionData recurrenceFrequency = null; this.recurrenceTypeOptions = recurrenceTypeOptions;
final Integer recurrenceInterval = null; this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
final MonthDay recurrenceOnMonthDay = null; this.transactions = transactions;
final Page<AccountTransferData> transactions = null; }
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
recurrenceFrequencyOptions); final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
} final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
public static StandingInstructionData withTransferData(StandingInstructionData instructionData, final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Page<AccountTransferData> transactions) { final Collection<EnumOptionData> recurrenceFrequencyOptions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, final Long id = null;
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, final Long accountDetailId = null;
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, final String name = null;
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, final EnumOptionData transferType = null;
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, final EnumOptionData priority = null;
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions, final EnumOptionData instructionType = null;
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions, final EnumOptionData status = null;
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions, final BigDecimal amount = null;
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions, final LocalDate validFrom = null;
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions, final LocalDate validTill = null;
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions); final EnumOptionData recurrenceType = null;
} final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
public StandingInstructionType getInstructionType() { final MonthDay recurrenceOnMonthDay = null;
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null); final Page<AccountTransferData> transactions = null;
} return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
public AccountTransferRecurrenceType getRecurrenceType() { recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null); fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
} toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
public PeriodFrequencyType getRecurrenceFrequency() { }
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
} public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
public PortfolioAccountType getFromAccountType() { return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
} instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
public PortfolioAccountType getToAccountType() { instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
} instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
public AccountTransferType getTransferType() { instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null); instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
} instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
public Integer getRecurrenceOnDay() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0); /**
} * Returns the domain enum for instruction type — used internally by business logic.
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
public Integer getRecurrenceOnMonth() { * return EnumOptionData so Jackson serializes it as an object, not a plain string).
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0); */
} public StandingInstructionType instructionTypeEnum() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
public Integer toTransferType() { }
Integer transferType = null;
AccountTransferType accountTransferType = getTransferType(); public AccountTransferRecurrenceType recurrenceTypeEnum() {
if (accountTransferType.isChargePayment()) { return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue(); }
} else if (accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue(); public PeriodFrequencyType recurrenceFrequencyEnum() {
} return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
return transferType; }
}
public PortfolioAccountType fromAccountTypeEnum() {
} return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType toAccountTypeEnum() {
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferType transferTypeEnum() {
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
}
public Integer getRecurrenceOnDay() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0);
}
public Integer getRecurrenceOnMonth() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0);
}
public Integer toTransferType() {
Integer transferType = null;
AccountTransferType accountTransferType = transferTypeEnum();
if (accountTransferType != null && accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType != null && accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue();
}
return transferType;
}
}
@@ -1,294 +1,296 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.account.AccountDetailConstants; import org.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class StandingInstructionDataValidator { public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName, AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName, AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName, AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName, AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName, AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName)); StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName, AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName)); StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired @Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper, public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) { final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator; this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
} }
public void validateForCreate(final JsonCommand command) { public void validateForCreate(final JsonCommand command) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator); this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault()); Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2); baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element); element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element); element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill) baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom); .validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault()); Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1, baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4); 4);
final Integer standingInstructionType = this.fromApiJsonHelper final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2); .notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault()); element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2); .inMinMaxRange(1, 2);
boolean isPeriodic = false; boolean isPeriodic = false;
if (recurrenceType != null) { if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
} }
final Integer recurrenceFrequency = this.fromApiJsonHelper final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3); .inMinMaxRange(0, 3);
if (recurrenceFrequency != null) { if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) { if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull(); .notNull();
} }
} }
final Integer recurrenceInterval = this.fromApiJsonHelper final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) { if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull(); .notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull(); .notNull();
} }
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero(); .integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) { if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) || PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
.notNull().inMinMaxRange(1, 1); baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() .notNull().inMinMaxRange(1, 1);
.inMinMaxRange(1, 1); baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 1);
}
if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) { }
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) {
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType); String errorCode = null;
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
if (fromAccountType != null && toAccountType != null) { final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType); if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType); PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
if (accountTransferType.isAccountTransfer() PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) { if (accountTransferType.isAccountTransfer()
errorCode = "not.account.transfer"; && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
} else if (accountTransferType.isLoanRepayment() errorCode = "not.account.transfer";
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) { } else if (accountTransferType.isLoanRepayment()
errorCode = "not.loan.repayment"; && (fromPortfolioAccountType.isLoanAccount()
} || (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
if (errorCode != null) { errorCode = "not.loan.repayment";
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); }
} if (errorCode != null) {
} baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors); }
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
public void validateForUpdate(final JsonCommand command) { }
final String json = command.json();
public void validateForUpdate(final JsonCommand command) {
if (StringUtils.isBlank(json)) { final String json = command.json();
throw new InvalidJsonException();
} if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); }
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS);
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS);
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
final JsonElement element = command.parsedJson(); .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, final JsonElement element = command.parsedJson();
element); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
} element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) { }
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
} element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) { }
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); final BigDecimal transferAmount = this.fromApiJsonHelper
} .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) { }
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
2); Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1,
2);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) { }
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull() final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
.inMinMaxRange(1, 4); Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull()
.inMinMaxRange(1, 4);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) { }
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) final Integer standingInstructionType = this.fromApiJsonHelper
.notNull().inMinMaxRange(1, 2); .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) { }
final Integer recurrenceType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() final Integer recurrenceType = this.fromApiJsonHelper
.inMinMaxRange(1, 2); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) { }
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) final Integer recurrenceFrequency = this.fromApiJsonHelper
.inMinMaxRange(0, 3); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) { }
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) final Integer recurrenceInterval = this.fromApiJsonHelper
.integerGreaterThanZero(); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) { }
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) {
} final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors); }
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) { }
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
} if (!dataValidationErrors.isEmpty()) {
} throw new PlatformApiDataValidationException(dataValidationErrors);
} }
}
}
@@ -1,214 +1,314 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import java.util.Locale; import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.springframework.beans.factory.annotation.Autowired; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.stereotype.Service; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
@Service import org.springframework.stereotype.Service;
public class AccountTransferDetailAssembler {
@Service
private final ClientRepositoryWrapper clientRepository; public class AccountTransferDetailAssembler {
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler; private final ClientRepositoryWrapper clientRepository;
private final FromJsonHelper fromApiJsonHelper; private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final LoanAssembler loanAccountAssembler; private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
@Autowired private final LoanAssembler loanAccountAssembler;
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository, private final CurrentAccountRepository currentAccountRepository;
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) { @Autowired
this.clientRepository = clientRepository; public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
this.officeRepositoryWrapper = officeRepositoryWrapper; final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
this.savingsAccountAssembler = savingsAccountAssembler; final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
this.fromApiJsonHelper = fromApiJsonHelper; final CurrentAccountRepository currentAccountRepository) {
this.loanAccountAssembler = loanAccountAssembler; this.clientRepository = clientRepository;
} this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); this.currentAccountRepository = currentAccountRepository;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); }
final boolean backdatedTxnsAllowedTill = false; public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
final boolean backdatedTxnsAllowedTill = false;
} final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false; }
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount); final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
} final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); }
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount); final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
} final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
final SavingsAccount toSavingsAccount) {
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final SavingsAccount toSavingsAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
toSavingsAccount, transfertype); final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
} final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
final Loan toLoanAccount) { toSavingsAccount, transfertype);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final Loan toLoanAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
transfertype); final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
} final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount, return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
final SavingsAccount toSavingsAccount) { transfertype);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final SavingsAccount toSavingsAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype); final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
} final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) { return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
final Office fromOffice = fromSavingsAccount.office(); transfertype);
final Client fromClient = fromSavingsAccount.getClient(); }
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client(); public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, final Office fromOffice = fromSavingsAccount.office();
transferType); final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
} final Client toClient = toLoanAccount.client();
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount, return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
final SavingsAccount toSavingsAccount, Integer transferType) { transferType);
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient(); }
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient(); public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, final Office fromOffice = fromSavingsAccount.office();
toSavingsAccount, transferType); final Client fromClient = fromSavingsAccount.getClient();
} final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) { return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
final Office fromOffice = fromLoanAccount.getOffice(); toSavingsAccount, transferType);
final Client fromClient = fromLoanAccount.client(); }
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient(); public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, final Office fromOffice = fromLoanAccount.getOffice();
transferType); final Client fromClient = fromLoanAccount.client();
} final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice(); return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
final Client fromClient = fromLoanAccount.client(); transferType);
final Office toOffice = toLoanAccount.getOffice(); }
final Client toClient = toLoanAccount.client();
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount, final Office fromOffice = fromLoanAccount.getOffice();
transferType); final Client fromClient = fromLoanAccount.client();
} final Office toOffice = toLoanAccount.getOffice();
} final Client toClient = toLoanAccount.client();
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
transferType);
}
// --- Current account assembly methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToCurrentTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, false);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToSavingsTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToCurrentTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToLoanTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toLoanAccount, transfertype);
}
}
@@ -1,165 +1,216 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details") @Entity
public class AccountTransferDetails extends AbstractPersistableCustom<Long> { @Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false) @ManyToOne
private Office fromOffice; @JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false) @ManyToOne
private Client fromClient; @JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true) @ManyToOne
private SavingsAccount fromSavingsAccount; @JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false) @ManyToOne
private Office toOffice; @JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false) @ManyToOne
private Client toClient; @JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true) @ManyToOne
private SavingsAccount toSavingsAccount; @JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true) @ManyToOne
private Loan toLoanAccount; @JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true) @ManyToOne
private Loan fromLoanAccount; @JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@Column(name = "transfer_type")
private Integer transferType; @ManyToOne
@JoinColumn(name = "from_current_account_id", nullable = true)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER) private CurrentAccount fromCurrentAccount;
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@ManyToOne
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "to_current_account_id", nullable = true)
private AccountTransferStandingInstruction accountTransferStandingInstruction; private CurrentAccount toCurrentAccount;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient, @Column(name = "transfer_type")
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, private Integer transferType;
Integer transferType) {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null, private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
transferType, null);
} @OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount, public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
Integer transferType) { final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount, Integer transferType) {
transferType, null);
} return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount, }
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null, public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
transferType, null); final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
} Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount,
protected AccountTransferDetails() { null, transferType, null);
// }
}
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
final Loan toLoanAccount, final Integer transferType, null, transferType, null);
final AccountTransferStandingInstruction accountTransferStandingInstruction) { }
this.fromOffice = fromOffice;
this.fromClient = fromClient; protected AccountTransferDetails() {
this.fromSavingsAccount = fromSavingsAccount; //
this.fromLoanAccount = fromLoanAccount; }
this.toOffice = toOffice;
this.toClient = toClient; private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
this.toSavingsAccount = toSavingsAccount; final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
this.toLoanAccount = toLoanAccount; final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
this.transferType = transferType; final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
this.accountTransferStandingInstruction = accountTransferStandingInstruction; final Integer transferType,
} final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
public SavingsAccount toSavingsAccount() { this.fromClient = fromClient;
return this.toSavingsAccount; this.fromSavingsAccount = fromSavingsAccount;
} this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
public SavingsAccount fromSavingsAccount() { this.toOffice = toOffice;
return this.fromSavingsAccount; this.toClient = toClient;
} this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) { this.toCurrentAccount = toCurrentAccount;
this.accountTransferTransactions.add(accountTransferTransaction); this.transferType = transferType;
} this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction; public SavingsAccount toSavingsAccount() {
} return this.toSavingsAccount;
}
public Loan toLoanAccount() {
return this.toLoanAccount; public SavingsAccount fromSavingsAccount() {
} return this.fromSavingsAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount; public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
} this.accountTransferTransactions.add(accountTransferTransaction);
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction; public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
} this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public AccountTransferType transferType() {
return AccountTransferType.fromInt(this.transferType); public Loan toLoanAccount() {
} return this.toLoanAccount;
}
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) { public Loan fromLoanAccount() {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount, return this.fromLoanAccount;
transferType, null); }
}
public CurrentAccount fromCurrentAccount() {
public List<AccountTransferTransaction> getAccountTransferTransactions() { return this.fromCurrentAccount;
return accountTransferTransactions; }
}
} public CurrentAccount toCurrentAccount() {
return this.toCurrentAccount;
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction;
}
public AccountTransferType transferType() {
return AccountTransferType.fromInt(this.transferType);
}
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
// --- Current account factory methods ---
public static AccountTransferDetails currentToCurrentTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToSavingsTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToCurrentTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToLoanTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
public List<AccountTransferTransaction> getAccountTransferTransactions() {
return accountTransferTransactions;
}
}
@@ -1,290 +1,302 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity @Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = { @Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") }) @UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> { public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne @ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true) @JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails; private AccountTransferDetails accountTransferDetails;
@Column(name = "name") @Column(name = "name")
private String name; private String name;
@Column(name = "priority") @Column(name = "priority")
private Integer priority; private Integer priority;
@Column(name = "instruction_type") @Column(name = "instruction_type")
private Integer instructionType; private Integer instructionType;
@Column(name = "status") @Column(name = "status")
private Integer status; private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true) @Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount; private BigDecimal amount;
@Column(name = "valid_from") @Column(name = "valid_from")
private LocalDate validFrom; private LocalDate validFrom;
@Column(name = "valid_till") @Column(name = "valid_till")
private LocalDate validTill; private LocalDate validTill;
@Column(name = "recurrence_type") @Column(name = "recurrence_type")
private Integer recurrenceType; private Integer recurrenceType;
@Column(name = "recurrence_frequency") @Column(name = "recurrence_frequency")
private Integer recurrenceFrequency; private Integer recurrenceFrequency;
@Column(name = "recurrence_interval") @Column(name = "recurrence_interval")
private Integer recurrenceInterval; private Integer recurrenceInterval;
@Column(name = "recurrence_on_day") @Column(name = "recurrence_on_day")
private Integer recurrenceOnDay; private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month") @Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth; private Integer recurrenceOnMonth;
@Column(name = "last_run_date") @Column(name = "last_run_date")
private LocalDate latsRunDate; private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() { protected AccountTransferStandingInstruction() {
} }
public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name, public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) { final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null; Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null; Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) { if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth(); recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue(); recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
} }
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom, return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth); validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
} }
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name, private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final Integer recurrenceOnDay, final Integer recurrenceOnMonth) { final Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails; this.accountTransferDetails = accountTransferDetails;
this.name = name; this.name = name;
this.priority = priority; this.priority = priority;
this.instructionType = instructionType; this.instructionType = instructionType;
this.status = status; this.status = status;
this.amount = amount; this.amount = amount;
this.validFrom = validFrom; this.validFrom = validFrom;
this.validTill = validTill; this.validTill = validTill;
this.recurrenceType = recurrenceType; this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency; this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval; this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay; this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth; this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME); .resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator); validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
} }
public Map<String, Object> update(JsonCommand command) { public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>(); final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME); .resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) { if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted"); baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
} }
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) { if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName); this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom); actualChanges.put(validFromParamName, this.validFrom);
} }
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) { if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName); this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill); actualChanges.put(validTillParamName, this.validTill);
} }
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) { if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName); final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue); actualChanges.put(amountParamName, newValue);
this.amount = newValue; this.amount = newValue;
} }
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) { if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName); final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue); actualChanges.put(statusParamName, newValue);
this.status = newValue; this.status = newValue;
} }
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) { if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName); final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue); actualChanges.put(priorityParamName, newValue);
this.priority = newValue; this.priority = newValue;
} }
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) { if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName); final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue); actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue; this.instructionType = newValue;
} }
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) { if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue); actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue; this.recurrenceType = newValue;
} }
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) { if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue); actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue; this.recurrenceFrequency = newValue;
} }
if (command.hasParameter(recurrenceOnMonthDayParamName)) { if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth(); final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) { if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue; this.recurrenceOnDay = dayOfMonthValue;
} }
final Integer monthOfYear = monthDay.getMonthValue(); final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) { if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear; this.recurrenceOnMonth = monthOfYear;
} }
} }
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) { if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue); actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue; this.recurrenceInterval = newValue;
} }
validateDependencies(baseDataValidator); validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
return actualChanges; return actualChanges;
} }
private void validateDependencies(final DataValidatorBuilder baseDataValidator) { private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) { if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom); baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
} }
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) { if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull(); baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull(); baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) { if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency); PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) { if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) { } else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
} }
} }
} }
if (this.accountTransferDetails.toSavingsAccount() != null) { // For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1); if (this.accountTransferDetails.toSavingsAccount() != null
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1); || this.accountTransferDetails.toCurrentAccount() != null) {
} baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) { }
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
} if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) {
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
String errorCode = null; }
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) { // Account Transfer: from must be savings OR current; to must be savings OR current
errorCode = "not.account.transfer"; boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
} else if (this.accountTransferDetails.transferType().isLoanRepayment() || this.accountTransferDetails.fromCurrentAccount() != null;
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) { boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
errorCode = "not.loan.repayment"; || this.accountTransferDetails.toCurrentAccount() != null;
}
if (errorCode != null) { // Loan Repayment: from must be savings OR current; to must be loan
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
} || this.accountTransferDetails.fromCurrentAccount() != null;
} String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
public void updateLatsRunDate(LocalDate latsRunDate) { && !(fromAccountValid && toAccountValid)) {
this.latsRunDate = latsRunDate; errorCode = "not.account.transfer";
} } else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
public void updateStatus(Integer status) { errorCode = "not.loan.repayment";
this.status = status; }
} if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
/** }
* delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name
**/ }
public void delete() {
this.status = StandingInstructionStatus.DELETED.getValue(); public void updateLatsRunDate(LocalDate latsRunDate) {
this.name = this.name + "_deleted_" + this.getId(); this.latsRunDate = latsRunDate;
} }
}
public void updateStatus(Integer status) {
this.status = status;
}
/**
* delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name
**/
public void delete() {
this.status = StandingInstructionStatus.DELETED.getValue();
this.name = this.name + "_deleted_" + this.getId();
}
}
@@ -1,93 +1,143 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.springframework.beans.factory.annotation.Autowired; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.springframework.stereotype.Service; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Service import org.springframework.beans.factory.annotation.Autowired;
public class StandingInstructionAssembler { import org.springframework.stereotype.Service;
private final AccountTransferDetailAssembler accountTransferDetailAssembler; @Service
public class StandingInstructionAssembler {
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) { private final AccountTransferDetailAssembler accountTransferDetailAssembler;
this.accountTransferDetailAssembler = accountTransferDetailAssembler; @Autowired
} public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { this.accountTransferDetailAssembler = accountTransferDetailAssembler;
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command); }
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails; public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
} final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) { return accountTransferDetails;
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName); }
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null; public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName); final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
if (transferAmount != null) { final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount); BigDecimal amount = null;
amount = monetaryAmount.getAmount(); final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
} if (transferAmount != null) {
final Integer status = command.integerValueOfParameterNamed(statusParamName); final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName); if (currency != null) {
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName); final Money monetaryAmount = Money.of(currency, transferAmount);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName); amount = monetaryAmount.getAmount();
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); } else {
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); amount = transferAmount;
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName); }
final String name = command.stringValueOfParameterNamed(nameParamName); }
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create( final Integer status = command.integerValueOfParameterNamed(statusParamName);
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType, final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay); final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
} final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command); final String name = command.stringValueOfParameterNamed(nameParamName);
assembleStandingInstruction(command, accountTransferDetails); AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
return accountTransferDetails; accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
} recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { }
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails); /**
return accountTransferDetails; * Resolves the currency from the "from" account — savings, loan (null), or current account.
} */
private MonetaryCurrency resolveCurrency(final AccountTransferDetails details) {
} final SavingsAccount fromSavings = details.fromSavingsAccount();
if (fromSavings != null) {
return fromSavings.getCurrency();
}
final CurrentAccount fromCurrent = details.fromCurrentAccount();
if (fromCurrent != null) {
return new MonetaryCurrency(fromCurrent.getCurrencyCode(), fromCurrent.getCurrencyDigits(),
fromCurrent.getCurrencyMultiplesOf());
}
return null; // loan accounts don't have a direct currency on AccountTransferDetails
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
// --- Current account methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.account.handler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.account.service.AccountTerminateWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "ACCOUNT", action = "TERMINATE")
public class AccountTerminateCommandHandler implements NewCommandSourceHandler {
private final AccountTerminateWritePlatformService writePlatformService;
@Autowired
public AccountTerminateCommandHandler(final AccountTerminateWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.terminate(command.entityId(), command);
}
}
@@ -1,70 +1,73 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.springframework.batch.core.Job; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.Step;
import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig { @Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository; @Autowired
@Autowired private JobRepository jobRepository;
private PlatformTransactionManager transactionManager; @Autowired
@Autowired private PlatformTransactionManager transactionManager;
private StandingInstructionReadPlatformService standingInstructionReadPlatformService; @Autowired
@Autowired private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private JdbcTemplate jdbcTemplate; @Autowired
@Autowired private JdbcTemplate jdbcTemplate;
private DatabaseSpecificSQLGenerator sqlGenerator; @Autowired
@Autowired private DatabaseSpecificSQLGenerator sqlGenerator;
private AccountTransfersWritePlatformService accountTransfersWritePlatformService; @Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean @Autowired
protected Step executeStandingInstructionsStep() { private AccountTransferDetailRepository accountTransferDetailRepository;
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build(); @Bean
} protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
@Bean .tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
public Job executeStandingInstructionsJob() { }
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build(); @Bean
} public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
@Bean .incrementer(new RunIdIncrementer()).build();
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() { }
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService); @Bean
} public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
} return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService, accountTransferDetailRepository);
}
}
@@ -1,172 +1,180 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData; import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.springframework.batch.core.StepContribution; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.batch.core.scope.context.ChunkContext; import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.StepContribution;
import org.springframework.batch.repeat.RepeatStatus; import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j import org.springframework.jdbc.core.JdbcTemplate;
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet { @Slf4j
@RequiredArgsConstructor
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator; private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
@Override private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { private final AccountTransferDetailRepository accountTransferDetailRepository;
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue()); @Override
List<Throwable> errors = new ArrayList<>(); public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
for (StandingInstructionData data : instructionData) { Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
boolean isDueForTransfer = false; .retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType(); List<Throwable> errors = new ArrayList<>();
StandingInstructionType instructionType = data.getInstructionType(); for (StandingInstructionData data : instructionData) {
LocalDate transactionDate = DateUtils.getBusinessLocalDate(); boolean isDueForTransfer = false;
if (recurrenceType.isPeriodicRecurrence()) { AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); StandingInstructionType instructionType = data.instructionTypeEnum();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency(); LocalDate transactionDate = DateUtils.getBusinessLocalDate();
LocalDate startDate = data.getValidFrom(); if (recurrenceType.isPeriodicRecurrence()) {
if (frequencyType.isMonthly()) { final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()); PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
if (DateUtils.isBefore(startDate, data.getValidFrom())) { LocalDate startDate = data.getValidFrom();
startDate = startDate.plusMonths(1); if (frequencyType.isMonthly()) {
} startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
} else if (frequencyType.isYearly()) { if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth()); startDate = startDate.plusMonths(1);
if (DateUtils.isBefore(startDate, data.getValidFrom())) { }
startDate = startDate.plusYears(1); } else if (frequencyType.isYearly()) {
} startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
} if (DateUtils.isBefore(startDate, data.getValidFrom())) {
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate, startDate = startDate.plusYears(1);
transactionDate); }
}
} isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
BigDecimal transactionAmount = data.getAmount(); transactionDate);
if (data.getToAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) { }
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService BigDecimal transactionAmount = data.getAmount();
.retriveLoanDuesData(data.getToAccount().getId()); if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
if (data.getInstructionType().isDuesAmoutTransfer()) { && (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
transactionAmount = standingInstructionDuesData.totalDueAmount(); StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
} .retriveLoanDuesData(data.getToAccount().getId());
if (recurrenceType.isDuesRecurrence()) { if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
isDueForTransfer = isDueForTransfer(standingInstructionDuesData); transactionAmount = standingInstructionDuesData.totalDueAmount();
} }
} if (recurrenceType.isDuesRecurrence()) {
isDueForTransfer = isDueForTransfer(standingInstructionDuesData);
if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) { }
final SavingsAccount fromSavingsAccount = null; }
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false; if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, final SavingsAccount fromSavingsAccount = null;
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(), final boolean isRegularTransaction = true;
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, final boolean isExceptionForBalanceCheck = false;
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, // Load existing AccountTransferDetails so transactions are linked to
isRegularTransaction, isExceptionForBalanceCheck); // this standing instruction and appear in its transaction history
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
if (transferCompleted) { : null;
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?"; AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
jdbcTemplate.update(updateQuery, transactionDate, data.getId()); data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
} data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.transferTypeEnum().getValue(), existingDetails, null, ExternalId.empty(), null, null, fromSavingsAccount,
} isRegularTransaction, isExceptionForBalanceCheck);
} final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (!errors.isEmpty()) {
throw new JobExecutionException(errors); if (transferCompleted) {
} final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
return RepeatStatus.FINISHED; jdbcTemplate.update(updateQuery, transactionDate, data.getId());
} }
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) { }
boolean transferCompleted = true; }
StringBuilder errorLog = new StringBuilder(); if (!errors.isEmpty()) {
StringBuilder updateQuery = new StringBuilder( throw new JobExecutionException(errors);
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status") }
+ ", amount,execution_time, error_log) VALUES ("); return RepeatStatus.FINISHED;
try { }
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) { private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from " boolean transferCompleted = true;
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); StringBuilder errorLog = new StringBuilder();
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage()); StringBuilder updateQuery = new StringBuilder(
} catch (final InsufficientAccountBalanceException e) { "INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id" + ", amount,execution_time, error_log) VALUES (");
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); try {
errorLog.append("InsufficientAccountBalance Exception "); accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final AbstractPlatformServiceUnavailableException e) { } catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from " errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage()); errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) { } catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage()); errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
} errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
updateQuery.append(instructionId).append(","); + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
if (errorLog.length() > 0) { errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
transferCompleted = false; } catch (Exception e) {
updateQuery.append("'failed'").append(","); errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
} else { + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
updateQuery.append("'success'").append(","); errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue()); }
updateQuery.append(", now(),"); updateQuery.append(instructionId).append(",");
updateQuery.append("'").append(errorLog).append("')"); if (errorLog.length() > 0) {
jdbcTemplate.update(updateQuery.toString()); transferCompleted = false;
return transferCompleted; updateQuery.append("'failed'").append(",");
} } else {
updateQuery.append("'success'").append(",");
public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) { }
return standingInstructionDuesData.dueDate() != null updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
&& !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); updateQuery.append(", now(),");
} updateQuery.append("'").append(errorLog).append("')");
} jdbcTemplate.update(updateQuery.toString());
return transferCompleted;
}
public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) {
return standingInstructionDuesData.dueDate() != null
&& !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
}
}
@@ -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.account.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface AccountTerminateWritePlatformService {
CommandProcessingResult terminate(Long accountId, JsonCommand command);
}
@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.account.service;
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.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Routes terminate to the correct account service based on the
* "accountType" field in the JSON body:
* SAVINGS → m_savings_account (depositType = savings)
* FD → m_savings_account (depositType = 200)
* RD → m_savings_account (depositType = 300)
* CURRENT → m_current_account
*/
@Service
@RequiredArgsConstructor
public class AccountTerminateWritePlatformServiceImpl implements AccountTerminateWritePlatformService {
private static final String PARAM_ACCOUNT_TYPE = "accountType";
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
@Override
@Transactional
public CommandProcessingResult terminate(final Long accountId, final JsonCommand command) {
final String accountType = command.stringValueOfParameterNamed(PARAM_ACCOUNT_TYPE);
if (accountType == null || accountType.isBlank()) {
throw new PlatformDataIntegrityException(
"error.msg.account.terminate.accountType.required",
"accountType is required: SAVINGS, CURRENT, FD or RD");
}
return switch (accountType.toUpperCase()) {
case "CURRENT" -> currentAccountWritePlatformService.terminateAccount(accountId, command);
case "SAVINGS", "FD", "RD" -> savingsAccountWritePlatformService.terminate(accountId, command);
default -> throw new PlatformDataIntegrityException(
"error.msg.account.terminate.accountType.invalid",
"Unknown accountType '" + accountType + "'. Expected SAVINGS, CURRENT, FD or RD.");
};
}
}
@@ -1,202 +1,206 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations { public final class AccountTransferEnumerations {
private AccountTransferEnumerations() { private AccountTransferEnumerations() {
} }
public static EnumOptionData accountType(final Integer type) { public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type)); return accountType(PortfolioAccountType.fromInt(type));
} }
public static EnumOptionData accountType(final PortfolioAccountType type) { public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null; EnumOptionData optionData = null;
if (type != null) { if (type != null) {
switch (type) { switch (type) {
case INVALID: case INVALID:
break; break;
case LOAN: case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(), optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account"); "Loan Account");
break; break;
case SAVINGS: case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(), optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account"); PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break; break;
} case CURRENT:
} optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
PortfolioAccountType.CURRENT.getCode(), "Current Account");
return optionData; break;
} }
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type)); return optionData;
} }
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) { public static EnumOptionData recurrenceType(final Integer type) {
EnumOptionData optionData = null; return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
break; EnumOptionData optionData = null;
case PERIODIC: if (type != null) {
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(), switch (type) {
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence"); case INVALID:
break; break;
case AS_PER_DUES: case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(), optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence"); AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break; break;
} case AS_PER_DUES:
} optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
return optionData; AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
} break;
}
public static EnumOptionData transferType(final Integer type) { }
return transferType(AccountTransferType.fromInt(type)); return optionData;
} }
public static EnumOptionData transferType(final AccountTransferType type) { public static EnumOptionData transferType(final Integer type) {
EnumOptionData optionData = null; return transferType(AccountTransferType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData transferType(final AccountTransferType type) {
break; EnumOptionData optionData = null;
case ACCOUNT_TRANSFER: if (type != null) {
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(), switch (type) {
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer"); case INVALID:
break; break;
case LOAN_REPAYMENT: case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment"); AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break; break;
case CHARGE_PAYMENT: case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment"); AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break; break;
case INTEREST_TRANSFER: case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer"); AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break; break;
case LOAN_DOWN_PAYMENT: case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment"); AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break; break;
case LOAN_DOWN_PAYMENT:
} optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
} AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
return optionData; break;
}
}
public static EnumOptionData standingInstructionPriority(final Integer type) { }
return standingInstructionPriority(StandingInstructionPriority.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) { public static EnumOptionData standingInstructionPriority(final Integer type) {
EnumOptionData optionData = null; return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
break; EnumOptionData optionData = null;
case HIGH: if (type != null) {
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(), switch (type) {
StandingInstructionPriority.HIGH.getCode(), "High Priority"); case INVALID:
break; break;
case LOW: case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority"); StandingInstructionPriority.HIGH.getCode(), "High Priority");
break; break;
case MEDIUM: case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority"); StandingInstructionPriority.LOW.getCode(), "Low Priority");
break; break;
case URGENT: case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority"); StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break; break;
} case URGENT:
} optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
return optionData; StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
} break;
}
public static EnumOptionData standingInstructionStatus(final Integer type) { }
return standingInstructionStatus(StandingInstructionStatus.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) { public static EnumOptionData standingInstructionStatus(final Integer type) {
EnumOptionData optionData = null; return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
break; EnumOptionData optionData = null;
case ACTIVE: if (type != null) {
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(), switch (type) {
StandingInstructionStatus.ACTIVE.getCode(), "Active"); case INVALID:
break; break;
case DELETED: case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted"); StandingInstructionStatus.ACTIVE.getCode(), "Active");
break; break;
case DISABLED: case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled"); StandingInstructionStatus.DELETED.getCode(), "Deleted");
break; break;
} case DISABLED:
} optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
return optionData; StandingInstructionStatus.DISABLED.getCode(), "Disabled");
} break;
}
public static EnumOptionData standingInstructionType(final Integer type) { }
return standingInstructionType(StandingInstructionType.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionType(final StandingInstructionType type) { public static EnumOptionData standingInstructionType(final Integer type) {
EnumOptionData optionData = null; return standingInstructionType(StandingInstructionType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
break; EnumOptionData optionData = null;
case DUES: if (type != null) {
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(), switch (type) {
StandingInstructionType.DUES.getCode(), "Dues"); case INVALID:
break; break;
case FIXED: case DUES:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed"); StandingInstructionType.DUES.getCode(), "Dues");
break; break;
case FIXED:
} optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
} StandingInstructionType.FIXED.getCode(), "Fixed");
return optionData; break;
}
}
} }
return optionData;
}
}
@@ -1,396 +1,482 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData; import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService { public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
// mapper // mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper; private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper; private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate; public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); this.jdbcTemplate = jdbcTemplate;
this.loanAccountMapper = new PortfolioLoanAccountMapper(); this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator); this.loanAccountMapper = new PortfolioLoanAccountMapper();
} this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
this.currentAccountMapper = new PortfolioCurrentAccountMapper();
@Override }
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) {
return retrieveOne(accountId, accountTypeId, null); @Override
} public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) {
return retrieveOne(accountId, accountTypeId, null);
@Override }
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
@Override
Object[] sqlParams = new Object[] { accountId }; public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
PortfolioAccountData accountData = null;
try { Object[] sqlParams = new Object[] { accountId };
String sql = null; PortfolioAccountData accountData = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId); try {
switch (accountType) { String sql = null;
case INVALID: final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
break; switch (accountType) {
case LOAN: case INVALID:
break;
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?"; case LOAN:
if (currencyCode != null) {
sql += " and la.currency_code = ?"; sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
sqlParams = new Object[] { accountId, currencyCode }; if (currencyCode != null) {
} sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams); }
break;
case SAVINGS: accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?"; break;
if (currencyCode != null) { case SAVINGS:
sql += " and sa.currency_code = ?"; sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
sqlParams = new Object[] { accountId, currencyCode }; if (currencyCode != null) {
} sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams); }
break;
} accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) { break;
throw new AccountTransferNotFoundException(accountId, e); case CURRENT:
} sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
if (currencyCode != null) {
return accountData; sql += " and ca.currency_code = ?";
} sqlParams = new Object[] { accountId, currencyCode };
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) { accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
break;
final List<Object> sqlParams = new ArrayList<>(); }
// sqlParams.add(portfolioAccountDTO.getClientId()); } catch (final EmptyResultDataAccessException e) {
Collection<PortfolioAccountData> accounts = null; throw new AccountTransferNotFoundException(accountId, e);
String sql = null; }
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) { return accountData;
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus(); }
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId()); @Override
switch (accountType) { public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
case INVALID:
break; final List<Object> sqlParams = new ArrayList<>();
case LOAN: // sqlParams.add(portfolioAccountDTO.getClientId());
sql = "select " + this.loanAccountMapper.schema() + " where "; Collection<PortfolioAccountData> accounts = null;
if (portfolioAccountDTO.getClientId() != null) { String sql = null;
sql += " la.client_id = ? and la.loan_status_id in (?) "; long defaultAccountStatus = 300; // Active Status
sqlParams.add(portfolioAccountDTO.getClientId()); if (portfolioAccountDTO.getAccountStatus() != null) {
sqlParams.add(defaultAccountStatus); defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
} else { }
sql += " la.loan_status_id in (?) "; final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
sqlParams.add(defaultAccountStatus); switch (accountType) {
} case INVALID:
if (portfolioAccountDTO.getCurrencyCode() != null) { break;
sql += " and la.currency_code = ?"; case LOAN:
sqlParams.add(portfolioAccountDTO.getCurrencyCode()); sql = "select " + this.loanAccountMapper.schema() + " where ";
} if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR sqlParams.add(portfolioAccountDTO.getClientId());
break; sqlParams.add(defaultAccountStatus);
case SAVINGS: } else {
sql = "select " + this.savingsAccountMapper.schema() + " where "; sql += " la.loan_status_id in (?) ";
if (portfolioAccountDTO.getClientId() != null) { sqlParams.add(defaultAccountStatus);
sql += " sa.client_id = ? and sa.status_enum in (?) "; }
sqlParams.add(portfolioAccountDTO.getClientId()); if (portfolioAccountDTO.getCurrencyCode() != null) {
sqlParams.add(defaultAccountStatus); sql += " and la.currency_code = ?";
} else { sqlParams.add(portfolioAccountDTO.getCurrencyCode());
sql += " sa.status_enum in (?) "; }
sqlParams.add(defaultAccountStatus);
} accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
if (portfolioAccountDTO.getCurrencyCode() != null) { break;
sql += " and sa.currency_code = ?"; case SAVINGS:
sqlParams.add(portfolioAccountDTO.getCurrencyCode()); sql = "select " + this.savingsAccountMapper.schema() + " where ";
} if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
if (portfolioAccountDTO.getDepositType() != null) { sqlParams.add(portfolioAccountDTO.getClientId());
sql += " and sa.deposit_type_enum = ?"; sqlParams.add(defaultAccountStatus);
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue()); } else {
} sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) { }
sql += " and sa.allow_overdraft = false"; if (portfolioAccountDTO.getCurrencyCode() != null) {
} sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) { }
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId()); if (portfolioAccountDTO.getDepositType() != null) {
} sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR }
break;
} if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
return accounts; }
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> { sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
private final String schemaSql; }
PortfolioSavingsAccountMapper() { accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
final StringBuilder sqlBuilder = new StringBuilder(400); case CURRENT:
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); sql = "select " + this.currentAccountMapper.schema() + " where ";
sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); if (portfolioAccountDTO.getClientId() != null) {
sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); sql += " ca.client_id = ? and ca.status_enum in (?) ";
sqlBuilder.append("sp.id as productId, sp.name as productName, "); sqlParams.add(portfolioAccountDTO.getClientId());
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); sqlParams.add(defaultAccountStatus);
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,"); } else {
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, "); sql += " ca.status_enum in (?) ";
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); sqlParams.add(defaultAccountStatus);
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); }
sqlBuilder.append("from m_savings_account sa "); if (portfolioAccountDTO.getCurrencyCode() != null) {
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); sql += " and ca.currency_code = ?";
sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); sqlParams.add(portfolioAccountDTO.getCurrencyCode());
sqlBuilder.append("left join m_client c ON c.id = sa.client_id "); }
sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id "); accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
break;
this.schemaSql = sqlBuilder.toString(); }
}
return accounts;
public String schema() { }
return this.schemaSql;
} private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
@Override private final String schemaSql;
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
PortfolioSavingsAccountMapper() {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String externalId = rs.getString("externalId"); sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("sp.id as productId, sp.name as productName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final String clientName = rs.getString("clientName"); sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final String productName = rs.getString("productName"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_savings_account sa ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = sa.client_id ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id ");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); this.schemaSql = sqlBuilder.toString();
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); }
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, public String schema() {
currencyNameCode); return this.schemaSql;
}
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency); @Override
} public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
}
final Long id = rs.getLong("id");
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> { final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
private final String schemaSql;
final Long groupId = JdbcSupport.getLong(rs, "groupId");
PortfolioLoanAccountMapper() { final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final StringBuilder sqlBuilder = new StringBuilder(400); final String clientName = rs.getString("clientName");
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); final Long productId = rs.getLong("productId");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); final String productName = rs.getString("productName");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); final Long fieldOfficerId = rs.getLong("fieldOfficerId");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); final String fieldOfficerName = rs.getString("fieldOfficerName");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, "); final String currencyCode = rs.getString("currencyCode");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); final String currencyName = rs.getString("currencyName");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); final String currencyNameCode = rs.getString("currencyNameCode");
sqlBuilder.append("from m_loan la "); final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
sqlBuilder.append("left join m_client c ON c.id = la.client_id "); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
sqlBuilder.append("left join m_group g ON g.id = la.group_id "); currencyNameCode);
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
this.schemaSql = sqlBuilder.toString(); fieldOfficerId, fieldOfficerName, currency);
} }
}
public String schema() {
return this.schemaSql; private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
}
private final String schemaSql;
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { PortfolioLoanAccountMapper() {
final Long id = rs.getLong("id"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String accountNo = rs.getString("accountNo"); sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
final String externalId = rs.getString("externalId"); sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("lp.id as productId, lp.name as productName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
final String clientName = rs.getString("clientName"); sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final String productName = rs.getString("productName"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); this.schemaSql = sqlBuilder.toString();
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); }
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); public String schema() {
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, return this.schemaSql;
currencyNameCode); }
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, @Override
fieldOfficerId, fieldOfficerName, currency, amtForTransfer); public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
}
} final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> { final String externalId = rs.getString("externalId");
private String schemaSql; final Long groupId = JdbcSupport.getLong(rs, "groupId");
private final DatabaseSpecificSQLGenerator sqlGenerator; final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) { final String clientName = rs.getString("clientName");
this.sqlGenerator = sqlGenerator;
} final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400); final Long fieldOfficerId = rs.getLong("fieldOfficerId");
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +"); final String fieldOfficerName = rs.getString("fieldOfficerName");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); final String currencyCode = rs.getString("currencyCode");
amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); final String currencyName = rs.getString("currencyName");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id"); final String currencyNameCode = rs.getString("currencyNameCode");
amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300"); final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having"); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + "); final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + "); final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid "); currencyNameCode);
final StringBuilder sqlBuilder = new StringBuilder(400); return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); }
sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); }
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); private String schemaSql;
sqlBuilder.append(amountQueryString.toString()); private final DatabaseSpecificSQLGenerator sqlGenerator;
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); this.sqlGenerator = sqlGenerator;
sqlBuilder.append("from m_loan la "); }
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); public String schema() {
sqlBuilder.append("left join m_client c ON c.id = la.client_id "); final StringBuilder amountQueryString = new StringBuilder(400);
sqlBuilder.append("left join m_group g ON g.id = la.group_id "); amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
this.schemaSql = sqlBuilder.toString(); amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id");
return this.schemaSql; amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300");
} amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having");
amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + ");
@Override amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String externalId = rs.getString("externalId"); sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("lp.id as productId, lp.name as productName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final String clientName = rs.getString("clientName"); sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append(amountQueryString.toString());
final String productName = rs.getString("productName"); sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
final String currencyNameCode = rs.getString("currencyNameCode"); sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); this.schemaSql = sqlBuilder.toString();
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, return this.schemaSql;
currencyNameCode); }
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, @Override
fieldOfficerId, fieldOfficerName, currency, amtForTransfer); public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
}
} final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
@Override final String externalId = rs.getString("externalId");
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub final Long groupId = JdbcSupport.getLong(rs, "groupId");
Object[] sqlParams = new Object[] { accountId, accountId }; final String groupName = rs.getString("groupName");
PortfolioAccountData accountData = null; final Long clientId = JdbcSupport.getLong(rs, "clientId");
// String currencyCode = null; final String clientName = rs.getString("clientName");
try {
String sql = null; final Long productId = rs.getLong("productId");
// final PortfolioAccountType accountType = final String productName = rs.getString("productName");
// PortfolioAccountType.fromInt(accountTypeId);
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?"; final String fieldOfficerName = rs.getString("fieldOfficerName");
/*
* if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId , final String currencyCode = rs.getString("currencyCode");
* accountId,currencyCode }; } final String currencyName = rs.getString("currencyName");
*/ final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
} catch (final EmptyResultDataAccessException e) { final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
throw new AccountTransferNotFoundException(accountId, e); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
} currencyNameCode);
return accountData; return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
} fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
} }
}
@Override
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub
Object[] sqlParams = new Object[] { accountId, accountId };
PortfolioAccountData accountData = null;
// String currencyCode = null;
try {
String sql = null;
// final PortfolioAccountType accountType =
// PortfolioAccountType.fromInt(accountTypeId);
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?";
/*
* if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId ,
* accountId,currencyCode }; }
*/
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
private static final class PortfolioCurrentAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioCurrentAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("ca.id as id, ca.account_no as accountNo, ca.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("null as groupId, null as groupName, ");
sqlBuilder.append("cp.id as productId, cp.name as productName, ");
sqlBuilder.append("null as fieldOfficerId, null as fieldOfficerName, ");
sqlBuilder.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ");
sqlBuilder.append("ca.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_current_account ca ");
sqlBuilder.append("join m_current_product cp ON ca.product_id = cp.id ");
sqlBuilder.append("join m_currency curr on curr.code = ca.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = ca.client_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = JdbcSupport.getLong(rs, "fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
currencyDisplaySymbol, currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
}
@@ -1,145 +1,179 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException; import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException; import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService { public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator; private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler; private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository; private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository; private final StandingInstructionRepository standingInstructionRepository;
@Transactional @Transactional
@Override @Override
public CommandProcessingResult create(final JsonCommand command) { public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command); this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName); final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null; Long standingInstructionId = null;
try { try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command); .assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); } else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
} this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} catch (final JpaSystemException | DataIntegrityViolationException dve) { standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
final Throwable throwable = dve.getMostSpecificCause(); } else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
handleDataIntegrityIssues(command, throwable, dve); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
return CommandProcessingResult.empty(); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId) } else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
.withClientId(fromClientId); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
return builder.build(); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) { final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
if (realCause.getMessage().contains("name")) { this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name", }
"Standinginstruction with name `" + name + "` already exists", "name", name); } catch (final JpaSystemException | DataIntegrityViolationException dve) {
} final Throwable throwable = dve.getMostSpecificCause();
log.error("Error occured.", dve); handleDataIntegrityIssues(command, throwable, dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue", return CommandProcessingResult.empty();
"Unknown data integrity issue with resource: " + realCause.getMessage()); }
} final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { return builder.build();
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); }
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { if (realCause.getMessage().contains("name")) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
} throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, }
final PortfolioAccountType toAccountType) { log.error("Error occured.", dve);
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
} "Unknown data integrity issue with resource: " + realCause.getMessage());
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) { private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
this.standingInstructionDataValidator.validateForUpdate(command); return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id) }
.orElseThrow(() -> new StandingInstructionNotFoundException(id));
final Map<String, Object> actualChanges = standingInstructionsForUpdate.update(command); private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build(); return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
} }
@Override private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
public CommandProcessingResult delete(final Long id) { final PortfolioAccountType toAccountType) {
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow(); return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
// update the "deleted" and "name" properties of the standing }
// instruction
standingInstructionsForUpdate.delete(); private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
final Map<String, Object> actualChanges = new HashMap<>(); return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue()); }
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
} private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
} final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) {
this.standingInstructionDataValidator.validateForUpdate(command);
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id)
.orElseThrow(() -> new StandingInstructionNotFoundException(id));
final Map<String, Object> actualChanges = standingInstructionsForUpdate.update(command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build();
}
@Override
public CommandProcessingResult delete(final Long id) {
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow();
// update the "deleted" and "name" properties of the standing
// instruction
standingInstructionsForUpdate.delete();
final Map<String, Object> actualChanges = new HashMap<>();
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue());
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
}
}
@@ -1,134 +1,140 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.starter; package org.apache.fineract.portfolio.account.starter;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper; import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.context.annotation.Bean; import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.context.annotation.Configuration; import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@Configuration import org.springframework.context.annotation.Configuration;
public class AccountConfiguration { import org.springframework.jdbc.core.JdbcTemplate;
@Bean @Configuration
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class) public class AccountConfiguration {
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate); @Bean
} @ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
@Bean return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class) }
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, @Bean
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator, @ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper, public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
SqlValidator sqlValidator) { ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator); DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
} SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
@Bean portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class) }
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler, @Bean
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler, @ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler, public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
FineractProperties fineractProperties) { LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties); CurrentAccountTransactionRepository currentAccountTransactionRepository,
} AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
@Bean accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class) loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate, gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
DatabaseSpecificSQLGenerator sqlGenerator) { currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator); }
}
@Bean
@Bean @ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class) public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper); }
}
@Bean
@Bean @ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class) public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate, ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, }
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { @Bean
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, @ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper); public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
} ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
@Bean DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
@ConditionalOnMissingBean(StandingInstructionWritePlatformService.class) DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
public StandingInstructionWritePlatformService standingInstructionWritePlatformService( return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler, portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) { }
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository); @Bean
} @ConditionalOnMissingBean(StandingInstructionWritePlatformService.class)
} public StandingInstructionWritePlatformService standingInstructionWritePlatformService(
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) {
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository);
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.accountdetails.data; package org.apache.fineract.portfolio.accountdetails.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
@@ -44,12 +45,28 @@ public class GuarantorAccountSummaryData {
private final Boolean isActive; private final Boolean isActive;
private final String relationship; private final String relationship;
private final BigDecimal onHoldAmount; private final BigDecimal onHoldAmount;
private final String savingsAccountNo;
private final BigDecimal amountReleased;
private final String borrowerName;
private final BigDecimal totalOverdue;
private final LocalDate overdueSinceDate;
private final LocalDate submittedOnDate;
private final LocalDate approvedOnDate;
private final LocalDate disbursedOnDate;
private final LocalDate expectedMaturityDate;
private final BigDecimal amountRemaining;
private final BigDecimal savingsBalance;
private final Integer missedInstallments;
public GuarantorAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, public GuarantorAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus, final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus,
final EnumOptionData loanType, final Integer loanCycle, final Boolean inArrears, final BigDecimal originalLoan, final EnumOptionData loanType, final Integer loanCycle, final Boolean inArrears, final BigDecimal originalLoan,
final BigDecimal loanBalance, final BigDecimal amountPaid, final Boolean isActive, final String relationship, final BigDecimal loanBalance, final BigDecimal amountPaid, final Boolean isActive, final String relationship,
final BigDecimal onHoldAmount) { final BigDecimal onHoldAmount, final String savingsAccountNo, final BigDecimal amountReleased,
final String borrowerName, final BigDecimal totalOverdue, final LocalDate overdueSinceDate,
final LocalDate submittedOnDate, final LocalDate approvedOnDate, final LocalDate disbursedOnDate,
final LocalDate expectedMaturityDate, final BigDecimal amountRemaining, final BigDecimal savingsBalance,
final Integer missedInstallments) {
this.id = id; this.id = id;
this.accountNo = accountNo; this.accountNo = accountNo;
this.externalId = externalId; this.externalId = externalId;
@@ -66,5 +83,17 @@ public class GuarantorAccountSummaryData {
this.isActive = isActive; this.isActive = isActive;
this.relationship = relationship; this.relationship = relationship;
this.onHoldAmount = onHoldAmount; this.onHoldAmount = onHoldAmount;
this.savingsAccountNo = savingsAccountNo;
this.amountReleased = amountReleased;
this.borrowerName = borrowerName;
this.totalOverdue = totalOverdue;
this.overdueSinceDate = overdueSinceDate;
this.submittedOnDate = submittedOnDate;
this.approvedOnDate = approvedOnDate;
this.disbursedOnDate = disbursedOnDate;
this.expectedMaturityDate = expectedMaturityDate;
this.amountRemaining = amountRemaining;
this.savingsBalance = savingsBalance;
this.missedInstallments = missedInstallments;
} }
} }
@@ -0,0 +1,298 @@
package org.apache.fineract.portfolio.cashexchange.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchange;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Path("/v1/cashexchange")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashExchangeApiResource {
private final CashExchangeRepository repository;
private final CashExchangeDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
private final DenominationBreakdownWriter denominationBreakdownWriter;
private final CashierRoleHelper cashierRoleHelper;
private final Gson gson = new Gson();
@POST
@Transactional
public String create(String body) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long accountId = json.has("accountId") && !json.get("accountId").isJsonNull() ? json.get("accountId").getAsLong() : null;
final String accountNo = json.has("accountNo") && !json.get("accountNo").isJsonNull() ? json.get("accountNo").getAsString()
: null;
final Long clientId = json.has("clientId") && !json.get("clientId").isJsonNull() ? json.get("clientId").getAsLong() : null;
final LocalDate exchangeDate = json.has("exchangeDate") && !json.get("exchangeDate").isJsonNull()
? LocalDate.parse(json.get("exchangeDate").getAsString())
: null;
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
final String customerIdProof = json.has("customerIdProof") && !json.get("customerIdProof").isJsonNull()
? json.get("customerIdProof").getAsString() : null;
final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received")
: new JsonArray();
final JsonArray given = json.has("given") && json.get("given").isJsonArray() ? json.getAsJsonArray("given") : new JsonArray();
final BigDecimal receivedTotal = sumTotal(received);
final BigDecimal givenTotal = sumTotal(given);
if (!hasPositiveCount(received) || !hasPositiveCount(given)) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
if (receivedTotal.compareTo(givenTotal) != 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.mismatch",
"Received total (" + receivedTotal + ") must equal given total (" + givenTotal + ").");
}
if (receivedTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
CashExchange header = new CashExchange();
header.setAccountId(accountId);
header.setAccountNo(accountNo);
header.setClientId(clientId);
header.setExchangeAmount(receivedTotal);
header.setExchangeDate(exchangeDate);
header.setNote(note);
header.setCustomerIdProof(customerIdProof);
header.setIsReversed(false);
header.setCreatedAt(LocalDateTime.now());
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
// Auto-detect cashier linked to this user
try {
Long cashierId = jdbcTemplate.queryForObject(
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
+ "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 e) {
// resolving the current user must never block recording the exchange
}
// Validate that cashier has enough of each denomination being GIVEN to the customer
if (header.getCashierId() != null) {
final java.util.Map<Long, Long> stock = denominationBreakdownWriter.getCashierNetPosition(header.getCashierId());
final java.util.List<String> shortages = new java.util.ArrayList<>();
for (JsonElement el : given) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) continue;
if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue;
long denomId = item.get("denominationId").getAsLong();
long available = stock.getOrDefault(denomId, 0L);
if (count > available) {
int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt() : 0;
shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")");
}
}
if (!shortages.isEmpty()) {
throw new PlatformDataIntegrityException(
"error.msg.cashier.denomination.insufficient",
"Insufficient denomination stock for exchange — cannot give: " + String.join("; ", shortages));
}
}
CashExchange saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), "RECEIVED", received);
saveDetails(saved.getId(), "GIVEN", given);
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
return gson.toJson(result);
}
@POST
@Path("{id}/reverse")
public String reverse(@PathParam("id") Long id, String body) {
CashExchange exchange = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
if (Boolean.TRUE.equals(exchange.getIsReversed())) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.already.reversed",
"Cash exchange " + id + " is already reversed.");
}
String reason = null;
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {}
exchange.setIsReversed(true);
exchange.setReversedAt(LocalDateTime.now());
exchange.setReverseReason(reason);
try {
AppUser user = this.context.authenticatedUser();
if (user != null) exchange.setReversedBy(user.getId());
} catch (Exception ignored) {}
repository.save(exchange);
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("reversed", true);
return gson.toJson(result);
}
@GET
public String getAll(
@QueryParam("accountId") Long accountId,
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
@QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") int limit) {
final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering();
List<CashExchange> all = accountId != null
? repository.findByAccountIdOrderByCreatedAtDesc(accountId)
: repository.findAllByOrderByCreatedAtDesc();
if (scopedCashierId != null) {
all = all.stream()
.filter(e -> scopedCashierId.equals(e.getCashierId()))
.collect(Collectors.toList());
}
final int total = all.size();
final int safeOffset = Math.max(0, Math.min(offset, total));
final int safeTo = Math.min(safeOffset + Math.max(1, limit), total);
final List<Map<String, Object>> page = all.subList(safeOffset, safeTo)
.stream().map(this::toMap).collect(Collectors.toList());
final Map<String, Object> resp = new LinkedHashMap<>();
resp.put("totalFilteredRecords", total);
resp.put("offset", safeOffset);
resp.put("limit", limit);
resp.put("pageItems", page);
return gson.toJson(resp);
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashExchange header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
Map<String, Object> map = toMap(header);
List<Map<String, Object>> details = detailRepository.findByExchangeId(id).stream().map(this::detailToMap)
.collect(Collectors.toList());
map.put("details", details);
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
total = total.add(BigDecimal.valueOf((long) value * count));
}
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
return true;
}
}
return false;
}
private void saveDetails(Long exchangeId, String direction, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) {
continue;
}
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong()
: null;
CashExchangeDetail detail = new CashExchangeDetail();
detail.setExchangeId(exchangeId);
detail.setDirection(direction);
detail.setDenominationId(denominationId);
detail.setDenominationValue(value);
detail.setCount(count);
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(detail);
}
}
private Map<String, Object> toMap(CashExchange e) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", e.getId());
map.put("cashierId", e.getCashierId());
map.put("accountId", e.getAccountId());
map.put("accountNo", e.getAccountNo());
map.put("clientId", e.getClientId());
map.put("customerIdProof", e.getCustomerIdProof());
map.put("exchangeAmount", e.getExchangeAmount());
map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null);
map.put("note", e.getNote());
map.put("performedBy", e.getPerformedBy());
map.put("performedByUsername", e.getPerformedByUsername());
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
map.put("reversedBy", e.getReversedBy());
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
map.put("reverseReason", e.getReverseReason());
return map;
}
private Map<String, Object> detailToMap(CashExchangeDetail d) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", d.getId());
map.put("direction", d.getDirection());
map.put("denominationId", d.getDenominationId());
map.put("denominationValue", d.getDenominationValue());
map.put("count", d.getCount());
map.put("totalAmount", d.getTotalAmount());
return map;
}
}
@@ -0,0 +1,75 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange")
@Getter
@Setter
@NoArgsConstructor
public class CashExchange {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "account_id")
private Long accountId;
@Column(name = "account_no", length = 50)
private String accountNo;
@Column(name = "client_id")
private Long clientId;
@Column(name = "exchange_amount", nullable = false)
private BigDecimal exchangeAmount;
@Column(name = "exchange_date")
private LocalDate exchangeDate;
@Column(name = "note", length = 500)
private String note;
@Column(name = "cashier_id")
private Long cashierId;
@Column(name = "customer_id_proof", length = 50)
private String customerIdProof;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "is_reversed")
private Boolean isReversed;
@Column(name = "reversed_by")
private Long reversedBy;
@Column(name = "reversed_at")
private LocalDateTime reversedAt;
@Column(name = "reverse_reason", length = 500)
private String reverseReason;
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashExchangeDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "exchange_id", nullable = false)
private Long exchangeId;
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}

Some files were not shown because too many files have changed in this diff Show More