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 pull *)",
"Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)",
"Bash(git ls-remote *)"
"Bash(git ls-remote *)",
"Bash(PGPASSWORD=root psql -h localhost -p 5432 -U postgres -d fineract_default -c \"SELECT cv.id, cv.code_value FROM m_code_value cv JOIN m_code c ON cv.code_id=c.id WHERE c.code_name='Constitution' ORDER BY cv.id;\")",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AQRPV8345E\",\"name\":\"Aditya Vyas\",\"dob\":\"19/12/1988\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"GIZMOGLADE PRIVATE LIMITED\",\"dob\":\"01/12/2025\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"\",\"dob\":\"\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"21AAMCG5406BIZ6\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"23AADCL9984A1ZH\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"PowerShell(Get-Process | Where-Object { $_.ProcessName -match '^\\(java|gradle\\)' } | Select-Object Id, ProcessName, StartTime, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}} | Format-Table -AutoSize)",
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -ErrorAction SilentlyContinue)",
"PowerShell(Stop-Process -Id 3468,3864,39964 -Force)",
"PowerShell(Get-Process -Id 3468,3864,39964 -ErrorAction SilentlyContinue)",
"PowerShell(foreach \\($id in 3468,3864,39964\\) { try { Stop-Process -Id $id -Force -ErrorAction Stop; \"killed $id\" } catch { \"skip $id - $\\($_.Exception.Message\\)\" } })",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue)",
"PowerShell(if \\(-not \\(Test-Path .\\\\fineract-provider\\\\logs\\)\\) { New-Item -ItemType Directory -Force .\\\\fineract-provider\\\\logs | Out-Null }; Remove-Item .\\\\fineract-provider\\\\logs\\\\devrun.out -ErrorAction SilentlyContinue; \"ready\")",
"PowerShell($j = Get-Process java -ErrorAction SilentlyContinue | Select-Object Id, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}}, StartTime; $j | Format-Table -AutoSize; \"---\"; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, OwningProcess)",
"PowerShell(if \\(Test-Path .\\\\fineract-provider\\\\logs\\\\devrun.out\\) { $f = Get-Item .\\\\fineract-provider\\\\logs\\\\devrun.out; \"size=$\\($f.Length\\) bytes mtime=$\\($f.LastWriteTime\\)\" } else { \"devrun.out missing\" })",
"PowerShell(netstat -ano)",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 800; $left = Get-Process java -ErrorAction SilentlyContinue; if \\($left\\) { $left | Select-Object Id, ProcessName } else { \"no java processes\" }; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Measure-Object | ForEach-Object { \"8443 listeners: $\\($_.Count\\)\" })",
"Bash(curl -sk -o /tmp/resp.txt -w \"STATUS=%{http_code}\\\\nCONTENT_TYPE=%{content_type}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -sk -o /tmp/r1.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r2.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r3.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Origin: http://localhost:4200' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r4.txt -w \"STATUS=%{http_code}\\\\n\" -X OPTIONS 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Origin: http://localhost:4200' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,fineract-platform-tenantid' -i)",
"Bash(git cherry-pick *)",
"PowerShell($parent = 'C:\\\\Users\\\\adity\\\\Music\\\\CBS'; if \\(Test-Path $parent\\) { \"parent exists: $parent\"; Get-ChildItem $parent -Directory | Select-Object -ExpandProperty Name } else { \"MISSING parent: $parent\" }; \"---\"; $target = Join-Path $parent 'LincpayEKYC'; if \\(Test-Path $target\\) { \"TARGET ALREADY EXISTS: $target\" } else { \"target free: $target\" })",
"Bash(git clone *)"
],
"additionalDirectories": [
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api",
+40 -37
View File
@@ -1,37 +1,40 @@
*.class
*.jar
*.war
*.ear
.classpath
.project
.settings
.gradle
build
*.iml
*.ipr
*.iws
*.swp
*.DS_Store
.idea
.vscode
catalina.base_IS_UNDEFINED/
keystore.jks
bin/
.lock
fineract-provider/out/
fineract-provider/config/swagger/config.json
fineract-provider/config/swagger/fineract-input.yaml
licenses
**/.asciidoctor/images/*
**/.asciidoctor/diagram/*
**/images/diag-*.svg
fineract-provider/src/main/generated/
**/out/
gradleExp/
.run/
.java-version
.windsurf/
*.class
*.jar
*.war
*.ear
.classpath
.project
.settings
.gradle
build
*.iml
*.ipr
*.iws
*.swp
*.DS_Store
.idea
.vscode
catalina.base_IS_UNDEFINED/
keystore.jks
bin/
.lock
fineract-provider/out/
fineract-provider/config/swagger/config.json
fineract-provider/config/swagger/fineract-input.yaml
licenses
**/.asciidoctor/images/*
**/.asciidoctor/diagram/*
**/images/diag-*.svg
fineract-provider/src/main/generated/
**/out/
gradleExp/
.run/
.java-version
.windsurf/
# Application logs
fineract-provider/logs/
@@ -1,272 +1,284 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.glaccount.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
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 final JdbcTemplate jdbcTemplate;
private static final String SQL_AND = " and ";
private static final class GLAccountMapper implements RowMapper<GLAccountData> {
private final JournalEntryAssociationParametersData associationParametersData;
GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) {
if (associationParametersData == null) {
this.associationParametersData = new JournalEntryAssociationParametersData();
} else {
this.associationParametersData = associationParametersData;
}
}
public String schema() {
StringBuilder sb = new StringBuilder();
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, ")
.append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ")
.append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ")
.append("cv.id as codeId, cv.code_value as codeValue ");
if (this.associationParametersData.isRunningBalanceRequired()) {
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 ");
if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append("left outer Join acc_gl_journal_entry gl_j on gl_j.account_id = gl.id");
}
return sb.toString();
}
@Override
public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String name = rs.getString("name");
final Long parentId = JdbcSupport.getLong(rs, "parentId");
final String glCode = rs.getString("glCode");
final boolean disabled = rs.getBoolean("disabled");
final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final int usageId = JdbcSupport.getInteger(rs, "accountUsage");
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId);
final String description = rs.getString("description");
final String nameDecorated = rs.getString("nameDecorated");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
final String codeValue = rs.getString("codeValue");
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
Long organizationRunningBalance = null;
if (associationParametersData.isRunningBalanceRequired()) {
organizationRunningBalance = rs.getLong("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) {
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
if (usage != null && !checkValidGLAccountUsage(usage)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
String sql = "select " + rm.schema();
// append SQL statement for fetching account totals
if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) {
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 "
+ "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 "
+ "group by t2.account_id desc) t1)";
}
final Object[] parameterArray = new Object[3];
int arrayPos = 0;
boolean filtersPresent = false;
if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null)
|| (disabled != null)) {
filtersPresent = true;
sql += " where";
}
if (filtersPresent) {
boolean firstWhereConditionAdded = false;
if (accountClassification != null) {
sql += " classification_enum = ?";
parameterArray[arrayPos] = accountClassification.shortValue();
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (StringUtils.isNotBlank(searchParam)) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
sql += " ( name like %?% or gl_code like %?% )";
parameterArray[arrayPos] = searchParam;
arrayPos = arrayPos + 1;
parameterArray[arrayPos] = searchParam;
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (usage != null) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
if (GLAccountUsage.HEADER.getValue().equals(usage)) {
sql += " account_usage = 2 ";
} else if (GLAccountUsage.DETAIL.getValue().equals(usage)) {
sql += " account_usage = 1 ";
}
firstWhereConditionAdded = true;
}
if (manualTransactionsAllowed != null) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed;
firstWhereConditionAdded = true;
}
if (disabled != null) {
if (firstWhereConditionAdded) {
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
}
@Override
public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) {
try {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
final StringBuilder sql = new StringBuilder();
sql.append("select ").append(rm.schema());
sql.append(" where gl.id = ?");
if (associationParametersData.isRunningBalanceRequired()) {
sql.append(" and gl_j.is_running_balance_calculated = true ")
.append(" ORDER BY gl_j.entry_date DESC,gl_j.id DESC LIMIT 1");
}
return this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
} catch (final EmptyResultDataAccessException e) {
throw new GLAccountNotFoundException(glAccountId, e);
}
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
private static boolean checkValidGLAccountType(final int type) {
for (final GLAccountType accountType : GLAccountType.values()) {
if (accountType.getValue().equals(type)) {
return true;
}
}
return false;
}
private static boolean checkValidGLAccountUsage(final int type) {
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) {
if (accountUsage.getValue().equals(type)) {
return true;
}
}
return false;
}
@Override
public GLAccountData retrieveNewGLAccountDetails(final Integer type) {
return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
}
@Override
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
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
}
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);
}
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.glaccount.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
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 final JdbcTemplate jdbcTemplate;
private static final String SQL_AND = " and ";
private static final class GLAccountMapper implements RowMapper<GLAccountData> {
private final JournalEntryAssociationParametersData associationParametersData;
GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) {
if (associationParametersData == null) {
this.associationParametersData = new JournalEntryAssociationParametersData();
} else {
this.associationParametersData = associationParametersData;
}
}
public String schema() {
StringBuilder sb = new StringBuilder();
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, ")
.append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ")
.append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ")
.append("cv.id as codeId, cv.code_value as codeValue ");
if (this.associationParametersData.isRunningBalanceRequired()) {
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 ");
if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append("left outer join acc_gl_journal_entry gl_j on gl_j.id = (")
.append("select id from acc_gl_journal_entry ")
.append("where account_id = gl.id ")
.append("order by id desc limit 1) ");
}
return sb.toString();
}
@Override
public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String name = rs.getString("name");
final Long parentId = JdbcSupport.getLong(rs, "parentId");
final String glCode = rs.getString("glCode");
final boolean disabled = rs.getBoolean("disabled");
final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final int usageId = JdbcSupport.getInteger(rs, "accountUsage");
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId);
final String description = rs.getString("description");
final String nameDecorated = rs.getString("nameDecorated");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
final String codeValue = rs.getString("codeValue");
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
Long organizationRunningBalance = null;
if (associationParametersData.isRunningBalanceRequired()) {
organizationRunningBalance = rs.getLong("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) {
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
if (usage != null && !checkValidGLAccountUsage(usage)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
String sql = "select " + rm.schema();
// append SQL statement for fetching account totals
if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) {
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 "
+ "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 "
+ "group by t2.account_id desc) t1)";
}
final Object[] parameterArray = new Object[3];
int arrayPos = 0;
boolean filtersPresent = false;
if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null)
|| (disabled != null)) {
filtersPresent = true;
sql += " where";
}
if (filtersPresent) {
boolean firstWhereConditionAdded = false;
if (accountClassification != null) {
sql += " classification_enum = ?";
parameterArray[arrayPos] = accountClassification.shortValue();
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (StringUtils.isNotBlank(searchParam)) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
sql += " ( name like ? or gl_code like ? )";
parameterArray[arrayPos] = "%" + searchParam + "%";
arrayPos = arrayPos + 1;
parameterArray[arrayPos] = "%" + searchParam + "%";
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (usage != null) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
if (GLAccountUsage.HEADER.getValue().equals(usage)) {
sql += " account_usage = 2 ";
} else if (GLAccountUsage.DETAIL.getValue().equals(usage)) {
sql += " account_usage = 1 ";
}
firstWhereConditionAdded = true;
}
if (manualTransactionsAllowed != null) {
if (firstWhereConditionAdded) {
sql += SQL_AND;
}
sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed;
firstWhereConditionAdded = true;
}
if (disabled != null) {
if (firstWhereConditionAdded) {
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
}
@Override
public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) {
try {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
final StringBuilder sql = new StringBuilder();
sql.append("select ").append(rm.schema());
sql.append(" where gl.id = ?");
final GLAccountData glAccount = this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
// Live balance: sum all non-reversed journal entries so cashier sees real-time balance
// 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 "
+ "WHERE account_id = ? AND (reversed = false OR reversed IS NULL)";
final java.math.BigDecimal liveBalance = this.jdbcTemplate.queryForObject(liveBalanceSql,
java.math.BigDecimal.class, glAccountId);
glAccount.setOrganizationRunningBalance(liveBalance != null ? liveBalance.longValue() : 0L);
}
return glAccount;
} catch (final EmptyResultDataAccessException e) {
throw new GLAccountNotFoundException(glAccountId, e);
}
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
private static boolean checkValidGLAccountType(final int type) {
for (final GLAccountType accountType : GLAccountType.values()) {
if (accountType.getValue().equals(type)) {
return true;
}
}
return false;
}
private static boolean checkValidGLAccountUsage(final int type) {
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) {
if (accountUsage.getValue().equals(type)) {
return true;
}
}
return false;
}
@Override
public GLAccountData retrieveNewGLAccountDetails(final Integer type) {
return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
}
@Override
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
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
}
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)
private Integer status;
@Column(name = "is_deleted", nullable = false)
private boolean deleted = false;
@OneToMany(mappedBy = "teller", fetch = FetchType.LAZY)
private Set<Cashier> cashiers;
@@ -161,4 +164,12 @@ public class Teller extends AbstractPersistableCustom<Long> {
this.office.getId();
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> {
Collection<Teller> findTellerByOfficeId(Long officeId);
long countByDeletedFalse();
}
@@ -36,6 +36,9 @@ public class TellerRepositoryWrapper {
@Transactional(readOnly = true)
public Teller findOneWithNotFoundDetection(final Long id) {
final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id));
if (teller.isDeleted()) {
throw new TellerNotFoundException(id);
}
teller.initializeLazyCollections();
return teller;
}
@@ -51,4 +54,8 @@ public class TellerRepositoryWrapper {
public void delete(final Teller 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"), //
REJECTED(3, "commandProcessingResultType.rejected"), //
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())
.collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v));
@@ -59,4 +60,8 @@ public enum CommandProcessingResultType {
public boolean isRejected() {
return this == REJECTED;
}
public boolean isTerminate() {
return this == TERMINATE;
}
}
@@ -71,6 +71,166 @@ public class CommandWrapperBuilder {
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() {
this.actionName = "UPDATE";
this.entityName = "ORGANISATIONCREDITBUREAU";
@@ -1611,6 +1771,14 @@ public class CommandWrapperBuilder {
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) {
this.actionName = "APPROVE";
this.entityName = "GSIMACCOUNT";
@@ -3565,7 +3733,7 @@ public class CommandWrapperBuilder {
this.actionName = "ACTIVATE";
this.entityName = "CURRENTACCOUNT";
this.savingsId = accountId;
this.entityId = null;
this.entityId = accountId;
this.href = "/currentaccounts/" + accountId + "?command=activate";
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.PROCESSED;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.TERMINATE;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson;
@@ -181,7 +182,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
currentSource.setResultStatusCode(SC_OK);
currentSource.updateForAudit(result);
currentSource.setResult(toApiResultJsonSerializer.serializeResult(result));
currentSource.setStatus(PROCESSED);
currentSource.setStatus("TERMINATE".equals(wrapper.getActionName()) ? TERMINATE : PROCESSED);
// Return saved command source
return commandSourceService.saveResultSameTransaction(currentSource);
@@ -1,116 +1,150 @@
/**
* 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.core.serialization;
import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.infrastructure.core.api.DateAdapter;
import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter;
import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter;
import org.apache.fineract.infrastructure.core.api.LocalDateAdapter;
import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy;
import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
import org.springframework.stereotype.Service;
/**
* Helper class for serialization of Java objects into JSON using Google's GSON.
*/
@Service
public final class GoogleGsonSerializerHelper {
public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) {
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters);
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
return builder.create();
}
public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
final Set<String> responseParameters) {
final Set<String> parameterNamesToSkip = new HashSet<>();
if (!responseParameters.isEmpty()) {
// strip out all known support parameters from expected response to
// see if unsupported parameters requested for response.
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
differentParametersDetectedSet.removeAll(supportedParameters);
if (!differentParametersDetectedSet.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet));
}
parameterNamesToSkip.addAll(supportedParameters);
parameterNamesToSkip.removeAll(responseParameters);
}
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip);
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
return builder.create();
}
public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects);
}
public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) {
return serializer.toJson(singleDataObject);
}
public static Gson createSimpleGson() {
return createGsonBuilder().create();
}
public static GsonBuilder createGsonBuilder() {
final GsonBuilder builder = new GsonBuilder();
registerTypeAdapters(builder);
return builder;
}
public static void registerTypeAdapters(final GsonBuilder builder) {
builder.registerTypeAdapter(java.util.Date.class, new DateAdapter());
builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter());
// 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());
}
}
/**
* 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.core.serialization;
import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.infrastructure.core.api.DateAdapter;
import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter;
import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter;
import org.apache.fineract.infrastructure.core.api.LocalDateAdapter;
import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy;
import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
import org.springframework.stereotype.Service;
/**
* Helper class for serialization of Java objects into JSON using Google's GSON.
*/
@Service
public final class GoogleGsonSerializerHelper {
public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) {
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters);
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
return builder.create();
}
public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
final Set<String> responseParameters) {
final Set<String> parameterNamesToSkip = new HashSet<>();
if (!responseParameters.isEmpty()) {
// strip out all known support parameters from expected response to
// see if unsupported parameters requested for response.
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
differentParametersDetectedSet.removeAll(supportedParameters);
if (!differentParametersDetectedSet.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet));
}
parameterNamesToSkip.addAll(supportedParameters);
parameterNamesToSkip.removeAll(responseParameters);
}
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip);
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
return builder.create();
}
public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects);
}
public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) {
return serializer.toJson(singleDataObject);
}
public static Gson createSimpleGson() {
return createGsonBuilder().create();
}
public static GsonBuilder createGsonBuilder() {
final GsonBuilder builder = new GsonBuilder();
registerTypeAdapters(builder);
return builder;
}
public static void registerTypeAdapters(final GsonBuilder builder) {
builder.registerTypeAdapter(java.util.Date.class, new DateAdapter());
builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter());
// 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"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), //
IMPS_DEEMED_RECONCILE("IMPS DEEMED Reconciliation"), //
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
; //
private final String name;
@@ -1,71 +1,79 @@
/**
* 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;
public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), //
;
private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) {
switch (type) {
case 1:
enumType = LOAN;
break;
case 2:
enumType = SAVINGS;
break;
default:
enumType = INVALID;
}
}
return enumType;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isSavingsAccount() {
return this.equals(SAVINGS);
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isLoanAccount() {
return this.equals(LOAN);
}
}
/**
* 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;
public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), //
CURRENT(3, "accountType.current"), //
;
private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) {
switch (type) {
case 1:
enumType = LOAN;
break;
case 2:
enumType = SAVINGS;
break;
case 3:
enumType = CURRENT;
break;
default:
enumType = INVALID;
}
}
return enumType;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isSavingsAccount() {
return this.equals(SAVINGS);
}
// 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
*/
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,
interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName,
@@ -512,6 +512,11 @@ public final class DepositsApiConstants {
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
// Exact calendar-based calculation params
fixedDepositInterestCalculationData.add("submittedOnDate");
fixedDepositInterestCalculationData.add("dateFormat");
fixedDepositInterestCalculationData.add("locale");
fixedDepositInterestCalculationData.add("daysInYear");
return fixedDepositInterestCalculationData;
@@ -170,6 +170,7 @@ public final class SavingsAccountData implements Serializable {
private Boolean balanceSwapOption;
private Boolean msme;
private Boolean autoDebitFacility;
private Boolean terminateTrue;
private String avgBalanceCalcPeriod;
private String validForPeriod;
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"), //
CLOSED(600, "savingsAccountStatusType.closed"), //
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 String code;
@@ -53,6 +54,7 @@ public enum SavingsAccountStatusType {
case 600 -> SavingsAccountStatusType.CLOSED;
case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE;
case 800 -> SavingsAccountStatusType.MATURED;
case 900 -> SavingsAccountStatusType.TERMINATED;
default -> SavingsAccountStatusType.INVALID;
};
}
@@ -113,4 +115,8 @@ public enum SavingsAccountStatusType {
public boolean isPreMatureClosure() {
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,
isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured);
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;
}
@@ -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.staff.domain.Staff;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@Table(name = "m_current_account", uniqueConstraints = {
@@ -102,6 +103,30 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "closedon_date")
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)
@Column(name = "currency_code", nullable = false, length = 3)
private String currencyCode;
@@ -304,6 +329,12 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "od_account_closed_zero_balance_in_day_closing")
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
@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 approve(LocalDate approvedOnDate) {
public void approve(LocalDate approvedOnDate, AppUser approvedOnByUser) {
this.statusEnum = 200;
this.approvedOnDate = approvedOnDate;
this.approvedOnByUser = approvedOnByUser;
}
public void activate(LocalDate activatedOnDate) {
public void activate(LocalDate activatedOnDate, AppUser activatedOnByUser) {
this.statusEnum = 300;
this.activatedOnDate = activatedOnDate;
this.activatedOnByUser = activatedOnByUser;
}
public void reject(LocalDate rejectedOnDate) {
public void reject(LocalDate rejectedOnDate, AppUser rejectedOnByUser) {
this.statusEnum = 500;
this.rejectedOnDate = rejectedOnDate;
this.rejectedOnByUser = rejectedOnByUser;
}
public void withdraw(LocalDate withdrawnOnDate) {
public void withdraw(LocalDate withdrawnOnDate, AppUser withdrawnOnByUser) {
this.statusEnum = 400;
this.withdrawnOnDate = withdrawnOnDate;
this.withdrawnOnByUser = withdrawnOnByUser;
}
public void close(LocalDate closedOnDate) {
public void close(LocalDate closedOnDate, AppUser closedOnByUser) {
this.statusEnum = 600;
this.closedOnDate = closedOnDate;
this.closedOnByUser = closedOnByUser;
}
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 setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = 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 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) {
Long productId = account.getProductId();
@@ -59,11 +84,10 @@ public class CurrentAccountNumberGenerator {
String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0');
if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2);
// Account ID — 5 digits
String accountId = StringUtils.leftPad(account.getId().toString(), 5, '0');
if (accountId.length() > 5) accountId = accountId.substring(accountId.length() - 5);
// Next sequence from existing account numbers
String nextSeq = getNextSequence();
return CBS_PREFIX + glCode + officeId + accountId;
return CBS_PREFIX + glCode + officeId + nextSeq;
}
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');
if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2);
// Next account ID estimate from m_current_account
Long maxId = jdbcTemplate.query(
"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);
// Next sequence from existing account numbers
String nextSeq = getNextSequence();
return CBS_PREFIX + glCode + officeStr + nextSeq;
}
}
@@ -26,6 +26,8 @@ public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId);
Map<String, Object> retrieveOneTransaction(Long accountId, Long transactionId);
List<Map<String, Object>> retrieveAll();
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 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 org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
public interface CurrentProductReadPlatformService {
@@ -28,4 +29,8 @@ public interface CurrentProductReadPlatformService {
Collection<SavingsProductData> retrieveAllForLookup();
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.CurrentProductFacilityLimit;
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.SavingsProductTempData;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
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
public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService {
@@ -58,6 +68,11 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository;
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 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 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.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.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("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.setValidForPeriod(rs.getString("validForPeriod"));
productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing"));
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
return productData;
}
@@ -28,4 +28,21 @@ public interface CurrentProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command);
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 org.apache.commons.lang3.exception.ExceptionUtils;
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.data.ApiParameterError;
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.exception.ErrorHandler;
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.portfolio.charge.domain.Charge;
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.CurrentProductFacilityLimitRepository;
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.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.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.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context;
private final CurrentProductRepository currentProductRepository;
private final CurrentProductDataValidator fromApiJsonDataValidator;
@@ -69,6 +91,13 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository;
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) {
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
@Override
public CommandProcessingResult delete(final Long productId) {
@@ -19,7 +19,14 @@
package org.apache.fineract.portfolio.currentproduct.starter;
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.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.domain.CurrentProductAssembler;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository;
@@ -43,9 +50,17 @@ public class CurrentProductConfiguration {
public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context,
JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) {
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository) {
return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository);
currentFacilityLimitRepository, currentDepositLimitKycRepository,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository,
pendingEditFacilityLimitRepository);
}
@Bean
@@ -56,9 +71,18 @@ public class CurrentProductConfiguration {
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
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,
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).
'dev.samstevens.totp:totp:1.7.1',
// Nimbus JOSE+JWT — RS256 sign/verify for the IMPS_BANK_API_SPEC_CBS_1.0 envelope
// (see org.apache.fineract.infrastructure.imps.crypto.ImpsJwtUtil).
'com.nimbusds:nimbus-jose-jwt:9.37',
'org.apache.commons:commons-lang3',
'commons-io:commons-io',
'org.apache.poi:poi',
+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);
}
/**
* 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.
*
@@ -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(" eao.external_id as externalAssetOwner, ")
.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 sa.account_no ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
.append(" ELSE NULL ")
.append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.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 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_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_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 ");
if (associationParametersData.isTransactionDetailsRequired()) {
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 ")
@@ -265,6 +264,10 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
int arrayPos = 0;
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)) {
sqlBuilder.append(whereClose).append(" journalEntry.transaction_id = ?");
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, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit "
+ "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(fromDate);
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, "
+ "COUNT(*) AS dw_count "
+ "FROM acc_gl_journal_entry je "
+ "WHERE 1=1");
+ "WHERE (je.reversed IS NULL OR je.reversed = false)");
if (glAccountId != null && glAccountId > 0) {
dwSql.append(" AND je.account_id = ?");
dwParams.add(glAccountId);
@@ -684,7 +687,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
final StringBuilder totSql = new StringBuilder(
"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 "
+ "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) {
totSql.append(" AND je.account_id = ?");
totParams.add(glAccountId);
@@ -723,7 +726,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
{
final List<Object> cntParams = new ArrayList<>();
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) {
cntSql.append(" AND je.account_id = ?");
cntParams.add(glAccountId);
@@ -146,6 +146,14 @@ public class SecurityConfig {
auth.requestMatchers(API_MATCHER.matcher(HttpMethod.OPTIONS, "/api/**")).permitAll()
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/echo")).permitAll()
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/authentication")).permitAll()
// 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,
// no Fineract auth required.
.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 {
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
} catch (final EmptyResultDataAccessException e) {
@@ -198,7 +198,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
hierarchySearchString = hierarchy + "%";
}
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
}
@@ -1,451 +1,394 @@
/**
* 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.service;
import jakarta.persistence.PersistenceException;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.staff.exception.StaffNotFoundException;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.organisation.teller.domain.Cashier;
import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.teller.domain.CashierTransaction;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.teller.domain.CashierTxnType;
import org.apache.fineract.organisation.teller.domain.Teller;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@AllArgsConstructor
@Slf4j
public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformService {
private final PlatformSecurityContext context;
private final TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final TellerRepositoryWrapper tellerRepositoryWrapper;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final StaffRepository staffRepository;
private final CashierRepository cashierRepository;
private final CashierTransactionRepository cashierTxnRepository;
private final JournalEntryRepository glJournalEntryRepository;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper;
private final CashierTransactionDataValidator cashierTransactionDataValidator;
@Override
@Transactional
public CommandProcessingResult createTeller(JsonCommand command) {
try {
this.context.authenticatedUser();
final Long officeId = command.longValueOfParameterNamed("officeId");
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
// final Office parent =
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final Teller teller = Teller.fromJson(tellerOffice, command);
// pre save to generate id for use in office hierarchy
this.tellerRepositoryWrapper.saveAndFlush(teller);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withOfficeId(teller.getOffice().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();
}
}
@Override
@Transactional
public CommandProcessingResult modifyTeller(Long tellerId, JsonCommand command) {
try {
final Long officeId = command.longValueOfParameterNamed("officeId");
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final AppUser currentUser = this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
final Teller teller = validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
final Map<String, Object> changes = teller.update(tellerOffice, command);
if (!changes.isEmpty()) {
this.tellerRepositoryWrapper.saveAndFlush(teller);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withOfficeId(teller.officeId()) //
.with(changes) //
.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();
}
}
/*
* 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) {
final Long userOfficeId = currentUser.getOffice().getId();
final Office userOffice = this.officeRepositoryWrapper.findOfficeHierarchy(userOfficeId);
final Teller tellerToReturn = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
final Long tellerOfficeId = tellerToReturn.officeId();
if (userOffice.doesNotHaveAnOfficeInHierarchyWithId(tellerOfficeId)) {
throw new NoAuthorizationException("User does not have sufficient priviledges to act on the provided office.");
}
return tellerToReturn;
}
@Override
@Transactional
public CommandProcessingResult deleteTeller(Long tellerId) {
// TODO Auto-generated method stub
Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
Set<Cashier> isTellerIdPresentInCashier = teller.getCashiers();
for (final Cashier tellerIdInCashier : isTellerIdPresentInCashier) {
if (tellerIdInCashier.getTeller().getId().toString().equalsIgnoreCase(tellerId.toString())) {
throw new CashierExistForTellerException(tellerId);
}
}
tellerRepositoryWrapper.delete(teller);
return new CommandProcessingResultBuilder() //
.withEntityId(teller.getId()) //
.build();
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
*/
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",
"name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.teller.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
@Override
public CommandProcessingResult allocateCashierToTeller(final Long tellerId, JsonCommand command) {
try {
this.context.authenticatedUser();
Long hourStartTime;
Long minStartTime;
Long hourEndTime;
Long minEndTime;
String startTime = " ";
String endTime = " ";
final Teller teller = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
final Office tellerOffice = teller.getOffice();
final Long staffId = command.longValueOfParameterNamed("staffId");
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
final Boolean isFullDay = command.booleanObjectValueOfParameterNamed("isFullDay");
if (!isFullDay) {
hourStartTime = command.longValueOfParameterNamed("hourStartTime");
minStartTime = command.longValueOfParameterNamed("minStartTime");
if (minStartTime == 0) {
startTime = hourStartTime.toString() + ":" + minStartTime.toString() + "0";
} else {
startTime = hourStartTime.toString() + ":" + minStartTime.toString();
}
hourEndTime = command.longValueOfParameterNamed("hourEndTime");
minEndTime = command.longValueOfParameterNamed("minEndTime");
if (minEndTime == 0) {
endTime = hourEndTime.toString() + ":" + minEndTime.toString() + "0";
} else {
endTime = hourEndTime.toString() + ":" + minEndTime.toString();
}
}
final Cashier cashier = Cashier.fromJson(tellerOffice, teller, staff, startTime, endTime, command);
this.cashierTransactionDataValidator.validateCashierAllowedDateAndTime(cashier, teller);
this.cashierRepository.save(cashier);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withSubEntityId(cashier.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();
}
}
@Override
@Transactional
public CommandProcessingResult updateCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
final Long staffId = command.longValueOfParameterNamed("staffId");
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
cashier.setStaff(staff);
// TODO - check if staff office and teller office match
final Map<String, Object> changes = cashier.update(command);
if (!changes.isEmpty()) {
this.cashierRepository.saveAndFlush(cashier);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(cashier.getTeller().getId()) //
.withSubEntityId(cashier.getId()) //
.with(changes) //
.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();
}
}
private Cashier validateUserPriviledgeOnCashierAndRetrieve(final AppUser currentUser, final Long tellerId, final Long cashierId) {
validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
return this.cashierRepository.findById(cashierId).orElse(null);
}
@Override
@Transactional
public CommandProcessingResult deleteCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
this.cashierRepository.delete(cashier);
} 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();
}
return new CommandProcessingResultBuilder() //
.withEntityId(cashierId) //
.build();
}
/*
* @Override public CommandProcessingResult inwardCashToCashier (final Long cashierId, final CashierTransaction
* cashierTxn) { CashierTxnType txnType = CashierTxnType.INWARD_CASH_TXN; // pre save to generate id for use in
* office hierarchy this.cashierTxnRepository.save(cashierTxn); }
*/
@Override
public CommandProcessingResult allocateCashToCashier(final Long cashierId, JsonCommand command) {
return doTransactionForCashier(cashierId, CashierTxnType.ALLOCATE, command); // For
// fund
// allocation
// to
// cashier
}
@Override
public CommandProcessingResult settleCashFromCashier(final Long cashierId, JsonCommand command) {
this.cashierTransactionDataValidator.validateSettleCashAndCashOutTransactions(cashierId, command);
return doTransactionForCashier(cashierId, CashierTxnType.SETTLE, command); // For
// fund
// settlement
// from
// cashier
}
private CommandProcessingResult doTransactionForCashier(final Long cashierId, final CashierTxnType txnType, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
final Cashier cashier = this.cashierRepository.findById(cashierId).orElseThrow(() -> new CashierNotFoundException(cashierId));
this.fromApiJsonDeserializer.validateForCashTxnForCashier(command.json());
// TODO: can we please remove this whole block?!? this is 20 lines of dead code!!!
final String entityType = command.stringValueOfParameterNamed("entityType");
if (entityType != null) {
if (entityType.equals("loan account")) {
// TODO : Check if loan account exists
// LoanAccount loan = null;
// if (loan == null) { throw new
// LoanAccountFoundException(entityId); }
} else if (entityType.equals("savings account")) {
// TODO : Check if loan account exists
// SavingsAccount savingsaccount = null;
// if (savingsaccount == null) { throw new
// SavingsAccountNotFoundException(entityId); }
}
if (entityType.equals("client")) {
// TODO: Check if client exists
// Client client = null;
// if (client == null) { throw new
// ClientNotFoundException(entityId); }
} else {
// TODO : Invalid type handling
}
}
final CashierTransaction cashierTxn = CashierTransaction.fromJson(cashier, command);
cashierTxn.setTxnType(txnType.getId());
this.cashierTxnRepository.save(cashierTxn);
// Pass the journal entries
FinancialActivityAccount mainVaultFinancialActivityAccount = this.financialActivityAccountRepositoryWrapper
.findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.CASH_AT_MAINVAULT.getValue());
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();
}
}
}
/**
* 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.service;
import jakarta.persistence.PersistenceException;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.staff.exception.StaffNotFoundException;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.organisation.teller.domain.Cashier;
import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.teller.domain.CashierTransaction;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.teller.domain.CashierTxnType;
import org.apache.fineract.organisation.teller.domain.Teller;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@AllArgsConstructor
@Slf4j
public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformService {
private final PlatformSecurityContext context;
private final TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final TellerRepositoryWrapper tellerRepositoryWrapper;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final StaffRepository staffRepository;
private final CashierRepository cashierRepository;
private final CashierTransactionRepository cashierTxnRepository;
private final CashierTransactionDataValidator cashierTransactionDataValidator;
@Override
@Transactional
public CommandProcessingResult createTeller(JsonCommand command) {
try {
this.context.authenticatedUser();
if (this.tellerRepositoryWrapper.existsAny()) {
throw new TellerAlreadyExistsException();
}
final Long officeId = command.longValueOfParameterNamed("officeId");
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
// final Office parent =
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final Teller teller = Teller.fromJson(tellerOffice, command);
// pre save to generate id for use in office hierarchy
this.tellerRepositoryWrapper.saveAndFlush(teller);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withOfficeId(teller.getOffice().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();
}
}
@Override
@Transactional
public CommandProcessingResult modifyTeller(Long tellerId, JsonCommand command) {
try {
final Long officeId = command.longValueOfParameterNamed("officeId");
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final AppUser currentUser = this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
final Teller teller = validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
final Map<String, Object> changes = teller.update(tellerOffice, command);
if (!changes.isEmpty()) {
this.tellerRepositoryWrapper.saveAndFlush(teller);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withOfficeId(teller.officeId()) //
.with(changes) //
.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();
}
}
/*
* 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) {
final Long userOfficeId = currentUser.getOffice().getId();
final Office userOffice = this.officeRepositoryWrapper.findOfficeHierarchy(userOfficeId);
final Teller tellerToReturn = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
final Long tellerOfficeId = tellerToReturn.officeId();
if (userOffice.doesNotHaveAnOfficeInHierarchyWithId(tellerOfficeId)) {
throw new NoAuthorizationException("User does not have sufficient priviledges to act on the provided office.");
}
return tellerToReturn;
}
@Override
@Transactional
public CommandProcessingResult deleteTeller(Long tellerId) {
Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
teller.delete();
tellerRepositoryWrapper.saveAndFlush(teller);
return new CommandProcessingResultBuilder() //
.withEntityId(teller.getId()) //
.build();
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
*/
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",
"name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.teller.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
@Override
public CommandProcessingResult allocateCashierToTeller(final Long tellerId, JsonCommand command) {
try {
this.context.authenticatedUser();
Long hourStartTime;
Long minStartTime;
Long hourEndTime;
Long minEndTime;
String startTime = " ";
String endTime = " ";
final Teller teller = this.tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
final Office tellerOffice = teller.getOffice();
final Long staffId = command.longValueOfParameterNamed("staffId");
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
final Boolean isFullDay = command.booleanObjectValueOfParameterNamed("isFullDay");
if (!isFullDay) {
hourStartTime = command.longValueOfParameterNamed("hourStartTime");
minStartTime = command.longValueOfParameterNamed("minStartTime");
if (minStartTime == 0) {
startTime = hourStartTime.toString() + ":" + minStartTime.toString() + "0";
} else {
startTime = hourStartTime.toString() + ":" + minStartTime.toString();
}
hourEndTime = command.longValueOfParameterNamed("hourEndTime");
minEndTime = command.longValueOfParameterNamed("minEndTime");
if (minEndTime == 0) {
endTime = hourEndTime.toString() + ":" + minEndTime.toString() + "0";
} else {
endTime = hourEndTime.toString() + ":" + minEndTime.toString();
}
}
final Cashier cashier = Cashier.fromJson(tellerOffice, teller, staff, startTime, endTime, command);
this.cashierTransactionDataValidator.validateCashierAllowedDateAndTime(cashier, teller);
this.cashierRepository.save(cashier);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(teller.getId()) //
.withSubEntityId(cashier.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();
}
}
@Override
@Transactional
public CommandProcessingResult updateCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForAllocateCashier(command.json());
final Long staffId = command.longValueOfParameterNamed("staffId");
final Staff staff = this.staffRepository.findById(staffId).orElseThrow(() -> new StaffNotFoundException(staffId));
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
cashier.setStaff(staff);
// TODO - check if staff office and teller office match
final Map<String, Object> changes = cashier.update(command);
if (!changes.isEmpty()) {
this.cashierRepository.saveAndFlush(cashier);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(cashier.getTeller().getId()) //
.withSubEntityId(cashier.getId()) //
.with(changes) //
.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();
}
}
private Cashier validateUserPriviledgeOnCashierAndRetrieve(final AppUser currentUser, final Long tellerId, final Long cashierId) {
validateUserPriviledgeOnTellerAndRetrieve(currentUser, tellerId);
return this.cashierRepository.findById(cashierId).orElse(null);
}
@Override
@Transactional
public CommandProcessingResult deleteCashierAllocation(Long tellerId, Long cashierId, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
final Cashier cashier = validateUserPriviledgeOnCashierAndRetrieve(currentUser, tellerId, cashierId);
this.cashierRepository.delete(cashier);
} 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();
}
return new CommandProcessingResultBuilder() //
.withEntityId(cashierId) //
.build();
}
/*
* @Override public CommandProcessingResult inwardCashToCashier (final Long cashierId, final CashierTransaction
* cashierTxn) { CashierTxnType txnType = CashierTxnType.INWARD_CASH_TXN; // pre save to generate id for use in
* office hierarchy this.cashierTxnRepository.save(cashierTxn); }
*/
@Override
public CommandProcessingResult allocateCashToCashier(final Long cashierId, JsonCommand command) {
return doTransactionForCashier(cashierId, CashierTxnType.ALLOCATE, command); // For
// fund
// allocation
// to
// cashier
}
@Override
public CommandProcessingResult settleCashFromCashier(final Long cashierId, JsonCommand command) {
this.cashierTransactionDataValidator.validateSettleCashAndCashOutTransactions(cashierId, command);
return doTransactionForCashier(cashierId, CashierTxnType.SETTLE, command); // For
// fund
// settlement
// from
// cashier
}
private CommandProcessingResult doTransactionForCashier(final Long cashierId, final CashierTxnType txnType, JsonCommand command) {
try {
final AppUser currentUser = this.context.authenticatedUser();
final Cashier cashier = this.cashierRepository.findById(cashierId).orElseThrow(() -> new CashierNotFoundException(cashierId));
this.fromApiJsonDeserializer.validateForCashTxnForCashier(command.json());
// TODO: can we please remove this whole block?!? this is 20 lines of dead code!!!
final String entityType = command.stringValueOfParameterNamed("entityType");
if (entityType != null) {
if (entityType.equals("loan account")) {
// TODO : Check if loan account exists
// LoanAccount loan = null;
// if (loan == null) { throw new
// LoanAccountFoundException(entityId); }
} else if (entityType.equals("savings account")) {
// TODO : Check if loan account exists
// SavingsAccount savingsaccount = null;
// if (savingsaccount == null) { throw new
// SavingsAccountNotFoundException(entityId); }
}
if (entityType.equals("client")) {
// TODO: Check if client exists
// Client client = null;
// if (client == null) { throw new
// ClientNotFoundException(entityId); }
} else {
// TODO : Invalid type handling
}
}
final CashierTransaction cashierTxn = CashierTransaction.fromJson(cashier, command);
cashierTxn.setTxnType(txnType.getId());
this.cashierTxnRepository.save(cashierTxn);
// GL entries skipped for vault-to-cashier (ALLOCATE) and cashier-to-vault (SETTLE)
// transfers. Cash is internal bank physical movement — denomination tracking is
// handled operationally via m_cash_transfer / m_cash_settlement tables.
// GL impact happens only on customer deposit/withdrawal transactions.
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
* 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.starter;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.staff.service.StaffReadPlatformService;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformService;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformServiceImpl;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformService;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformServiceJpaImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class OrganisationTellerConfiguration {
@Bean
@ConditionalOnMissingBean(TellerManagementReadPlatformService.class)
public TellerManagementReadPlatformService tellerManagementReadPlatformService(JdbcTemplate jdbcTemplate,
PlatformSecurityContext context, OfficeReadPlatformService officeReadPlatformService,
StaffReadPlatformService staffReadPlatformService, CurrencyReadPlatformService currencyReadPlatformService,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper, SqlValidator sqlValidator) {
return new TellerManagementReadPlatformServiceImpl(jdbcTemplate, context, officeReadPlatformService, staffReadPlatformService,
currencyReadPlatformService, sqlGenerator, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(TellerWritePlatformService.class)
public TellerWritePlatformService tellerWritePlatformService(PlatformSecurityContext context,
TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer, TellerRepositoryWrapper tellerRepositoryWrapper,
OfficeRepositoryWrapper officeRepositoryWrapper, StaffRepository staffRepository, CashierRepository cashierRepository,
CashierTransactionRepository cashierTxnRepository, JournalEntryRepository glJournalEntryRepository,
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper,
CashierTransactionDataValidator cashierTransactionDataValidator) {
return new TellerWritePlatformServiceJpaImpl(context, fromApiJsonDeserializer, tellerRepositoryWrapper, officeRepositoryWrapper,
staffRepository, cashierRepository, cashierTxnRepository, glJournalEntryRepository,
financialActivityAccountRepositoryWrapper, cashierTransactionDataValidator);
}
}
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.staff.service.StaffReadPlatformService;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
import org.apache.fineract.organisation.teller.domain.CashierRepository;
import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformService;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformServiceImpl;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformService;
import org.apache.fineract.organisation.teller.service.TellerWritePlatformServiceJpaImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class OrganisationTellerConfiguration {
@Bean
@ConditionalOnMissingBean(TellerManagementReadPlatformService.class)
public TellerManagementReadPlatformService tellerManagementReadPlatformService(JdbcTemplate jdbcTemplate,
PlatformSecurityContext context, OfficeReadPlatformService officeReadPlatformService,
StaffReadPlatformService staffReadPlatformService, CurrencyReadPlatformService currencyReadPlatformService,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper, SqlValidator sqlValidator) {
return new TellerManagementReadPlatformServiceImpl(jdbcTemplate, context, officeReadPlatformService, staffReadPlatformService,
currencyReadPlatformService, sqlGenerator, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(TellerWritePlatformService.class)
public TellerWritePlatformService tellerWritePlatformService(PlatformSecurityContext context,
TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer, TellerRepositoryWrapper tellerRepositoryWrapper,
OfficeRepositoryWrapper officeRepositoryWrapper, StaffRepository staffRepository, CashierRepository cashierRepository,
CashierTransactionRepository cashierTxnRepository,
CashierTransactionDataValidator cashierTransactionDataValidator) {
return new TellerWritePlatformServiceJpaImpl(context, fromApiJsonDeserializer, tellerRepositoryWrapper, officeRepositoryWrapper,
staffRepository, cashierRepository, cashierTxnRepository, 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
* 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.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry;
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.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
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.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions")
@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"
+ "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor
public class StandingInstructionApiResource {
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@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. "
+ "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"
+ "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType());
}
@POST
@Consumes({ 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")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@PUT
@Path("{standingInstructionId}")
@Consumes({ 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"
+ "\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"
+ "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null;
final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
}
@GET
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@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)))
public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template"));
}
if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
}
if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(),
standingInstructionData.getTransferType().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
}
}
return standingInstructionData;
}
}
/**
* 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.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry;
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.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
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.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions")
@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"
+ "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor
public class StandingInstructionApiResource {
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@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. "
+ "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"
+ "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType());
}
@POST
@Consumes({ 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")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@PUT
@Path("{standingInstructionId}")
@Consumes({ 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"
+ "\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"
+ "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null;
final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
}
@GET
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@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)))
public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template"));
}
if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
}
if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
}
}
return standingInstructionData;
}
}
@@ -1,79 +1,79 @@
/**
* 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.data;
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.fromClientIdParamName;
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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2));
}
}
/**
* 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.data;
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.fromClientIdParamName;
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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2), Integer.valueOf(3));
}
}
@@ -1,329 +1,359 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.ChronoField;
import java.util.Collection;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/**
* Immutable data object representing a savings account.
*/
@SuppressWarnings("unused")
public final class StandingInstructionData {
@Getter
private final Long id;
@Getter
private final Long accountDetailId;
@Getter
private final String name;
private final OfficeData fromOffice;
@Getter
private final ClientData fromClient;
private final EnumOptionData fromAccountType;
@Getter
private final PortfolioAccountData fromAccount;
private final OfficeData toOffice;
@Getter
private final ClientData toClient;
private final EnumOptionData toAccountType;
@Getter
private final PortfolioAccountData toAccount;
private final EnumOptionData transferType;
private final EnumOptionData priority;
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@Getter
private final BigDecimal amount;
@Getter
private final LocalDate validFrom;
private final LocalDate validTill;
private final EnumOptionData recurrenceType;
private final EnumOptionData recurrenceFrequency;
@Getter
private final Integer recurrenceInterval;
private final MonthDay recurrenceOnMonthDay;
private final Page<AccountTransferData> transactions;
private final Collection<OfficeData> fromOfficeOptions;
private final Collection<ClientData> fromClientOptions;
private final Collection<EnumOptionData> fromAccountTypeOptions;
private final Collection<PortfolioAccountData> fromAccountOptions;
private final Collection<OfficeData> toOfficeOptions;
private final Collection<ClientData> toClientOptions;
private final Collection<EnumOptionData> toAccountTypeOptions;
private final Collection<PortfolioAccountData> toAccountOptions;
private final Collection<EnumOptionData> transferTypeOptions;
private final Collection<EnumOptionData> statusOptions;
private final Collection<EnumOptionData> instructionTypeOptions;
private final Collection<EnumOptionData> priorityOptions;
private final Collection<EnumOptionData> recurrenceTypeOptions;
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final OfficeData fromOffice = null;
final OfficeData toOffice = null;
final ClientData fromClient = null;
final ClientData toClient = null;
final EnumOptionData fromAccountType = null;
final PortfolioAccountData fromAccount = null;
final EnumOptionData toAccountType = null;
final PortfolioAccountData toAccount = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = 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,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
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,
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
final Page<AccountTransferData> transactions = null;
final Collection<OfficeData> fromOfficeOptions = null;
final Collection<ClientData> fromClientOptions = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null;
final Collection<PortfolioAccountData> fromAccountOptions = null;
final Collection<OfficeData> toOfficeOptions = null;
final Collection<ClientData> toClientOptions = null;
final Collection<EnumOptionData> toAccountTypeOptions = null;
final Collection<PortfolioAccountData> toAccountOptions = null;
final Collection<EnumOptionData> transferTypeOptions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
final Collection<EnumOptionData> priorityOptions = null;
final Collection<EnumOptionData> recurrenceTypeOptions = null;
final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
}
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.id = id;
this.accountDetailId = accountDetailId;
this.name = name;
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromAccountType = fromAccountType;
this.fromAccount = fromAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toAccountType = toAccountType;
this.toAccount = toAccount;
this.transferType = transferType;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnMonthDay = recurrenceOnMonthDay;
this.fromOfficeOptions = fromOfficeOptions;
this.fromClientOptions = fromClientOptions;
this.fromAccountTypeOptions = fromAccountTypeOptions;
this.fromAccountOptions = fromAccountOptions;
this.toOfficeOptions = toOfficeOptions;
this.toClientOptions = toClientOptions;
this.toAccountTypeOptions = toAccountTypeOptions;
this.toAccountOptions = toAccountOptions;
this.transferTypeOptions = transferTypeOptions;
this.statusOptions = statusOptions;
this.instructionTypeOptions = instructionTypeOptions;
this.priorityOptions = priorityOptions;
this.recurrenceTypeOptions = recurrenceTypeOptions;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
this.transactions = transactions;
}
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = 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,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
public StandingInstructionType getInstructionType() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferRecurrenceType getRecurrenceType() {
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
}
public PeriodFrequencyType getRecurrenceFrequency() {
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getFromAccountType() {
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getToAccountType() {
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferType getTransferType() {
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 = getTransferType();
if (accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue();
}
return transferType;
}
}
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.ChronoField;
import java.util.Collection;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/**
* Immutable data object representing a savings account.
*/
@SuppressWarnings("unused")
public final class StandingInstructionData {
@Getter
private final Long id;
@Getter
private final Long accountDetailId;
@Getter
private final String name;
@Getter
private final OfficeData fromOffice;
@Getter
private final ClientData fromClient;
@Getter
private final EnumOptionData fromAccountType;
@Getter
private final PortfolioAccountData fromAccount;
@Getter
private final OfficeData toOffice;
@Getter
private final ClientData toClient;
@Getter
private final EnumOptionData toAccountType;
@Getter
private final PortfolioAccountData toAccount;
@Getter
private final EnumOptionData transferType;
@Getter
private final EnumOptionData priority;
@Getter
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@Getter
private final BigDecimal amount;
@Getter
private final LocalDate validFrom;
@Getter
private final LocalDate validTill;
@Getter
private final EnumOptionData recurrenceType;
@Getter
private final EnumOptionData recurrenceFrequency;
@Getter
private final Integer recurrenceInterval;
@Getter
private final MonthDay recurrenceOnMonthDay;
@Getter
private final Page<AccountTransferData> transactions;
@Getter
private final Collection<OfficeData> fromOfficeOptions;
@Getter
private final Collection<ClientData> fromClientOptions;
@Getter
private final Collection<EnumOptionData> fromAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> fromAccountOptions;
@Getter
private final Collection<OfficeData> toOfficeOptions;
@Getter
private final Collection<ClientData> toClientOptions;
@Getter
private final Collection<EnumOptionData> toAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> toAccountOptions;
@Getter
private final Collection<EnumOptionData> transferTypeOptions;
@Getter
private final Collection<EnumOptionData> statusOptions;
@Getter
private final Collection<EnumOptionData> instructionTypeOptions;
@Getter
private final Collection<EnumOptionData> priorityOptions;
@Getter
private final Collection<EnumOptionData> recurrenceTypeOptions;
@Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final OfficeData fromOffice = null;
final OfficeData toOffice = null;
final ClientData fromClient = null;
final ClientData toClient = null;
final EnumOptionData fromAccountType = null;
final PortfolioAccountData fromAccount = null;
final EnumOptionData toAccountType = null;
final PortfolioAccountData toAccount = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = 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,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
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,
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
final Page<AccountTransferData> transactions = null;
final Collection<OfficeData> fromOfficeOptions = null;
final Collection<ClientData> fromClientOptions = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null;
final Collection<PortfolioAccountData> fromAccountOptions = null;
final Collection<OfficeData> toOfficeOptions = null;
final Collection<ClientData> toClientOptions = null;
final Collection<EnumOptionData> toAccountTypeOptions = null;
final Collection<PortfolioAccountData> toAccountOptions = null;
final Collection<EnumOptionData> transferTypeOptions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
final Collection<EnumOptionData> priorityOptions = null;
final Collection<EnumOptionData> recurrenceTypeOptions = null;
final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
}
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.id = id;
this.accountDetailId = accountDetailId;
this.name = name;
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromAccountType = fromAccountType;
this.fromAccount = fromAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toAccountType = toAccountType;
this.toAccount = toAccount;
this.transferType = transferType;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnMonthDay = recurrenceOnMonthDay;
this.fromOfficeOptions = fromOfficeOptions;
this.fromClientOptions = fromClientOptions;
this.fromAccountTypeOptions = fromAccountTypeOptions;
this.fromAccountOptions = fromAccountOptions;
this.toOfficeOptions = toOfficeOptions;
this.toClientOptions = toClientOptions;
this.toAccountTypeOptions = toAccountTypeOptions;
this.toAccountOptions = toAccountOptions;
this.transferTypeOptions = transferTypeOptions;
this.statusOptions = statusOptions;
this.instructionTypeOptions = instructionTypeOptions;
this.priorityOptions = priorityOptions;
this.recurrenceTypeOptions = recurrenceTypeOptions;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
this.transactions = transactions;
}
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = 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,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
/**
* Returns the domain enum for instruction type — used internally by business logic.
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
* return EnumOptionData so Jackson serializes it as an object, not a plain string).
*/
public StandingInstructionType instructionTypeEnum() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferRecurrenceType recurrenceTypeEnum() {
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
}
public PeriodFrequencyType recurrenceFrequencyEnum() {
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
}
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
* 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.data;
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.transferTypeParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
}
public void validateForCreate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4);
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
boolean isPeriodic = false;
if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
}
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull();
}
}
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull();
}
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().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();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
if (accountTransferType.isAccountTransfer()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final JsonCommand command) {
final String json = command.json();
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 List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
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);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
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());
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());
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());
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());
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());
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());
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();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
/**
* 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.data;
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.transferTypeParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
}
public void validateForCreate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4);
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
boolean isPeriodic = false;
if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
}
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull();
}
}
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull();
}
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
|| PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().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();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
if (accountTransferType.isAccountTransfer()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount()
|| (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final JsonCommand command) {
final String json = command.json();
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 List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
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);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
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());
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());
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());
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());
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());
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());
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();
}
throwExceptionIfValidationWarningsExist(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
* 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.domain;
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.fromOfficeIdParamName;
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.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountTransferDetailAssembler {
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler;
@Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) {
this.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount) {
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.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Loan toLoanAccount) {
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.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transfertype);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final SavingsAccount toSavingsAccount) {
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.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transferType);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transferType);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transferType);
}
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
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);
}
}
/**
* 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.domain;
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.fromOfficeIdParamName;
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.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountTransferDetailAssembler {
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler;
private final CurrentAccountRepository currentAccountRepository;
@Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
final CurrentAccountRepository currentAccountRepository) {
this.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
this.currentAccountRepository = currentAccountRepository;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount) {
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.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Loan toLoanAccount) {
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.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transfertype);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final SavingsAccount toSavingsAccount) {
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.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transferType);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transferType);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transferType);
}
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
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
* 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.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@Column(name = "transfer_type")
private Integer transferType;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null,
transferType, null);
}
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount,
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,
transferType, null);
}
protected AccountTransferDetails() {
//
}
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public SavingsAccount toSavingsAccount() {
return this.toSavingsAccount;
}
public SavingsAccount fromSavingsAccount() {
return this.fromSavingsAccount;
}
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
this.accountTransferTransactions.add(accountTransferTransaction);
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public Loan toLoanAccount() {
return this.toLoanAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount;
}
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, toOffice, toClient, null, toLoanAccount,
transferType, null);
}
public List<AccountTransferTransaction> getAccountTransferTransactions() {
return accountTransferTransactions;
}
}
/**
* 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.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@ManyToOne
@JoinColumn(name = "from_current_account_id", nullable = true)
private CurrentAccount fromCurrentAccount;
@ManyToOne
@JoinColumn(name = "to_current_account_id", nullable = true)
private CurrentAccount toCurrentAccount;
@Column(name = "transfer_type")
private Integer transferType;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
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,
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, null, toOffice, toClient, toSavingsAccount, null,
null, transferType, null);
}
protected AccountTransferDetails() {
//
}
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
this.toCurrentAccount = toCurrentAccount;
this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public SavingsAccount toSavingsAccount() {
return this.toSavingsAccount;
}
public SavingsAccount fromSavingsAccount() {
return this.fromSavingsAccount;
}
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
this.accountTransferTransactions.add(accountTransferTransaction);
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public Loan toLoanAccount() {
return this.toLoanAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount;
}
public CurrentAccount fromCurrentAccount() {
return this.fromCurrentAccount;
}
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
* 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.domain;
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.amountParamName;
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.recurrenceFrequencyParamName;
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.recurrenceTypeParamName;
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.validTillParamName;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails;
@Column(name = "name")
private String name;
@Column(name = "priority")
private Integer priority;
@Column(name = "instruction_type")
private Integer instructionType;
@Column(name = "status")
private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount;
@Column(name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_till")
private LocalDate validTill;
@Column(name = "recurrence_type")
private Integer recurrenceType;
@Column(name = "recurrence_frequency")
private Integer recurrenceFrequency;
@Column(name = "recurrence_interval")
private Integer recurrenceInterval;
@Column(name = "recurrence_on_day")
private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth;
@Column(name = "last_run_date")
private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() {
}
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 LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
}
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
}
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
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 Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails;
this.name = name;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
}
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom);
}
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill);
}
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue);
this.amount = newValue;
}
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue);
this.status = newValue;
}
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue);
this.priority = newValue;
}
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue;
}
if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue;
}
final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear;
}
}
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue;
}
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
return actualChanges;
}
private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
}
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
}
}
}
if (this.accountTransferDetails.toSavingsAccount() != 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();
}
String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) {
errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
public void updateLatsRunDate(LocalDate latsRunDate) {
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();
}
}
/**
* 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.domain;
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.amountParamName;
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.recurrenceFrequencyParamName;
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.recurrenceTypeParamName;
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.validTillParamName;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails;
@Column(name = "name")
private String name;
@Column(name = "priority")
private Integer priority;
@Column(name = "instruction_type")
private Integer instructionType;
@Column(name = "status")
private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount;
@Column(name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_till")
private LocalDate validTill;
@Column(name = "recurrence_type")
private Integer recurrenceType;
@Column(name = "recurrence_frequency")
private Integer recurrenceFrequency;
@Column(name = "recurrence_interval")
private Integer recurrenceInterval;
@Column(name = "recurrence_on_day")
private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth;
@Column(name = "last_run_date")
private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() {
}
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 LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
}
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
}
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
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 Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails;
this.name = name;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
}
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom);
}
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill);
}
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue);
this.amount = newValue;
}
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue);
this.status = newValue;
}
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue);
this.priority = newValue;
}
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue;
}
if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue;
}
final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear;
}
}
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue;
}
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
return actualChanges;
}
private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
}
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
}
}
}
// For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
if (this.accountTransferDetails.toSavingsAccount() != null
|| 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();
}
// Account Transfer: from must be savings OR current; to must be savings OR current
boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
|| this.accountTransferDetails.toCurrentAccount() != null;
// Loan Repayment: from must be savings OR current; to must be loan
boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& !(fromAccountValid && toAccountValid)) {
errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
public void updateLatsRunDate(LocalDate latsRunDate) {
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
* 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.domain;
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.nameParamName;
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.recurrenceIntervalParamName;
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.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StandingInstructionAssembler {
private final AccountTransferDetailAssembler accountTransferDetailAssembler;
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
this.accountTransferDetailAssembler = accountTransferDetailAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) {
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount);
amount = monetaryAmount.getAmount();
}
final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final String name = command.stringValueOfParameterNamed(nameParamName);
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
}
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;
}
}
/**
* 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.domain;
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.nameParamName;
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.recurrenceIntervalParamName;
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.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StandingInstructionAssembler {
private final AccountTransferDetailAssembler accountTransferDetailAssembler;
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
this.accountTransferDetailAssembler = accountTransferDetailAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) {
final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
if (currency != null) {
final Money monetaryAmount = Money.of(currency, transferAmount);
amount = monetaryAmount.getAmount();
} else {
amount = transferAmount;
}
}
final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final String name = command.stringValueOfParameterNamed(nameParamName);
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
}
/**
* 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
* 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.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService);
}
}
/**
* 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.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Autowired
private AccountTransferDetailRepository accountTransferDetailRepository;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@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
* 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.jobs.executestandinginstructions;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType();
StandingInstructionType instructionType = data.getInstructionType();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency();
LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.getAmount();
if (data.getToAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId());
if (data.getInstructionType().isDuesAmoutTransfer()) {
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;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount,execution_time, error_log) VALUES (");
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
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()));
}
}
/**
* 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.jobs.executestandinginstructions;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final AccountTransferDetailRepository accountTransferDetailRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
StandingInstructionType instructionType = data.instructionTypeEnum();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.getAmount();
if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId());
if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
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;
// Load existing AccountTransferDetails so transactions are linked to
// this standing instruction and appear in its transaction history
final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
: null;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
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 (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount,execution_time, error_log) VALUES (");
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
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
* 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.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations {
private AccountTransferEnumerations() {
}
public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type));
}
public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account");
break;
case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break;
}
}
return optionData;
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
}
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break;
case AS_PER_DUES:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
break;
}
}
return optionData;
}
public static EnumOptionData transferType(final Integer type) {
return transferType(AccountTransferType.fromInt(type));
}
public static EnumOptionData transferType(final AccountTransferType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break;
case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break;
case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break;
case LOAN_DOWN_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionPriority(final Integer type) {
return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
}
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.HIGH.getCode(), "High Priority");
break;
case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority");
break;
case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break;
case URGENT:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionStatus(final Integer type) {
return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
}
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.ACTIVE.getCode(), "Active");
break;
case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted");
break;
case DISABLED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionType(final Integer type) {
return standingInstructionType(StandingInstructionType.fromInt(type));
}
public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case DUES:
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.DUES.getCode(), "Dues");
break;
case FIXED:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed");
break;
}
}
return optionData;
}
}
/**
* 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.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations {
private AccountTransferEnumerations() {
}
public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type));
}
public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account");
break;
case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break;
case CURRENT:
optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
PortfolioAccountType.CURRENT.getCode(), "Current Account");
break;
}
}
return optionData;
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
}
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break;
case AS_PER_DUES:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
break;
}
}
return optionData;
}
public static EnumOptionData transferType(final Integer type) {
return transferType(AccountTransferType.fromInt(type));
}
public static EnumOptionData transferType(final AccountTransferType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break;
case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break;
case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break;
case LOAN_DOWN_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionPriority(final Integer type) {
return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
}
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.HIGH.getCode(), "High Priority");
break;
case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority");
break;
case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break;
case URGENT:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionStatus(final Integer type) {
return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
}
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.ACTIVE.getCode(), "Active");
break;
case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted");
break;
case DISABLED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionType(final Integer type) {
return standingInstructionType(StandingInstructionType.fromInt(type));
}
public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case DUES:
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.DUES.getCode(), "Dues");
break;
case FIXED:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed");
break;
}
}
return optionData;
}
}
@@ -1,396 +1,482 @@
/**
* 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 java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate;
// mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
}
@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) {
Object[] sqlParams = new Object[] { accountId };
PortfolioAccountData accountData = null;
try {
String sql = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
if (currencyCode != null) {
sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
if (currencyCode != null) {
sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break;
}
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
final List<Object> sqlParams = new ArrayList<>();
// sqlParams.add(portfolioAccountDTO.getClientId());
Collection<PortfolioAccountData> accounts = null;
String sql = null;
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) {
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " la.loan_status_id in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and la.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
if (portfolioAccountDTO.getDepositType() != null) {
sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
}
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
}
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
}
return accounts;
}
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioSavingsAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("sp.id as productId, sp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.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_savings_account sa ");
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
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 ");
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 ");
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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioLoanAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
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, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
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 ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
private String schemaSql;
private final DatabaseSpecificSQLGenerator sqlGenerator;
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
this.sqlGenerator = sqlGenerator;
}
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400);
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
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");
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)) + ");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
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, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append(amountQueryString.toString());
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
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 ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
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;
}
}
/**
* 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 java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate;
// mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
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, final String currencyCode) {
Object[] sqlParams = new Object[] { accountId };
PortfolioAccountData accountData = null;
try {
String sql = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
if (currencyCode != null) {
sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
if (currencyCode != null) {
sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
if (currencyCode != null) {
sql += " and ca.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
break;
}
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
final List<Object> sqlParams = new ArrayList<>();
// sqlParams.add(portfolioAccountDTO.getClientId());
Collection<PortfolioAccountData> accounts = null;
String sql = null;
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) {
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " la.loan_status_id in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and la.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
if (portfolioAccountDTO.getDepositType() != null) {
sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
}
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
}
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " ca.client_id = ? and ca.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " ca.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and ca.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
break;
}
return accounts;
}
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioSavingsAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("sp.id as productId, sp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.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_savings_account sa ");
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
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 ");
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 ");
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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioLoanAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
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, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
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 ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
private String schemaSql;
private final DatabaseSpecificSQLGenerator sqlGenerator;
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
this.sqlGenerator = sqlGenerator;
}
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400);
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
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");
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)) + ");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final StringBuilder sqlBuilder = new StringBuilder(400);
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, ");
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, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append(amountQueryString.toString());
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
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 ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
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 = rs.getLong("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 inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
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
* 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 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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository;
@Transactional
@Override
public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null;
try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
}
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
return builder.build();
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("name")) {
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
}
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
@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();
}
}
/**
* 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 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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository;
@Transactional
@Override
public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null;
try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
}
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
return builder.build();
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("name")) {
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
}
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
}
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
* 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.starter;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class AccountConfiguration {
@Bean
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties);
}
@Bean
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator) {
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionWritePlatformService.class)
public StandingInstructionWritePlatformService standingInstructionWritePlatformService(
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) {
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository);
}
}
/**
* 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.starter;
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class AccountConfiguration {
@Bean
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository,
AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
}
@Bean
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator) {
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
}
@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;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
@@ -44,12 +45,28 @@ public class GuarantorAccountSummaryData {
private final Boolean isActive;
private final String relationship;
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,
final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus,
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 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.accountNo = accountNo;
this.externalId = externalId;
@@ -66,5 +83,17 @@ public class GuarantorAccountSummaryData {
this.isActive = isActive;
this.relationship = relationship;
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