Compare commits

...
32 Commits
Author SHA1 Message Date
nitesh cf60515bf6 Merge remote-tracking branch 'nexora/uate' into merge-uate-into-dev
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 1 of 10) (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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (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-5) (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 - PostgreSQL / test (test-core-3) (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-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (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-4) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (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-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / 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
# Conflicts:
#	.gitignore
#	fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
#	fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FacilityChargeAutoApplyService.java
2026-07-23 17:36:10 +05:30
niteshandClaude Sonnet 5 a098f8ba75 fix(chequebook): prevent cheque-leaf serial collisions across series at issuance time
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 - 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 - 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 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 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 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 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
The real root cause behind the leaf-serial collision bug: stock-lot
overlap checking was scoped to "within the same series only"
(ChequeStockLotRepository.countOverlappingInSeries), on the incorrect
assumption that two different cheque designs never share a numeric
range. A real bank allocates leaf serials from one bank-wide numbering
authority, so this is exactly what let two series end up with
overlapping ranges that, once both were issued to the same account,
produced an unresolvable serial collision (NpciInwardPushService's
drawer-account-number disambiguation can't help when every colliding
leaf belongs to the same account).

Widened the check to countOverlappingAnySeries — both the maker-submit
and checker-approval paths in ChequeBookWritePlatformServiceImpl now
reject a stock receipt whose range overlaps ANY existing lot, regardless
of series.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:24:17 +05:30
Jaanu423andClaude Sonnet 5 ab5666c0f4 feat(ctsclearing): signature-verification gate and full NPCI field capture for inward cheques
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 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 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 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 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 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 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 - 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 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
Adds a manual signature-verification step to the inward CTS decision flow:
a new VERIFY_SIGNATURE command records the clerk's confirmation, and
InwardClearingItem.pass() now refuses to pass a cheque until signatureOk
is true — enforced server-side, not just a disabled UI button.

Fixes a real bug where ChequeLeaf.leafSerialNo collides across different
cheque books/series (by design — serials are only unique per stock lot),
which could resolve an inward item to the wrong account. NpciInwardPushService
now disambiguates using the drawer account number NPCI already sends
alongside the cheque number.

Also closes a completeness gap found by auditing the outward-CBS -> NPCI ->
inward-CBS pipeline: five fields NPCI sends on every instrument (drawer
account number, deposit account number, instrument type/description, drawee
bank name, per-check IQA breakdown) were read and discarded instead of
persisted — now saved via new columns and exposed on the read API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 12:30:07 +05:30
Jaanu423andClaude Sonnet 5 b433edfdfe feat(clearing): add CTS/NACH clearing modules with real NPCI integration
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 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 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 - 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 - 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 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
- Inward/outward CTS clearing (capture, validate, settle, dishonour), positive pay,
  settlement/reconciliation, and NACH clearing modules
- Real, separate NPCI clearing-house integration: hybrid RSA+AES envelope crypto,
  bank self-registration, outward batch submission, and an inward push webhook
- Outward clearing is now session-batched: a 2-hourly scheduled job (plus the
  existing manual "Send Pending Batch" fallback) bundles CAPTURED cheques instead
  of sending them one at a time
- Batch-summary read APIs and date/text filters across inward batches, outward
  presentments, and outward batches

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:46:52 +05:30
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
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
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
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
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
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
390 changed files with 31535 additions and 2984 deletions
+1
View File
@@ -36,4 +36,5 @@ gradleExp/
.windsurf/
# Application logs
fineract-provider/logs/
@@ -71,6 +71,344 @@ 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;
}
// ===== Clearing (CTS + NACH) command wrappers =====
public CommandWrapperBuilder createClearingSettlementBatch() {
this.actionName = "CREATE";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = null;
this.href = "/clearing/settlement-batches";
return this;
}
public CommandWrapperBuilder postClearingSettlementBatch(final Long settlementBatchId) {
this.actionName = "POST";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = settlementBatchId;
this.href = "/clearing/settlement-batches/" + settlementBatchId;
return this;
}
public CommandWrapperBuilder rejectClearingSettlementBatch(final Long settlementBatchId) {
this.actionName = "REJECT";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = settlementBatchId;
this.href = "/clearing/settlement-batches/" + settlementBatchId;
return this;
}
public CommandWrapperBuilder createClearingReconciliation() {
this.actionName = "CREATE";
this.entityName = "CLEARINGRECONCILIATION";
this.entityId = null;
this.href = "/clearing/reconciliations";
return this;
}
public CommandWrapperBuilder createInwardClearingBatch() {
this.actionName = "CREATE";
this.entityName = "INWARDCLEARINGBATCH";
this.entityId = null;
this.href = "/ctsclearing/inward-batches";
return this;
}
public CommandWrapperBuilder validateInwardClearingBatch(final Long batchId) {
this.actionName = "VALIDATE";
this.entityName = "INWARDCLEARINGBATCH";
this.entityId = batchId;
this.href = "/ctsclearing/inward-batches/" + batchId;
return this;
}
public CommandWrapperBuilder passInwardClearingItem(final Long itemId) {
this.actionName = "PASS";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder returnInwardClearingItem(final Long itemId) {
this.actionName = "RETURN";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder verifyInwardClearingItemSignature(final Long itemId) {
this.actionName = "VERIFY_SIGNATURE";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder createPositivePayRegistration() {
this.actionName = "CREATE";
this.entityName = "POSITIVEPAYREGISTRATION";
this.entityId = null;
this.href = "/ctsclearing/positive-pay";
return this;
}
public CommandWrapperBuilder cancelPositivePayRegistration(final Long registrationId) {
this.actionName = "CANCEL";
this.entityName = "POSITIVEPAYREGISTRATION";
this.entityId = registrationId;
this.href = "/ctsclearing/positive-pay/" + registrationId;
return this;
}
public CommandWrapperBuilder sendPendingOutwardClearingBatch() {
this.actionName = "SENDBATCH";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = null;
this.href = "/ctsclearing/outward-presentments/send-pending-batch";
return this;
}
public CommandWrapperBuilder createOutwardClearingPresentment() {
this.actionName = "CREATE";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = null;
this.href = "/ctsclearing/outward-presentments";
return this;
}
public CommandWrapperBuilder sendOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "SEND";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder settleOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "SETTLE";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder dishonourOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "DISHONOUR";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder createNachMandate() {
this.actionName = "CREATE";
this.entityName = "NACHMANDATE";
this.entityId = null;
this.href = "/nachclearing/mandates";
return this;
}
public CommandWrapperBuilder cancelNachMandate(final Long mandateId) {
this.actionName = "CANCEL";
this.entityName = "NACHMANDATE";
this.entityId = mandateId;
this.href = "/nachclearing/mandates/" + mandateId;
return this;
}
public CommandWrapperBuilder createNachBatch() {
this.actionName = "CREATE";
this.entityName = "NACHBATCH";
this.entityId = null;
this.href = "/nachclearing/batches";
return this;
}
public CommandWrapperBuilder processNachBatch(final Long batchId) {
this.actionName = "PROCESS";
this.entityName = "NACHBATCH";
this.entityId = batchId;
this.href = "/nachclearing/batches/" + batchId;
return this;
}
public CommandWrapperBuilder passNachTransaction(final Long transactionId) {
this.actionName = "PASS";
this.entityName = "NACHTRANSACTION";
this.entityId = transactionId;
this.href = "/nachclearing/transactions/" + transactionId;
return this;
}
public CommandWrapperBuilder returnNachTransaction(final Long transactionId) {
this.actionName = "RETURN";
this.entityName = "NACHTRANSACTION";
this.entityId = transactionId;
this.href = "/nachclearing/transactions/" + transactionId;
return this;
}
public CommandWrapperBuilder updateCreditBureau() {
this.actionName = "UPDATE";
this.entityName = "ORGANISATIONCREDITBUREAU";
@@ -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;
}
};
}
});
}
}
@@ -64,6 +64,7 @@ public enum JobName {
IMPS_DEEMED_RECONCILE("IMPS DEEMED Reconciliation"), //
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
; //
private final String name;
@@ -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));
}
}
@@ -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);
@@ -146,11 +146,18 @@ 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()
// NPCI clearing-house receiver — the real, separate NPCI service pushes routed
// packages here without a Fineract Bearer token; security is enforced by the
// envelope's RSA signature/encryption (NpciCryptoUtil) inside the handler.
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/npci/**")).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()
@@ -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,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.api;
public final class ChequeBookApiConstants {
private ChequeBookApiConstants() {}
public static final String ENTITY_SERIES = "CHEQUESERIES";
public static final String ENTITY_STOCK_LOT = "CHEQUESTOCKLOT";
public static final String ENTITY_STOCK_TRANSFER = "CHEQUESTOCKTRANSFER";
public static final String ENTITY_BOOK = "CHEQUEBOOK";
public static final String ENTITY_PENDING_REQUEST = "CHEQUEBOOKPENDINGREQUEST";
public static final String RESOURCE_SERIES = "chequeseries";
public static final String RESOURCE_STOCK_LOT = "chequestocklot";
public static final String RESOURCE_STOCK_TRANSFER = "chequestocktransfer";
public static final String RESOURCE_BOOK = "chequebook";
}
@@ -0,0 +1,113 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public enum ChequeBookApiJsonInputParams {
// series create/update
SERIES_CODE("seriesCode"), //
NAME("name"), //
LEAVES_PER_BOOK("leavesPerBook"), //
APPLICABLE_ACCOUNT_TYPE("applicableAccountType"), //
CTS_COMPLIANT("ctsCompliant"), //
REORDER_LEVEL("reorderLevel"), //
// stock lot receive
SERIES_ID("seriesId"), //
OFFICE_ID("officeId"), //
RANGE_START("rangeStart"), //
RANGE_END("rangeEnd"), //
BATCH_REF("batchRef"), //
// stock lot report-lost
FROM_SERIAL_NO("fromSerialNo"), //
REASON("reason"), //
// stock transfer dispatch
FROM_OFFICE_ID("fromOfficeId"), //
TO_OFFICE_ID("toOfficeId"), //
FROM_LOT_ID("fromLotId"), //
NO_OF_LEAVES("noOfLeaves"), //
// book request
ACCOUNT_TYPE("accountType"), //
ACCOUNT_ID("accountId"), //
NO_OF_BOOKS("noOfBooks"), //
// book issue
COLLECTION_ACKNOWLEDGED("collectionAcknowledged"), //
// book reject
REJECTION_REASON("rejectionReason"), //
// book leaf actions (stop-payment / revoke / report-lost / cancel-leaf)
FROM_LEAF_SEQ("fromLeafSeq"), //
TO_LEAF_SEQ("toLeafSeq"), //
// common — the web client always sends these on command payloads
LOCALE("locale"), //
DATE_FORMAT("dateFormat"); //
private final String value;
ChequeBookApiJsonInputParams(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public static Set<String> seriesCreateParams() {
return toSet(SERIES_CODE, NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT);
}
public static Set<String> seriesUpdateParams() {
return toSet(NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT);
}
public static Set<String> stockLotReceiveParams() {
return toSet(SERIES_ID, OFFICE_ID, RANGE_START, RANGE_END, BATCH_REF, LOCALE, DATE_FORMAT);
}
public static Set<String> stockLotReportLostParams() {
return toSet(FROM_SERIAL_NO, REASON, LOCALE, DATE_FORMAT);
}
public static Set<String> stockTransferDispatchParams() {
return toSet(SERIES_ID, FROM_OFFICE_ID, TO_OFFICE_ID, FROM_LOT_ID, NO_OF_LEAVES, LOCALE, DATE_FORMAT);
}
public static Set<String> bookRequestParams() {
return toSet(ACCOUNT_TYPE, ACCOUNT_ID, OFFICE_ID, SERIES_ID, NO_OF_BOOKS, LOCALE, DATE_FORMAT);
}
public static Set<String> bookIssueParams() {
return toSet(COLLECTION_ACKNOWLEDGED, LOCALE, DATE_FORMAT);
}
public static Set<String> bookRejectParams() {
return toSet(REJECTION_REASON, LOCALE, DATE_FORMAT);
}
public static Set<String> bookLeafActionParams() {
return toSet(FROM_LEAF_SEQ, TO_LEAF_SEQ, REASON, LOCALE, DATE_FORMAT);
}
private static Set<String> toSet(final ChequeBookApiJsonInputParams... params) {
return Arrays.stream(params).map(ChequeBookApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new));
}
}
@@ -0,0 +1,132 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequebooks")
@Component
@Tag(name = "Cheque Books", description = "Requisition, issuance, stop-payment, hotlisting and cancellation of cheque books issued "
+ "against a Savings or Current account.")
@RequiredArgsConstructor
public class ChequeBookApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque books")
public Page<ChequeBookData> retrieveAll(@QueryParam("accountType") final String accountType,
@QueryParam("accountId") final Long accountId, @QueryParam("officeId") final Long officeId,
@QueryParam("status") final String status, @QueryParam("offset") @DefaultValue("0") final Integer offset,
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrieveAllBooks(accountType, accountId, officeId, status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque book")
public ChequeBookData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveBook(id);
}
@GET
@Path("{id}/leaves")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List the individual leaves of a cheque book")
public List<ChequeLeafData> retrieveLeaves(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveLeaves(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Request a cheque book against an account")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeBook().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque book", description = "Supported commands: issue, reject, withdraw, "
+ "stoppayment, revokestoppayment, reportlost, cancelleaf, markused, return.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "issue | reject | withdraw | stoppayment | revokestoppayment | "
+ "reportlost | cancelleaf | markused | return") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("issue".equalsIgnoreCase(commandParam)) {
request = builder.issueChequeBook(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeBook(id).build();
} else if ("withdraw".equalsIgnoreCase(commandParam)) {
request = builder.withdrawChequeBook(id).build();
} else if ("stoppayment".equalsIgnoreCase(commandParam)) {
request = builder.stopPaymentChequeBook(id).build();
} else if ("revokestoppayment".equalsIgnoreCase(commandParam)) {
request = builder.revokeStopPaymentChequeBook(id).build();
} else if ("reportlost".equalsIgnoreCase(commandParam)) {
request = builder.reportLostChequeBook(id).build();
} else if ("cancelleaf".equalsIgnoreCase(commandParam)) {
request = builder.cancelLeafChequeBook(id).build();
} else if ("markused".equalsIgnoreCase(commandParam)) {
request = builder.markUsedChequeBook(id).build();
} else if ("return".equalsIgnoreCase(commandParam)) {
request = builder.returnChequeBook(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
/**
* Bespoke maker-checker queue for the three cheque-book creation actions (series master, stock receipt,
* book requisition) — submitted via the normal {@code /chequeseries}, {@code /chequestocklots},
* {@code /chequebooks} POST endpoints, which now hold the request here as PENDING instead of creating the
* record immediately. A DIFFERENT user than the maker must approve or reject it from here.
*/
@Path("/v1/chequebook/pending-requests")
@Component
@Tag(name = "Cheque Book Pending Requests", description = "Maker-checker queue for cheque series, stock receipt, and book requisition "
+ "submissions — approving replays the original request and creates the record; the maker can never approve their own request.")
@RequiredArgsConstructor
public class ChequeBookPendingRequestApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque book pending requests")
public Page<ChequeBookPendingRequestData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("fromDate") final String fromDate, @QueryParam("toDate") final String toDate,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_PENDING_REQUEST);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrievePendingRequests(status, fromDate, toDate, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque book pending request")
public ChequeBookPendingRequestData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_PENDING_REQUEST);
return this.readPlatformService.retrievePendingRequest(id);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve or reject a cheque book pending request", description = "Supported commands: approve, reject. "
+ "The user submitting the original request can never approve/reject it themselves.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "approve | reject") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("approve".equalsIgnoreCase(commandParam)) {
request = builder.approveChequeBookPendingRequest(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeBookPendingRequest(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,56 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequeleaves")
@Component
@Tag(name = "Cheque Leaves", description = "The teller/counter quick-lookup: given a bare cheque-leaf serial number, is it "
+ "active, stopped, lost, or was it ever issued at all — one code path covers every case. A serial number is only "
+ "unique within its own series, so more than one series can match.")
@RequiredArgsConstructor
public class ChequeLeafApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
@GET
@Path("lookup")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Look up a cheque leaf by its serial number", description = "Returns every leaf across every series "
+ "that carries this serial number — normally one, but more than one series can legitimately share a number.")
public List<ChequeLeafData> lookup(@QueryParam("serialNo") @Parameter(description = "serialNo") final Long serialNo) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveLeafBySerialNo(serialNo);
}
}
@@ -0,0 +1,103 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequeseries")
@Component
@Tag(name = "Cheque Book Series", description = "Cheque-leaf stationery master — leaves-per-book, CTS-2010 compliance, low-stock reorder threshold.")
@RequiredArgsConstructor
public class ChequeSeriesApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque series")
public Page<ChequeSeriesData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_SERIES);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveAllSeries(status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque series")
public ChequeSeriesData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_SERIES);
return this.readPlatformService.retrieveSeries(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a cheque series")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeSeries().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a cheque series")
public CommandProcessingResult update(@PathParam("id") @Parameter(description = "id") final Long id,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateChequeSeries(id).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Deactivate a cheque series")
public CommandProcessingResult delete(@PathParam("id") @Parameter(description = "id") final Long id) {
final CommandWrapper request = new CommandWrapperBuilder().deleteChequeSeries(id).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequestocklots")
@Component
@Tag(name = "Cheque Stock Lots", description = "Unified stock ledger — a contiguous leaf-serial range currently sitting at one office, "
+ "with a drawdown cursor shared by both issuance-to-customer and transfer-to-office.")
@RequiredArgsConstructor
public class ChequeStockLotApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque stock lots")
public Page<ChequeStockLotData> retrieveAll(@QueryParam("officeId") final Long officeId,
@QueryParam("seriesId") final Long seriesId, @QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_LOT);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveStockLots(officeId, seriesId, status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque stock lot")
public ChequeStockLotData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_LOT);
return this.readPlatformService.retrieveStockLot(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Receive a bulk cheque stock lot (from press/vendor)")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeStockLot().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque stock lot", description = "Supported commands: reportlost.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "reportlost") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("reportlost".equalsIgnoreCase(commandParam)) {
request = builder.reportLostChequeStockLot(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,103 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequestocktransfers")
@Component
@Tag(name = "Cheque Stock Transfers", description = "Office-to-office cheque stock movement: dispatch draws a sub-range down from the "
+ "source lot, the destination office then acknowledges receipt (or rejects).")
@RequiredArgsConstructor
public class ChequeStockTransferApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque stock transfers")
public Page<ChequeStockTransferData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_TRANSFER);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveStockTransfers(status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque stock transfer")
public ChequeStockTransferData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_TRANSFER);
return this.readPlatformService.retrieveStockTransfer(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Dispatch a cheque stock transfer to another office")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeStockTransfer().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque stock transfer", description = "Supported commands: receive, reject.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "receive | reject") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("receive".equalsIgnoreCase(commandParam)) {
request = builder.receiveChequeStockTransfer(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeStockTransfer(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,55 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.data;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeBookData {
private final Long id;
private final String accountType;
private final Long accountId;
private final String accountNo;
private final String clientName;
private final Long officeId;
private final String officeName;
private final Long seriesId;
private final String seriesCode;
private final LocalDate requestedDate;
private final Long requestedBy;
private final Integer noOfBooks;
private final Integer noOfLeavesRequested;
private final String status;
private final LocalDate issueDate;
private final Long issuedBy;
private final Long lotId;
private final Long rangeStart;
private final Long rangeEnd;
private final Integer totalLeaves;
private final String rejectionReason;
private final boolean collectionAcknowledged;
private final LocalDate collectedDate;
private final Long collectedBy;
}
@@ -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.chequebook.data;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeBookPendingRequestData {
private final Long id;
private final String requestType;
private final String payloadJson;
private final String status;
private final Long makerId;
private final String makerName;
private final String makerUsername;
private final OffsetDateTime submittedOn;
private final Long checkerId;
private final String checkerName;
private final String checkerUsername;
private final OffsetDateTime checkedOn;
private final String rejectionReason;
private final Long createdResourceId;
private final String activityLog;
}
@@ -0,0 +1,63 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.data;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
/**
* A single leaf. {@code chequeBookId}/{@code accountNo}/{@code clientName} are null when the row was
* generated directly against a stock lot (stock reported lost/stolen before ever being issued) — the
* quick-lookup-by-serial-number screen relies on this being the single source of truth for every case.
*/
@Getter
@Builder
@AllArgsConstructor
public class ChequeLeafData {
private final Long id;
private final Long chequeBookId;
private final Long lotId;
private final String seriesCode;
private final Long leafSerialNo;
private final Integer leafSeqNo;
private final String status;
private final String stopReason;
private final Long stoppedBy;
private final OffsetDateTime stoppedOn;
private final String lostReason;
private final Long lostReportedBy;
private final OffsetDateTime lostReportedOn;
private final String cancelledReason;
private final Long cancelledBy;
private final OffsetDateTime cancelledOn;
private final String usedReason;
private final Long usedBy;
private final OffsetDateTime usedOn;
private final String returnReason;
private final Long returnedBy;
private final OffsetDateTime returnedOn;
private final String accountType;
private final Long accountId;
private final String accountNo;
private final String clientName;
private final String officeName;
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeSeriesData {
private final Long id;
private final String seriesCode;
private final String name;
private final Integer leavesPerBook;
private final String applicableAccountType;
private final boolean ctsCompliant;
private final Integer reorderLevel;
private final boolean active;
}
@@ -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.chequebook.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeStockLotData {
private final Long id;
private final Long seriesId;
private final String seriesCode;
private final Integer leavesPerBook;
private final Long officeId;
private final String officeName;
private final Long rangeStart;
private final Long rangeEnd;
private final Long nextSerialNo;
private final Long remainingLeaves;
private final Long remainingBooks;
private final String source;
private final Long sourceRefId;
private final String batchRef;
private final String status;
private final boolean lowStock;
}
@@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.data;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeStockTransferData {
private final Long id;
private final Long seriesId;
private final String seriesCode;
private final Long fromOfficeId;
private final String fromOfficeName;
private final Long toOfficeId;
private final String toOfficeName;
private final Long fromLotId;
private final Long toLotId;
private final Long rangeStart;
private final Long rangeEnd;
private final Integer totalLeaves;
private final LocalDate dispatchDate;
private final String status;
private final LocalDate receivedDate;
private final Long dispatchedBy;
private final Long receivedBy;
}
@@ -0,0 +1,40 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import org.apache.commons.lang3.StringUtils;
/** Which account model a cheque book is issued against — the two are not interchangeable in this codebase. */
public enum ChequeAccountType {
SAVINGS, //
CURRENT; //
public static ChequeAccountType fromString(final String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (final ChequeAccountType type : values()) {
if (type.name().equalsIgnoreCase(value.trim())) {
return type;
}
}
return null;
}
}
@@ -0,0 +1,184 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* A cheque book requested and (once stock is drawn) issued against an account. {@code accountId} is a
* plain column (no JPA relation) since the account may be either a {@code SavingsAccount} or a
* {@code CurrentAccount} — two account models in this codebase that are not interchangeable — mirroring
* the same "plain column to avoid cross-module entity dependency" precedent used by
* {@code CurrentAccount.productId}.
*/
@Entity
@Table(name = "m_cheque_book")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeBook extends AbstractPersistableCustom<Long> {
@Column(name = "account_type", nullable = false, length = 10)
private String accountType;
@Column(name = "account_id", nullable = false)
private Long accountId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id")
private ChequeSeries series;
@Column(name = "requested_date", nullable = false)
private LocalDate requestedDate;
@Column(name = "requested_by")
private Long requestedBy;
/** How many whole books the customer requested; leaves handed over = noOfBooks x series.leavesPerBook. */
@Column(name = "no_of_books", nullable = false)
private Integer noOfBooks;
/** Legacy free-form leaf count; superseded by {@link #noOfBooks}. Kept nullable for historical rows. */
@Column(name = "no_of_leaves_requested")
private Integer noOfLeavesRequested;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "issued_by")
private Long issuedBy;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lot_id")
private ChequeStockLot lot;
@Column(name = "range_start")
private Long rangeStart;
@Column(name = "range_end")
private Long rangeEnd;
@Column(name = "total_leaves")
private Integer totalLeaves;
@Column(name = "rejection_reason", length = 500)
private String rejectionReason;
@Column(name = "collection_acknowledged", nullable = false)
private boolean collectionAcknowledged;
@Column(name = "collected_date")
private LocalDate collectedDate;
@Column(name = "collected_by")
private Long collectedBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
@Version
@Column(name = "version")
private Integer version;
/**
* The customer/branch picks the denomination (series) at requisition time — matching real cheque-book
* requisition slips, which specify the book type up front rather than leaving it to the issuing teller
* to guess. {@code series} must already be applicable to {@code accountType} (checked by the caller).
*/
public static ChequeBook requestNew(final ChequeAccountType accountType, final Long accountId, final Office office,
final ChequeSeries series, final Integer noOfBooks, final Long requestedBy) {
final ChequeBook book = new ChequeBook();
book.accountType = accountType.name();
book.accountId = accountId;
book.office = office;
book.series = series;
book.noOfBooks = noOfBooks;
book.requestedDate = DateUtils.getBusinessLocalDate();
book.requestedBy = requestedBy;
book.status = ChequeBookStatus.REQUESTED.name();
book.collectionAcknowledged = false;
book.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return book;
}
private void assertRequested() {
if (!ChequeBookStatus.REQUESTED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.requested",
"Cheque book " + this.getId() + " is not in REQUESTED status.", this.getId());
}
}
/** The series was already fixed at request time; issuing only draws stock and materializes the leaves. */
public void issue(final ChequeStockLot lot, final Long rangeStart, final Long rangeEnd, final Integer totalLeaves,
final Long issuedBy) {
assertRequested();
this.lot = lot;
this.rangeStart = rangeStart;
this.rangeEnd = rangeEnd;
this.totalLeaves = totalLeaves;
this.issueDate = DateUtils.getBusinessLocalDate();
this.issuedBy = issuedBy;
this.status = ChequeBookStatus.ISSUED.name();
}
public void reject(final String reason) {
assertRequested();
this.status = ChequeBookStatus.REJECTED.name();
this.rejectionReason = reason;
}
public void withdraw() {
if (!ChequeBookStatus.ISSUED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.issued",
"Cheque book " + this.getId() + " is not in ISSUED status.", this.getId());
}
this.status = ChequeBookStatus.WITHDRAWN.name();
}
public void acknowledgeCollection(final Long collectedBy) {
this.collectionAcknowledged = true;
this.collectedBy = collectedBy;
this.collectedDate = DateUtils.getBusinessLocalDate();
}
public boolean isIssued() {
return ChequeBookStatus.ISSUED.name().equals(this.status);
}
}
@@ -0,0 +1,130 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Bespoke maker-checker for cheque-book creation actions (series master, stock receipt, book requisition) —
* modelled on {@code m_clerk_disbursement_request} rather than Fineract's native command-approval engine,
* because that engine's hold is bypassed by any role carrying ALL_FUNCTIONS (Super user, Clerk in this
* tenant), which made it a no-op for this CBS's actual admin accounts. A request holds the raw submission
* JSON; approval replays it through the same creation logic used elsewhere in this module. The maker can
* never approve/reject their own request — enforced in the write service, not just by role.
*/
@Entity
@Table(name = "m_chequebook_pending_request")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeBookPendingRequest extends AbstractPersistableCustom<Long> {
@Column(name = "request_type", nullable = false, length = 30)
private String requestType;
@Column(name = "payload_json", nullable = false)
private String payloadJson;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "maker_id", nullable = false)
private Long makerId;
@Column(name = "maker_name", length = 150)
private String makerName;
@Column(name = "maker_username", length = 100)
private String makerUsername;
@Column(name = "submitted_on", nullable = false)
private OffsetDateTime submittedOn;
@Column(name = "checker_id")
private Long checkerId;
@Column(name = "checker_name", length = 150)
private String checkerName;
@Column(name = "checker_username", length = 100)
private String checkerUsername;
@Column(name = "checked_on")
private OffsetDateTime checkedOn;
@Column(name = "rejection_reason", length = 500)
private String rejectionReason;
@Column(name = "created_resource_id")
private Long createdResourceId;
@Column(name = "activity_log")
private String activityLog;
public static ChequeBookPendingRequest submit(final ChequeBookPendingRequestType requestType, final String payloadJson,
final AppUser maker, final String activityLog) {
final ChequeBookPendingRequest request = new ChequeBookPendingRequest();
request.requestType = requestType.name();
request.payloadJson = payloadJson;
request.status = ChequeBookPendingRequestStatus.PENDING.name();
request.makerId = maker.getId();
request.makerName = maker.getDisplayName();
request.makerUsername = maker.getUsername();
request.submittedOn = DateUtils.getOffsetDateTimeOfTenant();
request.activityLog = activityLog;
return request;
}
public boolean isPending() {
return ChequeBookPendingRequestStatus.PENDING.name().equals(this.status);
}
/** The maker can never be their own checker — a distinct human must approve/reject a request. */
public boolean isMadeBy(final Long userId) {
return this.makerId != null && this.makerId.equals(userId);
}
public void approve(final AppUser checker, final Long createdResourceId, final String activityLog) {
this.status = ChequeBookPendingRequestStatus.APPROVED.name();
this.checkerId = checker.getId();
this.checkerName = checker.getDisplayName();
this.checkerUsername = checker.getUsername();
this.checkedOn = DateUtils.getOffsetDateTimeOfTenant();
this.createdResourceId = createdResourceId;
this.activityLog = activityLog;
}
public void reject(final AppUser checker, final String reason, final String activityLog) {
this.status = ChequeBookPendingRequestStatus.REJECTED.name();
this.checkerId = checker.getId();
this.checkerName = checker.getDisplayName();
this.checkerUsername = checker.getUsername();
this.checkedOn = DateUtils.getOffsetDateTimeOfTenant();
this.rejectionReason = reason;
this.activityLog = activityLog;
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeBookPendingRequestRepository extends JpaRepository<ChequeBookPendingRequest, Long> {
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookPendingRequestNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookPendingRequestRepositoryWrapper {
private final ChequeBookPendingRequestRepository repository;
public ChequeBookPendingRequest findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeBookPendingRequestNotFoundException(id));
}
public ChequeBookPendingRequest saveAndFlush(final ChequeBookPendingRequest request) {
return this.repository.saveAndFlush(request);
}
}
@@ -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.chequebook.domain;
/** Lifecycle of a {@link ChequeBookPendingRequest}. Terminal once APPROVED or REJECTED. */
public enum ChequeBookPendingRequestStatus {
PENDING, //
APPROVED, //
REJECTED; //
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
/** Which cheque-book action a {@link ChequeBookPendingRequest} holds. */
public enum ChequeBookPendingRequestType {
CREATE_SERIES, //
RECEIVE_STOCK, //
REQUEST_BOOK, //
DELETE_SERIES; //
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeBookRepository extends JpaRepository<ChequeBook, Long> {
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookRepositoryWrapper {
private final ChequeBookRepository repository;
public ChequeBook findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeBookNotFoundException(id));
}
public ChequeBook saveAndFlush(final ChequeBook book) {
return this.repository.saveAndFlush(book);
}
}
@@ -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.chequebook.domain;
public enum ChequeBookStatus {
REQUESTED, //
ISSUED, //
REJECTED, //
WITHDRAWN; //
}
@@ -0,0 +1,214 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/**
* One physical cheque leaf. {@code chequeBook} is nullable: a leaf reported lost/stolen while still
* unissued stock (see {@link ChequeStockLot#reportRemainingLost(long)}) has no book yet, but still needs a
* row so the serial-number quick-lookup screen has exactly one code path for every case — issued-active,
* issued-stopped, issued-lost, and unissued-but-stolen.
*
* <p>
* Legal transitions: (created)-&gt;ACTIVE (issued) or (created, no book)-&gt;LOST (unissued theft report);
* ACTIVE&lt;-&gt;STOP_PAYMENT (the one round-trip); ACTIVE-&gt;CANCELLED / LOST / WITHDRAWN / USED / RETURNED (all
* terminal). A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating,
* since the customer has typically already been issued a replacement book; a RETURNED (dishonoured) leaf
* likewise stays flagged rather than reverting to ACTIVE. USED is manually recorded by a branch, never
* automatic — see {@link #markUsed(String, Long)}.
*/
@Entity
@Table(name = "m_cheque_leaf")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeLeaf extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cheque_book_id")
private ChequeBook chequeBook;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lot_id", nullable = false)
private ChequeStockLot lot;
@Column(name = "leaf_serial_no", nullable = false)
private Long leafSerialNo;
@Column(name = "leaf_seq_no")
private Integer leafSeqNo;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "stop_reason", length = 500)
private String stopReason;
@Column(name = "stopped_by")
private Long stoppedBy;
@Column(name = "stopped_on")
private OffsetDateTime stoppedOn;
@Column(name = "lost_reason", length = 500)
private String lostReason;
@Column(name = "lost_reported_by")
private Long lostReportedBy;
@Column(name = "lost_reported_on")
private OffsetDateTime lostReportedOn;
@Column(name = "cancelled_reason", length = 500)
private String cancelledReason;
@Column(name = "cancelled_by")
private Long cancelledBy;
@Column(name = "cancelled_on")
private OffsetDateTime cancelledOn;
/** Manually recorded once the branch sees this leaf clear on the bank/clearing statement — never automatic. */
@Column(name = "used_reason", length = 500)
private String usedReason;
@Column(name = "used_by")
private Long usedBy;
@Column(name = "used_on")
private OffsetDateTime usedOn;
/** Set when a presented leaf is dishonoured/bounced (insufficient funds, signature mismatch, etc.). */
@Column(name = "return_reason", length = 500)
private String returnReason;
@Column(name = "returned_by")
private Long returnedBy;
@Column(name = "returned_on")
private OffsetDateTime returnedOn;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeLeaf issueForBook(final ChequeBook chequeBook, final ChequeStockLot lot, final Long leafSerialNo,
final Integer leafSeqNo) {
final ChequeLeaf leaf = new ChequeLeaf();
leaf.chequeBook = chequeBook;
leaf.lot = lot;
leaf.leafSerialNo = leafSerialNo;
leaf.leafSeqNo = leafSeqNo;
leaf.status = ChequeLeafStatus.ACTIVE.name();
leaf.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return leaf;
}
public static ChequeLeaf lostUnissued(final ChequeStockLot lot, final Long leafSerialNo, final String reason,
final Long reportedBy) {
final ChequeLeaf leaf = new ChequeLeaf();
leaf.lot = lot;
leaf.leafSerialNo = leafSerialNo;
leaf.status = ChequeLeafStatus.LOST.name();
leaf.lostReason = reason;
leaf.lostReportedBy = reportedBy;
leaf.lostReportedOn = DateUtils.getOffsetDateTimeOfTenant();
leaf.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return leaf;
}
private void assertActive() {
if (!ChequeLeafStatus.ACTIVE.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeleaf.not.active",
"Cheque leaf " + this.leafSerialNo + " is not ACTIVE.", this.leafSerialNo);
}
}
public void stopPayment(final String reason, final Long stoppedBy) {
assertActive();
this.status = ChequeLeafStatus.STOP_PAYMENT.name();
this.stopReason = reason;
this.stoppedBy = stoppedBy;
this.stoppedOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void revokeStopPayment() {
if (!ChequeLeafStatus.STOP_PAYMENT.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeleaf.not.stopped",
"Cheque leaf " + this.leafSerialNo + " is not under stop-payment.", this.leafSerialNo);
}
this.status = ChequeLeafStatus.ACTIVE.name();
this.stopReason = null;
this.stoppedBy = null;
this.stoppedOn = null;
}
public void reportLost(final String reason, final Long reportedBy) {
assertActive();
this.status = ChequeLeafStatus.LOST.name();
this.lostReason = reason;
this.lostReportedBy = reportedBy;
this.lostReportedOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void cancel(final String reason, final Long cancelledBy) {
assertActive();
this.status = ChequeLeafStatus.CANCELLED.name();
this.cancelledReason = reason;
this.cancelledBy = cancelledBy;
this.cancelledOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void withdraw() {
assertActive();
this.status = ChequeLeafStatus.WITHDRAWN.name();
}
/**
* Manually recorded once a branch sees this leaf clear on the bank/clearing statement. Never set
* automatically — this CBS has no clearing/presentment integration to detect it on its own.
*/
public void markUsed(final String reason, final Long markedBy) {
assertActive();
this.status = ChequeLeafStatus.USED.name();
this.usedReason = reason;
this.usedBy = markedBy;
this.usedOn = DateUtils.getOffsetDateTimeOfTenant();
}
/** The leaf was presented for payment and dishonoured/bounced. Terminal — no revoke, matching LOST/CANCELLED. */
public void returned(final String reason, final Long actorId) {
assertActive();
this.status = ChequeLeafStatus.RETURNED.name();
this.returnReason = reason;
this.returnedBy = actorId;
this.returnedOn = DateUtils.getOffsetDateTimeOfTenant();
}
}
@@ -0,0 +1,40 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeLeafRepository extends JpaRepository<ChequeLeaf, Long> {
Optional<ChequeLeaf> findByLeafSerialNo(Long leafSerialNo);
/**
* leafSerialNo is only unique per stock lot (see 0387_chequeleaf_serial_unique_per_lot.xml) — two
* different lots/books can legitimately share the same serial. Callers that need to disambiguate should
* use this instead of {@link #findByLeafSerialNo}, which throws when more than one row matches.
*/
List<ChequeLeaf> findAllByLeafSerialNo(Long leafSerialNo);
List<ChequeLeaf> findByChequeBookIdOrderByLeafSeqNoAsc(Long chequeBookId);
List<ChequeLeaf> findByChequeBookIdAndLeafSeqNoBetweenOrderByLeafSeqNoAsc(Long chequeBookId, Integer fromSeqNo,
Integer toSeqNo);
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeLeafNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeLeafRepositoryWrapper {
private final ChequeLeafRepository repository;
public ChequeLeaf findByLeafSerialNoWithNotFoundDetection(final Long leafSerialNo) {
return this.repository.findByLeafSerialNo(leafSerialNo).orElseThrow(() -> new ChequeLeafNotFoundException(leafSerialNo));
}
public ChequeLeaf saveAndFlush(final ChequeLeaf leaf) {
return this.repository.saveAndFlush(leaf);
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
/**
* Legal transitions: (created)-&gt;ACTIVE (issued) or (created, no book)-&gt;LOST (unissued stock reported
* stolen); ACTIVE&lt;-&gt;STOP_PAYMENT (the one round-trip, via stop-payment/revoke); ACTIVE-&gt;CANCELLED
* (spoiled leaf voided); ACTIVE-&gt;LOST (reported lost/stolen post-issuance); ACTIVE-&gt;WITHDRAWN (account
* closed); ACTIVE-&gt;USED (manually recorded once the branch sees the leaf clear on the bank/clearing
* statement — this CBS has no clearing/presentment integration, so USED can never be set automatically);
* ACTIVE-&gt;RETURNED (presented and dishonoured/bounced — insufficient funds, signature mismatch, etc.).
* LOST, CANCELLED, WITHDRAWN, USED and RETURNED are all terminal — a recovered "lost" cheque conventionally
* stays hotlisted since the customer has typically already been issued a replacement book, and a returned
* leaf stays flagged rather than silently reverting to ACTIVE.
*/
public enum ChequeLeafStatus {
ACTIVE, //
STOP_PAYMENT, //
CANCELLED, //
LOST, //
WITHDRAWN, //
USED, //
RETURNED; //
}
@@ -0,0 +1,111 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** Cheque-leaf stationery master — leaves-per-book, CTS-2010 compliance, and a low-stock reorder threshold. */
@Entity
@Table(name = "m_cheque_series")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeSeries extends AbstractPersistableCustom<Long> {
@Column(name = "series_code", nullable = false, length = 20)
private String seriesCode;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "leaves_per_book", nullable = false)
private Integer leavesPerBook;
/** SAVINGS / CURRENT — which account scheme this stationery may be issued to. */
@Column(name = "applicable_account_type", nullable = false, length = 10)
private String applicableAccountType;
@Column(name = "cts_compliant", nullable = false)
private boolean ctsCompliant;
@Column(name = "reorder_level")
private Integer reorderLevel;
@Column(name = "is_active", nullable = false)
private boolean active;
@Column(name = "created_by")
private Long createdBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeSeries createNew(final String seriesCode, final String name, final Integer leavesPerBook,
final ChequeSeriesApplicability applicableAccountType, final boolean ctsCompliant, final Integer reorderLevel,
final Long createdBy) {
final ChequeSeries series = new ChequeSeries();
series.seriesCode = seriesCode;
series.name = name;
series.leavesPerBook = leavesPerBook;
series.applicableAccountType = applicableAccountType.name();
series.ctsCompliant = ctsCompliant;
series.reorderLevel = reorderLevel;
series.active = true;
series.createdBy = createdBy;
series.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return series;
}
public void updateDetails(final String name, final Integer leavesPerBook, final ChequeSeriesApplicability applicableAccountType,
final Boolean ctsCompliant, final Integer reorderLevel) {
if (name != null) {
this.name = name;
}
if (leavesPerBook != null) {
this.leavesPerBook = leavesPerBook;
}
if (applicableAccountType != null) {
this.applicableAccountType = applicableAccountType.name();
}
if (ctsCompliant != null) {
this.ctsCompliant = ctsCompliant;
}
if (reorderLevel != null) {
this.reorderLevel = reorderLevel;
}
}
public void deactivate() {
this.active = false;
}
/** Whether this series' stationery may be issued to the given account type. */
public boolean appliesTo(final ChequeAccountType accountType) {
final ChequeSeriesApplicability applicability = ChequeSeriesApplicability.fromString(this.applicableAccountType);
// a legacy row with an unrecognised/blank value (e.g. a pre-existing "BOTH") is treated permissively
return applicability == null || applicability.appliesTo(accountType);
}
}
@@ -0,0 +1,53 @@
/**
* 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.chequebook.domain;
import org.apache.commons.lang3.StringUtils;
/**
* Which account scheme a cheque series' stationery is meant for. Real banks stock physically different
* cheque books for Savings (SB) and Current (CA) accounts (different leaf count, design, MICR transaction
* code), so every series is tagged SAVINGS-only or CURRENT-only — never both — and issuance is validated
* against it.
*/
public enum ChequeSeriesApplicability {
SAVINGS, //
CURRENT; //
public static ChequeSeriesApplicability fromString(final String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (final ChequeSeriesApplicability applicability : values()) {
if (applicability.name().equalsIgnoreCase(value.trim())) {
return applicability;
}
}
return null;
}
/** Whether a series with this applicability may be issued to the given account type. */
public boolean appliesTo(final ChequeAccountType accountType) {
if (accountType == null) {
return false;
}
return this.name().equals(accountType.name());
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeSeriesRepository extends JpaRepository<ChequeSeries, Long> {
boolean existsBySeriesCode(String seriesCode);
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeSeriesNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeSeriesRepositoryWrapper {
private final ChequeSeriesRepository repository;
public ChequeSeries findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeSeriesNotFoundException(id));
}
public ChequeSeries saveAndFlush(final ChequeSeries series) {
return this.repository.saveAndFlush(series);
}
}
@@ -0,0 +1,192 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* A contiguous range of cheque-leaf serials currently sitting at one office, with a drawdown cursor
* ({@link #nextSerialNo}) that advances every time a sub-range is issued to a customer or transferred to
* another office — both are structurally the same "take N off the front of the remaining range" operation.
* This single concept covers both an HO bulk receipt from the press and a branch's received-transfer-in
* stock; a transfer-in is materially the same thing as a receipt, just sourced from another office.
*/
@Entity
@Table(name = "m_cheque_stock_lot")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeStockLot extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id", nullable = false)
private ChequeSeries series;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@Column(name = "range_start", nullable = false)
private Long rangeStart;
@Column(name = "range_end", nullable = false)
private Long rangeEnd;
@Column(name = "next_serial_no", nullable = false)
private Long nextSerialNo;
@Column(name = "source", nullable = false, length = 20)
private String source;
@Column(name = "source_ref_id")
private Long sourceRefId;
@Column(name = "batch_ref", length = 100)
private String batchRef;
@Column(name = "status", nullable = false, length = 25)
private String status;
@Column(name = "created_by")
private Long createdBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
@Column(name = "received_by")
private Long receivedBy;
@Column(name = "received_on")
private OffsetDateTime receivedOn;
@Version
@Column(name = "version")
private Integer version;
public static ChequeStockLot createReceipt(final ChequeSeries series, final Office office, final Long rangeStart,
final Long rangeEnd, final String batchRef, final Long createdBy) {
final ChequeStockLot lot = new ChequeStockLot();
lot.series = series;
lot.office = office;
lot.rangeStart = rangeStart;
lot.rangeEnd = rangeEnd;
lot.nextSerialNo = rangeStart;
lot.source = ChequeStockLotSource.RECEIPT.name();
lot.batchRef = batchRef;
lot.status = ChequeStockLotStatus.AVAILABLE.name();
lot.createdBy = createdBy;
lot.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return lot;
}
public static ChequeStockLot createTransferIn(final ChequeSeries series, final Office office, final Long rangeStart,
final Long rangeEnd, final Long transferId) {
final ChequeStockLot lot = new ChequeStockLot();
lot.series = series;
lot.office = office;
lot.rangeStart = rangeStart;
lot.rangeEnd = rangeEnd;
lot.nextSerialNo = rangeStart;
lot.source = ChequeStockLotSource.TRANSFER_IN.name();
lot.sourceRefId = transferId;
lot.status = ChequeStockLotStatus.DISPATCHED.name();
lot.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return lot;
}
/** Leaves still available to draw down, regardless of status. */
public long remainingLeaves() {
return this.rangeEnd - this.nextSerialNo + 1;
}
/**
* Takes the next {@code qty} leaf serials off the front of this lot's remaining range, advancing the
* cursor and marking the lot EXHAUSTED if nothing remains. Used identically for issuing a book to a
* customer and for dispatching a sub-range to another office.
*/
public long[] drawDown(final int qty) {
if (!ChequeStockLotStatus.AVAILABLE.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.not.available",
"Cheque stock lot " + this.getId() + " is not available for drawdown.", this.getId());
}
if (remainingLeaves() < qty) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.insufficient.stock",
"Cheque stock lot " + this.getId() + " does not have enough remaining leaves.", this.getId());
}
final long start = this.nextSerialNo;
final long end = start + qty - 1;
this.nextSerialNo = end + 1;
if (this.nextSerialNo > this.rangeEnd) {
this.status = ChequeStockLotStatus.EXHAUSTED.name();
}
return new long[] { start, end };
}
/** Transfer-in receive-ack: the destination lot becomes usable at its new custodian office. */
public void markAvailable(final Long receivedBy) {
if (!ChequeStockLotStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.not.dispatched",
"Cheque stock lot " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockLotStatus.AVAILABLE.name();
this.receivedBy = receivedBy;
this.receivedOn = DateUtils.getOffsetDateTimeOfTenant();
}
/** Transfer rejected in transit: the destination lot never becomes usable. */
public void cancel() {
this.status = ChequeStockLotStatus.CANCELLED.name();
}
/**
* Reports the remaining undrawn stock from {@code fromSerialNo} onward as lost/stolen. Only a suffix of
* the remaining range can be reported (an arbitrary internal sub-range would imply a custody failure
* severe enough that manual reconciliation, not an automatic system action, is the honest response).
* Returns the pre-mutation range end so the caller can generate LOST leaf rows for
* [fromSerialNo, returned value].
*/
public long reportRemainingLost(final long fromSerialNo) {
if (fromSerialNo < this.nextSerialNo || fromSerialNo > this.rangeEnd) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.invalid.report.range",
"The reported range must be a suffix of the lot's remaining undrawn stock.", this.getId());
}
final long originalRangeEnd = this.rangeEnd;
if (fromSerialNo == this.nextSerialNo) {
this.nextSerialNo = this.rangeEnd + 1;
this.status = ChequeStockLotStatus.CANCELLED.name();
} else {
this.rangeEnd = fromSerialNo - 1;
this.status = ChequeStockLotStatus.PARTIALLY_COMPROMISED.name();
}
return originalRangeEnd;
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ChequeStockLotRepository extends JpaRepository<ChequeStockLot, Long> {
List<ChequeStockLot> findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(Long seriesId, Long officeId, String status);
/**
* Anti-duplication control: a newly received range must not overlap any existing lot's range, REGARDLESS of
* series. A real bank's leaf-serial numbers are allocated from one bank-wide numbering authority — two
* different cheque designs (series) never legitimately reuse the same numeric range, because whichever
* account eventually receives books from both would end up holding two leaves with the identical serial
* (a collision NpciInwardPushService's drawer-account-number disambiguation can't resolve when both leaves
* belong to the same account). This used to be scoped to "within the same series only", which is exactly
* what let that collision happen in the first place.
*/
@Query("SELECT COUNT(l) FROM ChequeStockLot l WHERE l.rangeStart <= :rangeEnd AND l.rangeEnd >= :rangeStart")
long countOverlappingAnySeries(@Param("rangeStart") long rangeStart, @Param("rangeEnd") long rangeEnd);
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockLotNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeStockLotRepositoryWrapper {
private final ChequeStockLotRepository repository;
public ChequeStockLot findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeStockLotNotFoundException(id));
}
public ChequeStockLot saveAndFlush(final ChequeStockLot lot) {
return this.repository.saveAndFlush(lot);
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
/** How a {@link ChequeStockLot} came to exist at its current custodian office. */
public enum ChequeStockLotSource {
RECEIPT, //
TRANSFER_IN; //
}
@@ -0,0 +1,34 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
/**
* Lifecycle of a {@link ChequeStockLot}. DISPATCHED = in transit to another office, awaiting receive-ack.
* AVAILABLE = usable at this office for issuance or further transfer-out. EXHAUSTED = fully drawn down.
* PARTIALLY_COMPROMISED = a suffix of the remaining range was reported lost/stolen, but a front portion is
* still issuable. CANCELLED = fully voided (rejected transfer, or entirely reported lost/stolen).
*/
public enum ChequeStockLotStatus {
DISPATCHED, //
AVAILABLE, //
EXHAUSTED, //
PARTIALLY_COMPROMISED, //
CANCELLED; //
}
@@ -0,0 +1,133 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* Audit/workflow record of a stock movement between offices. The actual range drawdown at the source and
* the destination lot creation happen as part of dispatch (see write-service); this row tracks the
* dispatch → receive-ack (or reject) lifecycle.
*/
@Entity
@Table(name = "m_cheque_stock_transfer")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeStockTransfer extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id", nullable = false)
private ChequeSeries series;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_lot_id", nullable = false)
private ChequeStockLot fromLot;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_lot_id", nullable = false)
private ChequeStockLot toLot;
@Column(name = "range_start", nullable = false)
private Long rangeStart;
@Column(name = "range_end", nullable = false)
private Long rangeEnd;
@Column(name = "total_leaves", nullable = false)
private Integer totalLeaves;
@Column(name = "dispatch_date", nullable = false)
private LocalDate dispatchDate;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "received_date")
private LocalDate receivedDate;
@Column(name = "dispatched_by")
private Long dispatchedBy;
@Column(name = "received_by")
private Long receivedBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeStockTransfer createNew(final ChequeSeries series, final Office fromOffice, final Office toOffice,
final ChequeStockLot fromLot, final ChequeStockLot toLot, final Long rangeStart, final Long rangeEnd,
final Integer totalLeaves, final LocalDate dispatchDate, final Long dispatchedBy) {
final ChequeStockTransfer transfer = new ChequeStockTransfer();
transfer.series = series;
transfer.fromOffice = fromOffice;
transfer.toOffice = toOffice;
transfer.fromLot = fromLot;
transfer.toLot = toLot;
transfer.rangeStart = rangeStart;
transfer.rangeEnd = rangeEnd;
transfer.totalLeaves = totalLeaves;
transfer.dispatchDate = dispatchDate;
transfer.status = ChequeStockTransferStatus.DISPATCHED.name();
transfer.dispatchedBy = dispatchedBy;
transfer.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return transfer;
}
public void markReceived(final Long receivedBy, final LocalDate receivedDate) {
if (!ChequeStockTransferStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocktransfer.not.dispatched",
"Transfer " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockTransferStatus.RECEIVED.name();
this.receivedBy = receivedBy;
this.receivedDate = receivedDate;
}
public void reject(final Long rejectedBy, final LocalDate rejectedDate) {
if (!ChequeStockTransferStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocktransfer.not.dispatched",
"Transfer " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockTransferStatus.REJECTED.name();
this.receivedBy = rejectedBy;
this.receivedDate = rejectedDate;
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeStockTransferRepository extends JpaRepository<ChequeStockTransfer, Long> {
}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockTransferNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeStockTransferRepositoryWrapper {
private final ChequeStockTransferRepository repository;
public ChequeStockTransfer findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeStockTransferNotFoundException(id));
}
public ChequeStockTransfer saveAndFlush(final ChequeStockTransfer transfer) {
return this.repository.saveAndFlush(transfer);
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.domain;
public enum ChequeStockTransferStatus {
DISPATCHED, //
RECEIVED, //
REJECTED; //
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeBookNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeBookNotFoundException(final Long id) {
super("error.msg.chequebook.id.invalid", "Cheque book with identifier " + id + " does not exist", id);
}
}
@@ -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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeBookPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeBookPendingRequestNotFoundException(final Long id) {
super("error.msg.chequebookpendingrequest.id.invalid", "Cheque book pending request with identifier " + id + " does not exist",
id);
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeLeafNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeLeafNotFoundException(final Long leafSerialNo) {
super("error.msg.chequeleaf.serialno.invalid", "No cheque leaf found with serial number " + leafSerialNo, leafSerialNo);
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeSeriesNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeSeriesNotFoundException(final Long id) {
super("error.msg.chequeseries.id.invalid", "Cheque series with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeStockLotNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeStockLotNotFoundException(final Long id) {
super("error.msg.chequestocklot.id.invalid", "Cheque stock lot with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeStockTransferNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeStockTransferNotFoundException(final Long id) {
super("error.msg.chequestocktransfer.id.invalid", "Cheque stock transfer with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOKPENDINGREQUEST", action = "APPROVE")
@RequiredArgsConstructor
public class ApproveChequeBookPendingRequestCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.approvePendingRequest(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "CANCELLEAF")
@RequiredArgsConstructor
public class CancelLeafChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.cancelLeaf(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.requestChequeBook(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createSeries(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKLOT", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeStockLotCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.receiveStock(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.dispatchStockTransfer(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "DELETE")
@RequiredArgsConstructor
public class DeleteChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteSeries(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "ISSUE")
@RequiredArgsConstructor
public class IssueChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.issueChequeBook(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "MARKUSED")
@RequiredArgsConstructor
public class MarkUsedChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.markLeafUsed(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "RECEIVE")
@RequiredArgsConstructor
public class ReceiveChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.receiveStockTransfer(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectChequeBook(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOKPENDINGREQUEST", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeBookPendingRequestCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectPendingRequest(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectStockTransfer(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REPORTLOST")
@RequiredArgsConstructor
public class ReportLostChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.reportChequeBookLost(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKLOT", action = "REPORTLOST")
@RequiredArgsConstructor
public class ReportLostChequeStockLotCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.reportStockLotLost(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "RETURN")
@RequiredArgsConstructor
public class ReturnChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.returnChequeBook(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REVOKESTOPPAYMENT")
@RequiredArgsConstructor
public class RevokeStopPaymentChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.revokeStopPayment(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "STOPPAYMENT")
@RequiredArgsConstructor
public class StopPaymentChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.stopPayment(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateSeries(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.handler;
import lombok.RequiredArgsConstructor;
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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "WITHDRAW")
@RequiredArgsConstructor
public class WithdrawChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.withdrawChequeBook(command.entityId(), command);
}
}
@@ -0,0 +1,267 @@
/**
* 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.chequebook.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.chequebook.api.ChequeBookApiConstants;
import org.apache.fineract.portfolio.chequebook.api.ChequeBookApiJsonInputParams;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public final class ChequeBookCommandFromApiJsonDeserializer {
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreateSeries(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.seriesCreateParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_SERIES);
final String seriesCode = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_CODE.getValue()).value(seriesCode).notBlank().notExceedingLengthOf(20);
final String name = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.NAME.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NAME.getValue()).value(name).notBlank().notExceedingLengthOf(100);
final Integer leavesPerBook = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue()).value(leavesPerBook).notNull()
.integerGreaterThanZero();
final Integer reorderLevel = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue()).value(reorderLevel).ignoreIfNull()
.integerGreaterThanZero();
validateApplicableAccountType(element, v, true);
throwIfErrors(errors);
}
public void validateForUpdateSeries(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.seriesUpdateParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_SERIES);
final Integer leavesPerBook = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue()).value(leavesPerBook).ignoreIfNull()
.integerGreaterThanZero();
final Integer reorderLevel = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue()).value(reorderLevel).ignoreIfNull()
.integerGreaterThanZero();
validateApplicableAccountType(element, v, false);
throwIfErrors(errors);
}
private void validateApplicableAccountType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String applicableAccountType = this.fromApiJsonHelper
.extractStringNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue(), element);
// NB: isOneOfTheseStringValues lowercases the input before comparing, so the expected set must be lowercase
final DataValidatorBuilder builder = v.reset().parameter(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue())
.value(applicableAccountType);
if (required) {
builder.notBlank();
} else {
builder.ignoreIfNull();
}
builder.isOneOfTheseStringValues("savings", "current");
}
public void validateForStockLotReceive(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockLotReceiveParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_LOT);
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull().longGreaterThanZero();
final Long rangeStart = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_START.getValue()).value(rangeStart).notNull().longGreaterThanZero();
final Long rangeEnd = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_END.getValue()).value(rangeEnd).notNull().longGreaterThanZero();
if (rangeStart != null && rangeEnd != null && rangeEnd <= rangeStart) {
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_END.getValue()).value(rangeEnd)
.failWithCode("must.be.greater.than.range.start", "rangeEnd must be greater than rangeStart");
}
throwIfErrors(errors);
}
public void validateForStockLotReportLost(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockLotReportLostParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_LOT);
final Long fromSerialNo = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue()).value(fromSerialNo).notNull().longGreaterThanZero();
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REASON.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REASON.getValue()).value(reason).notBlank().notExceedingLengthOf(500);
throwIfErrors(errors);
}
public void validateForStockTransferDispatch(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockTransferDispatchParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_TRANSFER);
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue()).value(fromOfficeId).notNull().longGreaterThanZero();
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue()).value(toOfficeId).notNull().longGreaterThanZero();
if (fromOfficeId != null && fromOfficeId.equals(toOfficeId)) {
v.reset().parameter(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue()).value(toOfficeId)
.failWithCode("must.differ.from.from.office", "toOfficeId must differ from fromOfficeId");
}
final Long fromLotId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue()).value(fromLotId).notNull().longGreaterThanZero();
final Integer noOfLeaves = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(
ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue()).value(noOfLeaves).notNull().integerGreaterThanZero();
throwIfErrors(errors);
}
public void validateForBookRequest(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookRequestParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final String accountType = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()).value(accountType).notBlank();
if (StringUtils.isNotBlank(accountType) && ChequeAccountType.fromString(accountType) == null) {
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()).value(accountType)
.failWithCode("not.one.of.expected.enumerations", "SAVINGS, CURRENT");
}
final Long accountId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue()).value(accountId).notNull().longGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull().longGreaterThanZero();
final Integer noOfBooks = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue()).value(noOfBooks).notNull().integerGreaterThanZero();
// the denomination is picked at requisition time, matching a real cheque-book request slip
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
throwIfErrors(errors);
}
/** Issue no longer takes a series — it was already fixed at request time; only optional collection-ack. */
public void validateForBookIssue(final String json) {
parse(json, ChequeBookApiJsonInputParams.bookIssueParams());
}
public void validateForBookReject(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookRejectParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue()).value(reason).notBlank().notExceedingLengthOf(500);
throwIfErrors(errors);
}
/** Shared by stop-payment / revoke-stop-payment / report-lost / cancel-leaf on a book. */
public void validateForBookLeafAction(final String json, final boolean requireReason) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookLeafActionParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final Integer fromLeafSeq = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(
ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue()).value(fromLeafSeq).ignoreIfNull()
.integerGreaterThanZero();
final Integer toLeafSeq = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue()).value(toLeafSeq).ignoreIfNull()
.integerGreaterThanZero();
if (fromLeafSeq != null && toLeafSeq != null && toLeafSeq < fromLeafSeq) {
v.reset().parameter(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue()).value(toLeafSeq)
.failWithCode("must.be.greater.than.or.equal.to.from.leaf.seq", "toLeafSeq must not be less than fromLeafSeq");
}
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REASON.getValue(), element);
final DataValidatorBuilder reasonValidator = v.reset().parameter(ChequeBookApiJsonInputParams.REASON.getValue()).value(reason);
if (requireReason) {
reasonValidator.notBlank().notExceedingLengthOf(500);
} else {
reasonValidator.ignoreIfNull().notExceedingLengthOf(500);
}
throwIfErrors(errors);
}
private JsonElement parse(final String json, final Set<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,346 @@
/**
* 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.chequebook.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepositoryWrapper;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMapping;
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionSummaryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsHelper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* Per-leaf cheque-book issuance charge: rate lookup, balance-sufficiency validation, and the actual
* deduction + GL posting once a request is approved. Kept out of {@link ChequeBookWritePlatformServiceImpl}
* as its own bean, mirroring how {@code ChequeSeriesHardDeleteAttempter} was split out for the same reason
* — this is a self-contained unit of financial logic that's easier to review and reuse in isolation.
*
* <p>
* Reuses the existing "CHEQUE_BOOK" facility charge (also used by {@code FacilityChargeAutoApplyService}
* for the flat one-time activation-time fee) — its {@code amount} is interpreted here as a per-leaf rate.
* Both uses share the same {@code m_charge} row by design (confirmed with the bank): changing this
* charge's amount affects the activation fee and the per-leaf issuance rate together.
* </p>
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ChequeBookIssuanceChargeService {
private static final String FACILITY_TYPE = "CHEQUE_BOOK";
private final FacilityChargeMappingRepository facilityChargeMappingRepository;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final CurrentAccountRepositoryWrapper currentAccountRepositoryWrapper;
private final CurrentAccountRepository currentAccountRepository;
private final SavingsAccountChargeRepository savingsAccountChargeRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final JdbcTemplate jdbcTemplate;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final FineractRequestContextHolder fineractRequestContextHolder;
private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper;
private final SavingsHelper savingsHelper;
/** The configured charge, if the admin has set one up — absent means "no charge configured yet." */
private FacilityChargeMapping resolveMapping() {
return facilityChargeMappingRepository.findByFacilityType(FACILITY_TYPE)
.filter(m -> Boolean.TRUE.equals(m.getIsActive()))
.filter(m -> m.getCharge() != null && m.getCharge().isActive())
.orElse(null);
}
/**
* Per-leaf rate to charge for this book. A missing/inactive admin configuration degrades to "no
* charge" (zero) rather than blocking the whole cheque-book workflow.
*/
public BigDecimal ratePerLeaf() {
final FacilityChargeMapping mapping = resolveMapping();
if (mapping == null) {
log.warn("Cheque book issuance charge ('{}') is not configured or inactive - no charge will be applied.", FACILITY_TYPE);
return BigDecimal.ZERO;
}
return mapping.getCharge().getAmount();
}
/**
* Throws if this account cannot absorb {@code chargeAmount} without breaching its minimum required
* balance. Called both at request-submit time (fail fast) and again at approval time (authoritative
* re-check, in case the rate or the account's balance changed in between).
*/
public void assertBalanceCovers(final ChequeAccountType accountType, final Long accountId, final BigDecimal chargeAmount) {
if (chargeAmount == null || chargeAmount.signum() <= 0) {
return;
}
if (accountType == ChequeAccountType.SAVINGS) {
assertSavingsBalanceCovers(accountId, chargeAmount);
} else {
assertCurrentBalanceCovers(accountId, chargeAmount);
}
}
private void assertSavingsBalanceCovers(final Long accountId, final BigDecimal chargeAmount) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
final BigDecimal withdrawable = account.getWithdrawableBalance();
if (chargeAmount.compareTo(withdrawable) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.charge.insufficient.balance",
"Savings account " + accountId + " cannot absorb the cheque-book issuance charge of " + chargeAmount
+ " without breaching its minimum required balance. Available: " + withdrawable + ".",
accountId, chargeAmount, withdrawable);
}
}
private void assertCurrentBalanceCovers(final Long accountId, final BigDecimal chargeAmount) {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
final BigDecimal available = currentBalance.subtract(onHold);
if (!account.isAllowOverdraft() && chargeAmount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.charge.insufficient.balance",
"Current account " + accountId + " has insufficient available balance for the cheque-book issuance charge of "
+ chargeAmount + ". Available: " + available + ".",
accountId, chargeAmount, available);
}
final BigDecimal newBalance = currentBalance.subtract(chargeAmount);
if (account.isEnforceMinRequiredBalance() && account.getMinRequiredBalance() != null
&& account.getMinRequiredBalance().compareTo(BigDecimal.ZERO) > 0
&& newBalance.compareTo(account.getMinRequiredBalance()) < 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.charge.min.required.balance.breached",
"The cheque-book issuance charge of " + chargeAmount + " would breach the minimum required balance of "
+ account.getMinRequiredBalance() + " on current account " + accountId + ". Balance after charge would be "
+ newBalance + ".",
accountId, chargeAmount, account.getMinRequiredBalance());
}
}
private BigDecimal safeBalance(final BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
/**
* Reserves {@code chargeAmount} on the account by directly incrementing its on-hold-funds field — a
* plain entity mutation, not Fineract's native hold-amount command, so it creates no transaction row
* and never appears in the passbook/transactions list. Called right after {@link #assertBalanceCovers}
* passes at request-submit time, so the money is set aside for the pending duration rather than being
* spendable elsewhere while the request awaits a checker.
*/
public void placeHold(final ChequeAccountType accountType, final Long accountId, final BigDecimal chargeAmount) {
if (chargeAmount == null || chargeAmount.signum() <= 0) {
return;
}
if (accountType == ChequeAccountType.SAVINGS) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
account.holdAmount(chargeAmount);
this.savingsAccountRepositoryWrapper.saveAndFlush(account);
} else {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
account.setOnHoldFundsDerived(safeBalance(account.getOnHoldFundsDerived()).add(chargeAmount));
this.currentAccountRepository.saveAndFlush(account);
}
}
/**
* Releases a previously-placed hold — reverses the same field increment, again with no transaction row.
* Safe to call with a zero/null amount (nothing was held) — becomes a no-op.
*/
public void releaseHold(final ChequeAccountType accountType, final Long accountId, final BigDecimal chargeAmount) {
if (chargeAmount == null || chargeAmount.signum() <= 0) {
return;
}
if (accountType == ChequeAccountType.SAVINGS) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
account.releaseOnHoldAmount(chargeAmount);
this.savingsAccountRepositoryWrapper.saveAndFlush(account);
} else {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
final BigDecimal newHold = safeBalance(account.getOnHoldFundsDerived()).subtract(chargeAmount).max(BigDecimal.ZERO);
account.setOnHoldFundsDerived(newHold);
this.currentAccountRepository.saveAndFlush(account);
}
}
/**
* Deducts the charge and posts the corresponding GL entry. Only called from within an already-open
* transaction (the checker's approval) — the charge, GL entry, and book issuance commit or roll back
* together as one atomic unit, so no separate transaction boundary is introduced here.
*/
public void deduct(final ChequeAccountType accountType, final Long accountId, final Office office, final BigDecimal chargeAmount,
final Long chequeBookId) {
if (chargeAmount == null || chargeAmount.signum() <= 0) {
return;
}
final FacilityChargeMapping mapping = resolveMapping();
if (mapping == null || mapping.getCharge() == null) {
log.warn("Cheque book {} approved but issuance charge is no longer configured - skipping charge deduction.", chequeBookId);
return;
}
final Charge charge = mapping.getCharge();
final LocalDate today = DateUtils.getBusinessLocalDate();
final Long ownAccountGlId = accountType == ChequeAccountType.SAVINGS ? getSavingsControlGlId(accountId)
: getCurrentControlGlId(accountId);
final Long incomeGlId = charge.getAccount() != null ? charge.getAccount().getId() : null;
if (ownAccountGlId == null || incomeGlId == null) {
log.warn("Cannot post GL for cheque book {} issuance charge - missing control GL ({}) or income GL ({}); "
+ "charge deduction skipped to avoid an unbalanced ledger.", chequeBookId, ownAccountGlId, incomeGlId);
return;
}
if (accountType == ChequeAccountType.SAVINGS) {
deductFromSavings(accountId, charge, chargeAmount, today);
} else {
deductFromCurrent(accountId, office, chargeAmount, today);
}
postChargeJournalEntry(office.getId(), charge.getCurrencyCode(), today, ownAccountGlId, incomeGlId, chargeAmount, chequeBookId,
accountId);
log.info("Deducted cheque book issuance charge {} from {} account {} for book {}", chargeAmount, accountType, accountId,
chequeBookId);
}
private void deductFromSavings(final Long accountId, final Charge charge, final BigDecimal chargeAmount, final LocalDate today) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
// a plain repository fetch leaves these transient collaborators unset — recalculateBalancesAfterFacilityCharges()
// NPEs without them (they're normally wired in by the account-assembly step of the full savings write flow)
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(charge, chargeAmount, null, null,
null, true, null, null);
accountCharge.update(account);
account.charges().add(accountCharge);
this.savingsAccountChargeRepository.saveAndFlush(accountCharge);
final Money amountMoney = Money.of(account.getCurrency(), chargeAmount);
final SavingsAccountTransaction txn = account.payCharge(accountCharge, amountMoney, today, false, null);
this.savingsAccountTransactionRepository.saveAndFlush(txn);
account.recalculateBalancesAfterFacilityCharges(today);
this.savingsAccountRepositoryWrapper.saveAndFlush(account);
}
private void deductFromCurrent(final Long accountId, final Office office, final BigDecimal chargeAmount, final LocalDate today) {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
final BigDecimal newBalance = safeBalance(account.getAccountBalanceDerived()).subtract(chargeAmount);
final CurrentAccountTransaction txn = CurrentAccountTransaction.chargeDeduction(account, office, chargeAmount, today);
txn.setRunningBalanceDerived(newBalance);
this.currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
this.currentAccountRepository.saveAndFlush(account);
}
/** Same shape as {@code getSavingsControlGlId}/{@code getCurrentControlGlId} in ClerkDisbursementRequestApiResource. */
private Long getSavingsControlGlId(final Long savingsAccountId) {
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("SELECT apm.gl_account_id FROM m_savings_account sa "
+ "JOIN acc_product_mapping apm ON apm.product_id = sa.product_id "
+ "WHERE sa.id = ? AND apm.product_type = 2 AND apm.financial_account_type = 2 LIMIT 1", savingsAccountId);
return rows.isEmpty() ? null : ((Number) rows.get(0).get("gl_account_id")).longValue();
}
private Long getCurrentControlGlId(final Long currentAccountId) {
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("SELECT apm.gl_account_id FROM m_current_account ca "
+ "JOIN acc_product_mapping apm ON apm.product_id = ca.product_id "
+ "WHERE ca.id = ? AND apm.product_type = 2 AND apm.financial_account_type = 2 LIMIT 1", currentAccountId);
return rows.isEmpty() ? null : ((Number) rows.get(0).get("gl_account_id")).longValue();
}
/** DR the account's own control GL (liability down), CR the charge's income GL (fee income). */
private void postChargeJournalEntry(final Long officeId, final String currencyCode, final LocalDate txnDate, final Long debitGlId,
final Long creditGlId, final BigDecimal amount, final Long chequeBookId, final Long accountId) {
final JsonArray debitArr = new JsonArray();
final JsonObject debit = new JsonObject();
debit.addProperty("glAccountId", debitGlId);
debit.addProperty("amount", amount);
debitArr.add(debit);
final JsonArray creditArr = new JsonArray();
final JsonObject credit = new JsonObject();
credit.addProperty("glAccountId", creditGlId);
credit.addProperty("amount", amount);
creditArr.add(credit);
final JsonObject payload = new JsonObject();
payload.addProperty("officeId", officeId);
payload.addProperty("transactionDate", txnDate.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")));
payload.addProperty("dateFormat", "dd MMMM yyyy");
payload.addProperty("locale", "en");
payload.addProperty("currencyCode", currencyCode != null ? currencyCode : "INR");
payload.addProperty("comments", "Cheque book issuance charge - book #" + chequeBookId);
payload.add("debits", debitArr);
payload.add("credits", creditArr);
final CommandWrapper cmd = new CommandWrapperBuilder().createJournalEntry().withJson(payload.toString()).build();
this.fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null);
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(cmd);
if (result == null) {
log.warn("GL journal entry for cheque book {} issuance charge did not return a result.", chequeBookId);
return;
}
// Without this, the debit row posts fine but the Journal Entries screen shows no "Account No" for it —
// link it back to the customer's account, same as ClerkDisbursementRequestApiResource's direct debits do.
final String glTxnId = result.getTransactionId();
if (glTxnId != null) {
linkDebitEntryToAccount(glTxnId, accountId);
}
}
/** Links the DEBIT row (type_enum=2) of this journal transaction to the customer's account so it shows an Account No. */
private void linkDebitEntryToAccount(final String glTxnId, final Long accountId) {
final List<Long> rowIds = this.jdbcTemplate.queryForList(
"SELECT id FROM acc_gl_journal_entry WHERE transaction_id = ? AND type_enum = 2 ORDER BY id ASC LIMIT 1", Long.class,
glTxnId);
if (!rowIds.isEmpty()) {
this.jdbcTemplate.update("UPDATE acc_gl_journal_entry SET entity_type_enum = 2, entity_id = ? WHERE id = ?", accountId,
rowIds.get(0));
}
}
}
@@ -0,0 +1,63 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.chequebook.service;
import java.util.List;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
public interface ChequeBookReadPlatformService {
/** {@code status}: "ACTIVE" / "INACTIVE" — maps to the series' is_active flag. */
Page<ChequeSeriesData> retrieveAllSeries(String status, Integer offset, Integer limit);
ChequeSeriesData retrieveSeries(Long id);
Page<ChequeStockLotData> retrieveStockLots(Long officeId, Long seriesId, String status, Integer offset, Integer limit);
ChequeStockLotData retrieveStockLot(Long id);
Page<ChequeStockTransferData> retrieveStockTransfers(String status, Integer offset, Integer limit);
ChequeStockTransferData retrieveStockTransfer(Long id);
Page<ChequeBookData> retrieveAllBooks(String accountType, Long accountId, Long officeId, String status, Integer offset,
Integer limit);
ChequeBookData retrieveBook(Long id);
List<ChequeLeafData> retrieveLeaves(Long chequeBookId);
/**
* Quick-lookup — the single teller/counter operation that answers "is this cheque number safe to honour".
* A bare serial number is only unique within its own series, so more than one series can legitimately match.
*/
List<ChequeLeafData> retrieveLeafBySerialNo(Long leafSerialNo);
/** Bespoke maker-checker queue for series/stock/book creation requests — see {@code m_chequebook_pending_request}. */
Page<ChequeBookPendingRequestData> retrievePendingRequests(String status, String fromDate, String toDate, Integer offset,
Integer limit);
ChequeBookPendingRequestData retrievePendingRequest(Long id);
}
@@ -0,0 +1,504 @@
/**
* 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.chequebook.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookPendingRequestNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeLeafNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeSeriesNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockLotNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockTransferNotFoundException;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final SavingsAccountRepository savingsAccountRepository;
private final CurrentAccountRepository currentAccountRepository;
private final SeriesMapper seriesMapper = new SeriesMapper();
private final StockLotMapper stockLotMapper = new StockLotMapper();
private final StockTransferMapper stockTransferMapper = new StockTransferMapper();
private final BookMapper bookMapper = new BookMapper();
private final LeafMapper leafMapper = new LeafMapper();
private final PendingRequestMapper pendingRequestMapper = new PendingRequestMapper();
@Override
public Page<ChequeSeriesData> retrieveAllSeries(final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if ("ACTIVE".equalsIgnoreCase(status)) {
where.append(" and is_active = true");
} else if ("INACTIVE".equalsIgnoreCase(status)) {
where.append(" and is_active = false");
}
final String countSql = "select count(*) from (select " + this.seriesMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.seriesMapper.schema()).append(where)
.append(" order by id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeSeriesData> series = this.jdbcTemplate.query(pageSql.toString(), this.seriesMapper, pageArgs.toArray());
return new Page<>(series, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeSeriesData retrieveSeries(final Long id) {
try {
final String sql = "select " + this.seriesMapper.schema() + " where id = ?";
return this.jdbcTemplate.queryForObject(sql, this.seriesMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeSeriesNotFoundException(id);
}
}
@Override
public Page<ChequeStockLotData> retrieveStockLots(final Long officeId, final Long seriesId, final String status,
final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (officeId != null) {
where.append(" and l.office_id = ?");
args.add(officeId);
}
if (seriesId != null) {
where.append(" and l.series_id = ?");
args.add(seriesId);
}
if (StringUtils.isNotBlank(status)) {
where.append(" and l.status = ?");
args.add(status.trim().toUpperCase());
}
final String countSql = "select count(*) from (select " + this.stockLotMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.stockLotMapper.schema()).append(where)
.append(" order by l.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeStockLotData> lots = this.jdbcTemplate.query(pageSql.toString(), this.stockLotMapper, pageArgs.toArray());
return new Page<>(lots, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeStockLotData retrieveStockLot(final Long id) {
try {
final String sql = "select " + this.stockLotMapper.schema() + " where l.id = ?";
return this.jdbcTemplate.queryForObject(sql, this.stockLotMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeStockLotNotFoundException(id);
}
}
@Override
public Page<ChequeStockTransferData> retrieveStockTransfers(final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(status)) {
where.append(" and t.status = ?");
args.add(status.trim().toUpperCase());
}
final String countSql = "select count(*) from (select " + this.stockTransferMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.stockTransferMapper.schema()).append(where)
.append(" order by t.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeStockTransferData> transfers = this.jdbcTemplate.query(pageSql.toString(), this.stockTransferMapper,
pageArgs.toArray());
return new Page<>(transfers, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeStockTransferData retrieveStockTransfer(final Long id) {
try {
final String sql = "select " + this.stockTransferMapper.schema() + " where t.id = ?";
return this.jdbcTemplate.queryForObject(sql, this.stockTransferMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeStockTransferNotFoundException(id);
}
}
@Override
public Page<ChequeBookData> retrieveAllBooks(final String accountType, final Long accountId, final Long officeId,
final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(accountType)) {
where.append(" and b.account_type = ?");
args.add(accountType.trim().toUpperCase());
}
if (accountId != null) {
where.append(" and b.account_id = ?");
args.add(accountId);
}
if (officeId != null) {
where.append(" and b.office_id = ?");
args.add(officeId);
}
if (StringUtils.isNotBlank(status)) {
where.append(" and b.status = ?");
args.add(status.trim().toUpperCase());
}
// wrap the mapper's own select (joins to office/series) so the count always matches the page query 1:1
final String countSql = "select count(*) from (select " + this.bookMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.bookMapper.schema()).append(where)
.append(" order by b.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeBookData> books = this.jdbcTemplate.query(pageSql.toString(), this.bookMapper, pageArgs.toArray());
final List<ChequeBookData> withAccountInfo = books.stream().map(this::withAccountInfo).toList();
return new Page<>(withAccountInfo, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeBookData retrieveBook(final Long id) {
try {
final String sql = "select " + this.bookMapper.schema() + " where b.id = ?";
final ChequeBookData book = this.jdbcTemplate.queryForObject(sql, this.bookMapper, id);
return withAccountInfo(book);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeBookNotFoundException(id);
}
}
@Override
public List<ChequeLeafData> retrieveLeaves(final Long chequeBookId) {
final String sql = "select " + this.leafMapper.schema() + " where f.cheque_book_id = ? order by f.leaf_seq_no asc";
final List<ChequeLeafData> leaves = this.jdbcTemplate.query(sql, this.leafMapper, chequeBookId);
return leaves.stream().map(this::withAccountInfo).toList();
}
@Override
public List<ChequeLeafData> retrieveLeafBySerialNo(final Long leafSerialNo) {
// new stock lots are now checked for overlap across every series (see
// ChequeStockLotRepository.countOverlappingAnySeries), so a bare serial number stays globally unique going
// forward. Older lots received before that check existed can still collide, so this stays defensive and
// may return more than one match for pre-existing data.
final String sql = "select " + this.leafMapper.schema() + " where f.leaf_serial_no = ? order by s.series_code asc";
final List<ChequeLeafData> leaves = this.jdbcTemplate.query(sql, this.leafMapper, leafSerialNo);
if (leaves.isEmpty()) {
throw new ChequeLeafNotFoundException(leafSerialNo);
}
return leaves.stream().map(this::withAccountInfo).toList();
}
@Override
public Page<ChequeBookPendingRequestData> retrievePendingRequests(final String status, final String fromDate, final String toDate,
final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(status)) {
where.append(" and status = ?");
args.add(status.trim().toUpperCase());
}
if (StringUtils.isNotBlank(fromDate)) {
where.append(" and submitted_on::date >= ?::date");
args.add(fromDate);
}
if (StringUtils.isNotBlank(toDate)) {
where.append(" and submitted_on::date <= ?::date");
args.add(toDate);
}
final String countSql = "select count(*) from m_chequebook_pending_request" + where;
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.pendingRequestMapper.schema()).append(where)
.append(" order by id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeBookPendingRequestData> requests = this.jdbcTemplate.query(pageSql.toString(), this.pendingRequestMapper,
pageArgs.toArray());
return new Page<>(requests, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeBookPendingRequestData retrievePendingRequest(final Long id) {
try {
final String sql = "select " + this.pendingRequestMapper.schema() + " where id = ?";
return this.jdbcTemplate.queryForObject(sql, this.pendingRequestMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeBookPendingRequestNotFoundException(id);
}
}
/** Best-effort account/client display-name resolution — a missing account never fails the whole read. */
private ChequeBookData withAccountInfo(final ChequeBookData book) {
final AccountDisplay display = resolveAccountDisplay(book.getAccountType(), book.getAccountId());
return ChequeBookData.builder().id(book.getId()).accountType(book.getAccountType()).accountId(book.getAccountId())
.accountNo(display.accountNo).clientName(display.clientName).officeId(book.getOfficeId())
.officeName(book.getOfficeName()).seriesId(book.getSeriesId()).seriesCode(book.getSeriesCode())
.requestedDate(book.getRequestedDate()).requestedBy(book.getRequestedBy()).noOfBooks(book.getNoOfBooks())
.noOfLeavesRequested(book.getNoOfLeavesRequested()).status(book.getStatus()).issueDate(book.getIssueDate())
.issuedBy(book.getIssuedBy()).lotId(book.getLotId()).rangeStart(book.getRangeStart()).rangeEnd(book.getRangeEnd())
.totalLeaves(book.getTotalLeaves()).rejectionReason(book.getRejectionReason())
.collectionAcknowledged(book.isCollectionAcknowledged()).collectedDate(book.getCollectedDate())
.collectedBy(book.getCollectedBy()).build();
}
private ChequeLeafData withAccountInfo(final ChequeLeafData leaf) {
final AccountDisplay display = resolveAccountDisplay(leaf.getAccountType(), leaf.getAccountId());
return ChequeLeafData.builder().id(leaf.getId()).chequeBookId(leaf.getChequeBookId()).lotId(leaf.getLotId())
.seriesCode(leaf.getSeriesCode()).leafSerialNo(leaf.getLeafSerialNo()).leafSeqNo(leaf.getLeafSeqNo()).status(leaf.getStatus())
.stopReason(leaf.getStopReason()).stoppedBy(leaf.getStoppedBy()).stoppedOn(leaf.getStoppedOn())
.lostReason(leaf.getLostReason()).lostReportedBy(leaf.getLostReportedBy()).lostReportedOn(leaf.getLostReportedOn())
.cancelledReason(leaf.getCancelledReason()).cancelledBy(leaf.getCancelledBy()).cancelledOn(leaf.getCancelledOn())
.usedReason(leaf.getUsedReason()).usedBy(leaf.getUsedBy()).usedOn(leaf.getUsedOn())
.returnReason(leaf.getReturnReason()).returnedBy(leaf.getReturnedBy()).returnedOn(leaf.getReturnedOn())
.accountType(leaf.getAccountType()).accountId(leaf.getAccountId()).accountNo(display.accountNo)
.clientName(display.clientName).officeName(leaf.getOfficeName()).build();
}
private AccountDisplay resolveAccountDisplay(final String accountType, final Long accountId) {
if (accountType == null || accountId == null) {
return new AccountDisplay(null, null);
}
final ChequeAccountType type = ChequeAccountType.fromString(accountType);
if (type == ChequeAccountType.SAVINGS) {
final SavingsAccount account = this.savingsAccountRepository.findById(accountId).orElse(null);
if (account == null) {
return new AccountDisplay(null, null);
}
final String clientName = account.getClient() == null ? null : account.getClient().getDisplayName();
return new AccountDisplay(account.getAccountNumber(), clientName);
}
if (type == ChequeAccountType.CURRENT) {
final CurrentAccount account = this.currentAccountRepository.findById(accountId).orElse(null);
if (account == null) {
return new AccountDisplay(null, null);
}
final String clientName = account.getClient() == null ? null : account.getClient().getDisplayName();
return new AccountDisplay(account.getAccountNo(), clientName);
}
return new AccountDisplay(null, null);
}
private static final class AccountDisplay {
private final String accountNo;
private final String clientName;
private AccountDisplay(final String accountNo, final String clientName) {
this.accountNo = accountNo;
this.clientName = clientName;
}
}
private static final class SeriesMapper implements RowMapper<ChequeSeriesData> {
String schema() {
return " id, series_code, name, leaves_per_book, applicable_account_type, cts_compliant, reorder_level, is_active "
+ "from m_cheque_series";
}
@Override
public ChequeSeriesData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeSeriesData.builder().id(rs.getLong("id")).seriesCode(rs.getString("series_code")).name(rs.getString("name"))
.leavesPerBook((Integer) rs.getObject("leaves_per_book"))
.applicableAccountType(rs.getString("applicable_account_type")).ctsCompliant(rs.getBoolean("cts_compliant"))
.reorderLevel((Integer) rs.getObject("reorder_level")).active(rs.getBoolean("is_active")).build();
}
}
private static final class StockLotMapper implements RowMapper<ChequeStockLotData> {
String schema() {
return " l.id as id, l.series_id as series_id, s.series_code as series_code, s.leaves_per_book as leaves_per_book, "
+ "l.office_id as office_id, o.name as office_name, l.range_start as range_start, l.range_end as range_end, "
+ "l.next_serial_no as next_serial_no, l.source as source, l.source_ref_id as source_ref_id, "
+ "l.batch_ref as batch_ref, l.status as status, s.reorder_level as reorder_level "
+ "from m_cheque_stock_lot l join m_cheque_series s on s.id = l.series_id join m_office o on o.id = l.office_id";
}
@Override
public ChequeStockLotData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
final long rangeEnd = rs.getLong("range_end");
final long nextSerialNo = rs.getLong("next_serial_no");
final long remaining = rangeEnd - nextSerialNo + 1;
final Integer leavesPerBook = (Integer) rs.getObject("leaves_per_book");
// full issuable books of this denomination — a trailing partial book cannot be issued
final Long remainingBooks = leavesPerBook != null && leavesPerBook > 0 ? remaining / leavesPerBook : null;
final Integer reorderLevel = (Integer) rs.getObject("reorder_level");
final boolean lowStock = reorderLevel != null && remaining <= reorderLevel;
return ChequeStockLotData.builder().id(rs.getLong("id")).seriesId(rs.getLong("series_id"))
.seriesCode(rs.getString("series_code")).leavesPerBook(leavesPerBook).officeId(rs.getLong("office_id"))
.officeName(rs.getString("office_name")).rangeStart(rs.getLong("range_start")).rangeEnd(rangeEnd)
.nextSerialNo(nextSerialNo).remainingLeaves(remaining).remainingBooks(remainingBooks)
.source(rs.getString("source")).sourceRefId((Long) rs.getObject("source_ref_id"))
.batchRef(rs.getString("batch_ref")).status(rs.getString("status")).lowStock(lowStock).build();
}
}
private static final class StockTransferMapper implements RowMapper<ChequeStockTransferData> {
String schema() {
return " t.id as id, t.series_id as series_id, s.series_code as series_code, t.from_office_id as from_office_id, "
+ "fo.name as from_office_name, t.to_office_id as to_office_id, tofc.name as to_office_name, "
+ "t.from_lot_id as from_lot_id, t.to_lot_id as to_lot_id, t.range_start as range_start, "
+ "t.range_end as range_end, t.total_leaves as total_leaves, t.dispatch_date as dispatch_date, "
+ "t.status as status, t.received_date as received_date, t.dispatched_by as dispatched_by, "
+ "t.received_by as received_by "
+ "from m_cheque_stock_transfer t join m_cheque_series s on s.id = t.series_id "
+ "join m_office fo on fo.id = t.from_office_id join m_office tofc on tofc.id = t.to_office_id";
}
@Override
public ChequeStockTransferData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeStockTransferData.builder().id(rs.getLong("id")).seriesId(rs.getLong("series_id"))
.seriesCode(rs.getString("series_code")).fromOfficeId(rs.getLong("from_office_id"))
.fromOfficeName(rs.getString("from_office_name")).toOfficeId(rs.getLong("to_office_id"))
.toOfficeName(rs.getString("to_office_name")).fromLotId(rs.getLong("from_lot_id"))
.toLotId(rs.getLong("to_lot_id")).rangeStart(rs.getLong("range_start")).rangeEnd(rs.getLong("range_end"))
.totalLeaves((Integer) rs.getObject("total_leaves")).dispatchDate(rs.getObject("dispatch_date", LocalDate.class))
.status(rs.getString("status")).receivedDate(rs.getObject("received_date", LocalDate.class))
.dispatchedBy((Long) rs.getObject("dispatched_by")).receivedBy((Long) rs.getObject("received_by")).build();
}
}
private static final class BookMapper implements RowMapper<ChequeBookData> {
String schema() {
return " b.id as id, b.account_type as account_type, b.account_id as account_id, b.office_id as office_id, "
+ "o.name as office_name, b.series_id as series_id, s.series_code as series_code, "
+ "b.requested_date as requested_date, b.requested_by as requested_by, b.no_of_books as no_of_books, "
+ "b.no_of_leaves_requested as no_of_leaves_requested, b.status as status, b.issue_date as issue_date, "
+ "b.issued_by as issued_by, b.lot_id as lot_id, b.range_start as range_start, b.range_end as range_end, "
+ "b.total_leaves as total_leaves, b.rejection_reason as rejection_reason, "
+ "b.collection_acknowledged as collection_acknowledged, b.collected_date as collected_date, "
+ "b.collected_by as collected_by "
+ "from m_cheque_book b join m_office o on o.id = b.office_id left join m_cheque_series s on s.id = b.series_id";
}
@Override
public ChequeBookData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeBookData.builder().id(rs.getLong("id")).accountType(rs.getString("account_type"))
.accountId(rs.getLong("account_id")).accountNo(null).clientName(null).officeId(rs.getLong("office_id"))
.officeName(rs.getString("office_name")).seriesId((Long) rs.getObject("series_id"))
.seriesCode(rs.getString("series_code")).requestedDate(rs.getObject("requested_date", LocalDate.class))
.requestedBy((Long) rs.getObject("requested_by")).noOfBooks((Integer) rs.getObject("no_of_books"))
.noOfLeavesRequested((Integer) rs.getObject("no_of_leaves_requested"))
.status(rs.getString("status")).issueDate(rs.getObject("issue_date", LocalDate.class))
.issuedBy((Long) rs.getObject("issued_by")).lotId((Long) rs.getObject("lot_id"))
.rangeStart((Long) rs.getObject("range_start")).rangeEnd((Long) rs.getObject("range_end"))
.totalLeaves((Integer) rs.getObject("total_leaves")).rejectionReason(rs.getString("rejection_reason"))
.collectionAcknowledged(rs.getBoolean("collection_acknowledged"))
.collectedDate(rs.getObject("collected_date", LocalDate.class)).collectedBy((Long) rs.getObject("collected_by"))
.build();
}
}
private static final class LeafMapper implements RowMapper<ChequeLeafData> {
String schema() {
return " f.id as id, f.cheque_book_id as cheque_book_id, f.lot_id as lot_id, s.series_code as series_code, "
+ "f.leaf_serial_no as leaf_serial_no, "
+ "f.leaf_seq_no as leaf_seq_no, f.status as status, f.stop_reason as stop_reason, f.stopped_by as stopped_by, "
+ "f.stopped_on as stopped_on, f.lost_reason as lost_reason, f.lost_reported_by as lost_reported_by, "
+ "f.lost_reported_on as lost_reported_on, f.cancelled_reason as cancelled_reason, "
+ "f.cancelled_by as cancelled_by, f.cancelled_on as cancelled_on, "
+ "f.used_reason as used_reason, f.used_by as used_by, f.used_on as used_on, "
+ "f.return_reason as return_reason, f.returned_by as returned_by, f.returned_on as returned_on, "
+ "b.account_type as account_type, b.account_id as account_id, bo.name as book_office_name, "
+ "lo.name as lot_office_name "
+ "from m_cheque_leaf f left join m_cheque_book b on b.id = f.cheque_book_id left join m_office bo on bo.id = b.office_id "
+ "join m_cheque_stock_lot l on l.id = f.lot_id join m_office lo on lo.id = l.office_id "
+ "join m_cheque_series s on s.id = l.series_id";
}
@Override
public ChequeLeafData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
final String bookOfficeName = rs.getString("book_office_name");
final String officeName = bookOfficeName != null ? bookOfficeName : rs.getString("lot_office_name");
return ChequeLeafData.builder().id(rs.getLong("id")).chequeBookId((Long) rs.getObject("cheque_book_id"))
.lotId(rs.getLong("lot_id")).seriesCode(rs.getString("series_code")).leafSerialNo(rs.getLong("leaf_serial_no"))
.leafSeqNo((Integer) rs.getObject("leaf_seq_no")).status(rs.getString("status"))
.stopReason(rs.getString("stop_reason")).stoppedBy((Long) rs.getObject("stopped_by"))
.stoppedOn(rs.getObject("stopped_on", OffsetDateTime.class)).lostReason(rs.getString("lost_reason"))
.lostReportedBy((Long) rs.getObject("lost_reported_by"))
.lostReportedOn(rs.getObject("lost_reported_on", OffsetDateTime.class))
.cancelledReason(rs.getString("cancelled_reason")).cancelledBy((Long) rs.getObject("cancelled_by"))
.cancelledOn(rs.getObject("cancelled_on", OffsetDateTime.class)).usedReason(rs.getString("used_reason"))
.usedBy((Long) rs.getObject("used_by")).usedOn(rs.getObject("used_on", OffsetDateTime.class))
.returnReason(rs.getString("return_reason")).returnedBy((Long) rs.getObject("returned_by"))
.returnedOn(rs.getObject("returned_on", OffsetDateTime.class))
.accountType(rs.getString("account_type")).accountId((Long) rs.getObject("account_id")).accountNo(null)
.clientName(null).officeName(officeName).build();
}
}
private static final class PendingRequestMapper implements RowMapper<ChequeBookPendingRequestData> {
String schema() {
return " id, request_type as request_type, payload_json as payload_json, status as status, maker_id as maker_id, "
+ "maker_name as maker_name, maker_username as maker_username, submitted_on as submitted_on, "
+ "checker_id as checker_id, checker_name as checker_name, checker_username as checker_username, "
+ "checked_on as checked_on, rejection_reason as rejection_reason, created_resource_id as created_resource_id, "
+ "activity_log as activity_log from m_chequebook_pending_request";
}
@Override
public ChequeBookPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeBookPendingRequestData.builder().id(rs.getLong("id")).requestType(rs.getString("request_type"))
.payloadJson(rs.getString("payload_json")).status(rs.getString("status")).makerId(rs.getLong("maker_id"))
.makerName(rs.getString("maker_name")).makerUsername(rs.getString("maker_username"))
.submittedOn(rs.getObject("submitted_on", OffsetDateTime.class)).checkerId((Long) rs.getObject("checker_id"))
.checkerName(rs.getString("checker_name")).checkerUsername(rs.getString("checker_username"))
.checkedOn(rs.getObject("checked_on", OffsetDateTime.class)).rejectionReason(rs.getString("rejection_reason"))
.createdResourceId((Long) rs.getObject("created_resource_id")).activityLog(rs.getString("activity_log")).build();
}
}
}
@@ -0,0 +1,81 @@
/**
* 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.chequebook.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface ChequeBookWritePlatformService {
CommandProcessingResult createSeries(JsonCommand command);
CommandProcessingResult updateSeries(Long id, JsonCommand command);
CommandProcessingResult deleteSeries(Long id);
/** HO/branch bulk receipt from the press — creates an AVAILABLE stock lot. */
CommandProcessingResult receiveStock(JsonCommand command);
/** Reports the remaining undrawn suffix of a stock lot's range as lost/stolen before ever being issued. */
CommandProcessingResult reportStockLotLost(Long lotId, JsonCommand command);
/** Dispatches a sub-range from an available lot to another office. */
CommandProcessingResult dispatchStockTransfer(JsonCommand command);
CommandProcessingResult receiveStockTransfer(Long transferId, JsonCommand command);
CommandProcessingResult rejectStockTransfer(Long transferId, JsonCommand command);
/** Customer/branch requisition — no stock drawn yet. */
CommandProcessingResult requestChequeBook(JsonCommand command);
/** Draws a range from an available lot at the book's office and materializes the leaf rows. */
CommandProcessingResult issueChequeBook(Long chequeBookId, JsonCommand command);
CommandProcessingResult rejectChequeBook(Long chequeBookId, JsonCommand command);
/** Account closed — withdraws the book's remaining ACTIVE leaves. */
CommandProcessingResult withdrawChequeBook(Long chequeBookId, JsonCommand command);
CommandProcessingResult stopPayment(Long chequeBookId, JsonCommand command);
CommandProcessingResult revokeStopPayment(Long chequeBookId, JsonCommand command);
CommandProcessingResult reportChequeBookLost(Long chequeBookId, JsonCommand command);
CommandProcessingResult cancelLeaf(Long chequeBookId, JsonCommand command);
/** Flags leaves in range as RETURNED — presented and dishonoured/bounced. Terminal, like reportlost. */
CommandProcessingResult returnChequeBook(Long chequeBookId, JsonCommand command);
/**
* Manually records that leaves cleared, once a branch sees them on the bank/clearing statement — never
* automatic, since this CBS has no clearing/presentment integration to detect it on its own.
*/
CommandProcessingResult markLeafUsed(Long chequeBookId, JsonCommand command);
/**
* Checker approves a pending series/stock/book request — replays the maker's original submission and
* actually creates the record. Fails if the checker is the same user who submitted it.
*/
CommandProcessingResult approvePendingRequest(Long pendingRequestId);
/** Checker rejects a pending series/stock/book request. Fails if the checker is the original maker. */
CommandProcessingResult rejectPendingRequest(Long pendingRequestId, JsonCommand command);
}
@@ -0,0 +1,820 @@
/**
* 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.chequebook.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
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.GeneralPlatformDomainRuleException;
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.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.api.ChequeBookApiJsonInputParams;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBook;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequest;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequestRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequestType;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeaf;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeafRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeafStatus;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeries;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesApplicability;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLot;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotStatus;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockTransfer;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockTransferRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.serialization.ChequeBookCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepositoryWrapper;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfig;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfig;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class ChequeBookWritePlatformServiceImpl implements ChequeBookWritePlatformService {
private final PlatformSecurityContext context;
private final ChequeBookCommandFromApiJsonDeserializer validator;
private final ChequeSeriesRepository chequeSeriesRepository;
private final ChequeSeriesRepositoryWrapper chequeSeriesRepositoryWrapper;
private final ChequeStockLotRepository chequeStockLotRepository;
private final ChequeStockLotRepositoryWrapper chequeStockLotRepositoryWrapper;
private final ChequeStockTransferRepositoryWrapper chequeStockTransferRepositoryWrapper;
private final ChequeBookRepositoryWrapper chequeBookRepositoryWrapper;
private final ChequeLeafRepository chequeLeafRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final CurrentAccountRepositoryWrapper currentAccountRepositoryWrapper;
private final CurrentProductFacilityConfigRepository currentProductFacilityConfigRepository;
private final ChequeBookPendingRequestRepositoryWrapper chequeBookPendingRequestRepositoryWrapper;
private final ChequeSeriesHardDeleteAttempter chequeSeriesHardDeleteAttempter;
private final ChequeBookIssuanceChargeService chequeBookIssuanceChargeService;
private final FromJsonHelper fromApiJsonHelper;
private final Gson gson = new GsonBuilder().serializeNulls().create();
// ============================= Series =============================
@Transactional
@Override
public CommandProcessingResult createSeries(final JsonCommand command) {
this.validator.validateForCreateSeries(command.json());
final String seriesCode = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue());
if (this.chequeSeriesRepository.existsBySeriesCode(seriesCode)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeseries.duplicate.code",
"A cheque series with code " + seriesCode + " already exists.", seriesCode);
}
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.CREATE_SERIES, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*/
private Long finalizeCreateSeries(final JsonCommand command, final Long makerId) {
final String seriesCode = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue());
if (this.chequeSeriesRepository.existsBySeriesCode(seriesCode)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeseries.duplicate.code",
"A cheque series with code " + seriesCode + " already exists.", seriesCode);
}
final String name = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.NAME.getValue());
final Integer leavesPerBook = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue());
final ChequeSeriesApplicability applicableAccountType = ChequeSeriesApplicability
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue()));
final Boolean ctsCompliant = command.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.CTS_COMPLIANT.getValue());
final Integer reorderLevel = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue());
final ChequeSeries series = ChequeSeries.createNew(seriesCode, name, leavesPerBook, applicableAccountType,
ctsCompliant == null || ctsCompliant, reorderLevel, makerId);
final ChequeSeries saved = this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult updateSeries(final Long id, final JsonCommand command) {
this.validator.validateForUpdateSeries(command.json());
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(id);
final String name = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.NAME.getValue());
final Integer leavesPerBook = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue());
final ChequeSeriesApplicability applicableAccountType = ChequeSeriesApplicability
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue()));
final Boolean ctsCompliant = command.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.CTS_COMPLIANT.getValue());
final Integer reorderLevel = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue());
series.updateDetails(name, leavesPerBook, applicableAccountType, ctsCompliant, reorderLevel);
this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).build();
}
@Transactional
@Override
public CommandProcessingResult deleteSeries(final Long id) {
this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(id); // fail fast for the maker if it doesn't exist
final JsonObject payload = new JsonObject();
payload.addProperty(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), id);
final Long pendingId = insertPendingRequest(ChequeBookPendingRequestType.DELETE_SERIES, this.gson.toJson(payload));
return new CommandProcessingResultBuilder().withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request. Permanently removes the series if the
* database's own foreign-key constraints allow it (nothing ever referenced it); otherwise falls back to
* the same deactivate used previously, preserving every stock lot/book/leaf already tied to this series.
*/
private Long finalizeDeleteSeries(final Long seriesId) {
final boolean hardDeleted = this.chequeSeriesHardDeleteAttempter.attemptDelete(seriesId);
if (!hardDeleted) {
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
series.deactivate();
this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
}
return seriesId;
}
// ============================= Stock =============================
@Transactional
@Override
public CommandProcessingResult receiveStock(final JsonCommand command) {
this.validator.validateForStockLotReceive(command.json());
final Long rangeStart = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue());
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
// fail fast for the maker; re-checked authoritatively at approval time in case stock changed meanwhile
if (this.chequeStockLotRepository.countOverlappingAnySeries(rangeStart, rangeEnd) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
"The given leaf-serial range overlaps with an existing stock lot (checked across every series, "
+ "not just this one) — pick a different range.",
rangeStart);
}
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.RECEIVE_STOCK, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*/
private Long finalizeReceiveStock(final JsonCommand command, final Long makerId) {
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Long officeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue());
final Long rangeStart = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue());
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
final String batchRef = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.BATCH_REF.getValue());
if (this.chequeStockLotRepository.countOverlappingAnySeries(rangeStart, rangeEnd) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
"The given leaf-serial range overlaps with an existing stock lot (checked across every series, "
+ "not just this one) — pick a different range.",
rangeStart);
}
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final ChequeStockLot lot = ChequeStockLot.createReceipt(series, office, rangeStart, rangeEnd, batchRef, makerId);
final ChequeStockLot saved = this.chequeStockLotRepositoryWrapper.saveAndFlush(lot);
log.info("Received cheque stock lot {} ({} - {}) for series {} at office {}", saved.getId(), rangeStart, rangeEnd, seriesId,
officeId);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult reportStockLotLost(final Long lotId, final JsonCommand command) {
this.validator.validateForStockLotReportLost(command.json());
final ChequeStockLot lot = this.chequeStockLotRepositoryWrapper.findOneWithNotFoundDetection(lotId);
final Long fromSerialNo = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue());
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long reporterId = this.context.authenticatedUser().getId();
final long originalRangeEnd = lot.reportRemainingLost(fromSerialNo);
this.chequeStockLotRepositoryWrapper.saveAndFlush(lot);
final List<ChequeLeaf> lostLeaves = new ArrayList<>();
for (long serial = fromSerialNo; serial <= originalRangeEnd; serial++) {
lostLeaves.add(ChequeLeaf.lostUnissued(lot, serial, reason, reporterId));
}
this.chequeLeafRepository.saveAll(lostLeaves);
log.info("Reported {} unissued cheque leaves lost/stolen from stock lot {}", lostLeaves.size(), lotId);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lotId).build();
}
@Transactional
@Override
public CommandProcessingResult dispatchStockTransfer(final JsonCommand command) {
this.validator.validateForStockTransferDispatch(command.json());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Long fromOfficeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue());
final Long toOfficeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue());
final Long fromLotId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue());
final Integer noOfLeaves = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue());
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final ChequeStockLot fromLot = this.chequeStockLotRepositoryWrapper.findOneWithNotFoundDetection(fromLotId);
if (!fromLot.getOffice().getId().equals(fromOfficeId)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.office.mismatch",
"Stock lot " + fromLotId + " is not custodied at the given fromOfficeId.", fromLotId);
}
if (!fromLot.getSeries().getId().equals(seriesId)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.series.mismatch",
"Stock lot " + fromLotId + " does not belong to the given series.", fromLotId);
}
final long[] range = fromLot.drawDown(noOfLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(fromLot);
final ChequeStockLot toLot = ChequeStockLot.createTransferIn(series, toOffice, range[0], range[1], null);
final ChequeStockLot savedToLot = this.chequeStockLotRepositoryWrapper.saveAndFlush(toLot);
final Long dispatchedBy = this.context.authenticatedUser().getId();
final ChequeStockTransfer transfer = ChequeStockTransfer.createNew(series, fromOffice, toOffice, fromLot, savedToLot, range[0],
range[1], noOfLeaves, DateUtils.getBusinessLocalDate(), dispatchedBy);
final ChequeStockTransfer saved = this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
log.info("Dispatched cheque stock transfer {} ({} leaves, {} - {}) from office {} to office {}", saved.getId(), noOfLeaves,
range[0], range[1], fromOfficeId, toOfficeId);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).build();
}
@Transactional
@Override
public CommandProcessingResult receiveStockTransfer(final Long transferId, final JsonCommand command) {
final ChequeStockTransfer transfer = this.chequeStockTransferRepositoryWrapper.findOneWithNotFoundDetection(transferId);
final Long receivedBy = this.context.authenticatedUser().getId();
transfer.markReceived(receivedBy, DateUtils.getBusinessLocalDate());
this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
transfer.getToLot().markAvailable(receivedBy);
this.chequeStockLotRepositoryWrapper.saveAndFlush(transfer.getToLot());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transferId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectStockTransfer(final Long transferId, final JsonCommand command) {
final ChequeStockTransfer transfer = this.chequeStockTransferRepositoryWrapper.findOneWithNotFoundDetection(transferId);
final Long rejectedBy = this.context.authenticatedUser().getId();
transfer.reject(rejectedBy, DateUtils.getBusinessLocalDate());
this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
transfer.getToLot().cancel();
this.chequeStockLotRepositoryWrapper.saveAndFlush(transfer.getToLot());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transferId).build();
}
// ============================= Cheque books =============================
@Transactional
@Override
public CommandProcessingResult requestChequeBook(final JsonCommand command) {
this.validator.validateForBookRequest(command.json());
final ChequeAccountType accountType = ChequeAccountType
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()));
final Long accountId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Integer noOfBooks = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue());
// fail fast for the maker; re-checked authoritatively at approval time in case anything changed meanwhile
assertEligible(accountType, accountId);
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
if (!series.appliesTo(accountType)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.account.type.mismatch",
"Cheque series " + seriesId + " is not applicable to a " + accountType + " account.", seriesId);
}
// per-leaf issuance charge must not push the account below its minimum required balance — checked
// at request time itself, not only on approval
final BigDecimal chargeAmount = this.chequeBookIssuanceChargeService.ratePerLeaf()
.multiply(BigDecimal.valueOf((long) noOfBooks * series.getLeavesPerBook()));
this.chequeBookIssuanceChargeService.assertBalanceCovers(accountType, accountId, chargeAmount);
// reserve the charge amount on the account for the pending duration (a direct on-hold-funds field
// update — no transaction row, so it never shows in the passbook/transactions list) — released
// (then charged) on approval, or released (returned) on rejection; never left dangling
this.chequeBookIssuanceChargeService.placeHold(accountType, accountId, chargeAmount);
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.REQUEST_BOOK, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*
* Requesting and issuing happen together here — the checker's approval IS the authorization to hand
* out the book, so there is no separate "Issue" click left for anyone to do afterward. The book is
* created and immediately drawn down from available stock in the same step.
*/
private Long finalizeRequestChequeBook(final JsonCommand command, final Long makerId) {
final ChequeAccountType accountType = ChequeAccountType
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()));
final Long accountId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue());
final Long officeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue());
final Integer noOfBooks = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
assertEligible(accountType, accountId);
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
// the denomination requested must be meant for this account's scheme (SB vs CA)
if (!series.appliesTo(accountType)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.account.type.mismatch",
"Cheque series " + seriesId + " is not applicable to a " + accountType + " account.", seriesId);
}
// leaves actually handed to the customer = requested books x this series' fixed denomination
final int totalLeaves = noOfBooks * series.getLeavesPerBook();
final BigDecimal chargeAmount = this.chequeBookIssuanceChargeService.ratePerLeaf().multiply(BigDecimal.valueOf(totalLeaves));
// release the hold placed at submit time before re-checking — otherwise the held amount would count
// against the account twice (once as "on hold", once as the charge being re-validated)
this.chequeBookIssuanceChargeService.releaseHold(accountType, accountId, chargeAmount);
// authoritative re-check: rate or balance may have moved since the maker submitted this request
this.chequeBookIssuanceChargeService.assertBalanceCovers(accountType, accountId, chargeAmount);
final ChequeBook book = ChequeBook.requestNew(accountType, accountId, office, series, noOfBooks, makerId);
final ChequeBook saved = this.chequeBookRepositoryWrapper.saveAndFlush(book);
this.chequeBookIssuanceChargeService.deduct(accountType, accountId, office, chargeAmount, saved.getId());
final List<ChequeStockLot> candidateLots = this.chequeStockLotRepository
.findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(seriesId, officeId, ChequeStockLotStatus.AVAILABLE.name());
ChequeStockLot chosenLot = null;
for (final ChequeStockLot candidate : candidateLots) {
if (candidate.remainingLeaves() >= totalLeaves) {
chosenLot = candidate;
break;
}
}
if (chosenLot == null) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.insufficient.stock",
"No cheque stock lot at this office has enough remaining leaves (" + totalLeaves + " needed) for series "
+ seriesId + ".",
seriesId);
}
final long[] range = chosenLot.drawDown(totalLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(chosenLot);
// the checker approving right now is the one authorizing the physical issue, not the original maker
final Long checkerId = this.context.authenticatedUser().getId();
saved.issue(chosenLot, range[0], range[1], totalLeaves, checkerId);
this.chequeBookRepositoryWrapper.saveAndFlush(saved);
final List<ChequeLeaf> leaves = new ArrayList<>();
int seqNo = 1;
for (long serial = range[0]; serial <= range[1]; serial++) {
leaves.add(ChequeLeaf.issueForBook(saved, chosenLot, serial, seqNo++));
}
this.chequeLeafRepository.saveAll(leaves);
log.info("Cheque book {} requested and issued together on approval ({} leaves, {} - {}) for {} account {}", saved.getId(),
leaves.size(), range[0], range[1], accountType, accountId);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult issueChequeBook(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookIssue(command.json());
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
// the denomination was already fixed at request time — matches a real requisition slip
final ChequeSeries series = book.getSeries();
if (series == null) {
// a request created before series-selection-at-request-time was introduced has no series on file
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.not.set",
"Cheque book " + chequeBookId + " has no series recorded against it. Reject this request and submit "
+ "a new one, which will capture the series at requisition time.",
chequeBookId);
}
final Long seriesId = series.getId();
// re-validate eligibility at issue time — time may have passed since the request was recorded
final ChequeAccountType accountType = ChequeAccountType.fromString(book.getAccountType());
assertEligible(accountType, book.getAccountId());
// leaves actually handed to the customer = requested books x this series' fixed denomination
final int totalLeaves = book.getNoOfBooks() * series.getLeavesPerBook();
final List<ChequeStockLot> candidateLots = this.chequeStockLotRepository.findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(seriesId,
book.getOffice().getId(), ChequeStockLotStatus.AVAILABLE.name());
ChequeStockLot chosenLot = null;
for (final ChequeStockLot candidate : candidateLots) {
if (candidate.remainingLeaves() >= totalLeaves) {
chosenLot = candidate;
break;
}
}
if (chosenLot == null) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.insufficient.stock",
"No cheque stock lot at this office has enough remaining leaves (" + totalLeaves + " needed) for series "
+ seriesId + ".",
seriesId);
}
final long[] range = chosenLot.drawDown(totalLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(chosenLot);
final Long issuedBy = this.context.authenticatedUser().getId();
book.issue(chosenLot, range[0], range[1], totalLeaves, issuedBy);
final Boolean collectionAcknowledged = command
.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.COLLECTION_ACKNOWLEDGED.getValue());
if (Boolean.TRUE.equals(collectionAcknowledged)) {
book.acknowledgeCollection(issuedBy);
}
this.chequeBookRepositoryWrapper.saveAndFlush(book);
final List<ChequeLeaf> leaves = new ArrayList<>();
int seqNo = 1;
for (long serial = range[0]; serial <= range[1]; serial++) {
leaves.add(ChequeLeaf.issueForBook(book, chosenLot, serial, seqNo++));
}
this.chequeLeafRepository.saveAll(leaves);
log.info("Issued cheque book {} ({} leaves, {} - {}) against {} account {}", chequeBookId, leaves.size(), range[0], range[1],
book.getAccountType(), book.getAccountId());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectChequeBook(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookReject(command.json());
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue());
book.reject(reason);
this.chequeBookRepositoryWrapper.saveAndFlush(book);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult withdrawChequeBook(final Long chequeBookId, final JsonCommand command) {
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
book.withdraw();
this.chequeBookRepositoryWrapper.saveAndFlush(book);
final List<ChequeLeaf> leaves = this.chequeLeafRepository.findByChequeBookIdOrderByLeafSeqNoAsc(chequeBookId);
final List<ChequeLeaf> toWithdraw = new ArrayList<>();
for (final ChequeLeaf leaf : leaves) {
if (ChequeLeafStatus.ACTIVE.name().equals(leaf.getStatus())) {
leaf.withdraw();
toWithdraw.add(leaf);
}
}
this.chequeLeafRepository.saveAll(toWithdraw);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult stopPayment(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.stopPayment(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult revokeStopPayment(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), false);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.revokeStopPayment();
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult reportChequeBookLost(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long reporterId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.reportLost(reason, reporterId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult cancelLeaf(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.cancel(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult returnChequeBook(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.returned(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult markLeafUsed(final Long chequeBookId, final JsonCommand command) {
// informational record-keeping, not a fraud control — a note is welcome but not required
this.validator.validateForBookLeafAction(command.json(), false);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.markUsed(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
// ============================= Pending requests (bespoke maker-checker) =============================
//
// Fineract's native command-approval engine holds nothing for a user whose role carries ALL_FUNCTIONS
// (Super user, Clerk in this tenant) - it bypasses maker-checker unconditionally for them, which made it
// a no-op for this CBS's actual admin accounts. These three actions instead go through this bespoke
// table (mirrors m_clerk_disbursement_request): submit stores the raw request JSON as PENDING; approval
// replays that JSON through the same finalize* method that used to run immediately.
private Long submitPendingRequest(final ChequeBookPendingRequestType type, final JsonCommand command) {
return insertPendingRequest(type, command.json());
}
private Long insertPendingRequest(final ChequeBookPendingRequestType type, final String payloadJson) {
final AppUser maker = this.context.authenticatedUser();
final String activityLog = appendActivityLog(null, "SUBMITTED", maker, null);
final ChequeBookPendingRequest request = ChequeBookPendingRequest.submit(type, payloadJson, maker, activityLog);
final ChequeBookPendingRequest saved = this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} ({}) submitted by {}", saved.getId(), type, maker.getUsername());
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult approvePendingRequest(final Long pendingRequestId) {
final ChequeBookPendingRequest request = this.chequeBookPendingRequestRepositoryWrapper
.findOneWithNotFoundDetection(pendingRequestId);
if (!request.isPending()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.not.pending",
"Cheque book pending request " + pendingRequestId + " has already been actioned.", pendingRequestId);
}
final AppUser checker = this.context.authenticatedUser();
if (request.isMadeBy(checker.getId())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.self.approval",
"You cannot approve a cheque book request you submitted yourself — a different user must approve it.",
pendingRequestId);
}
final JsonCommand replay = reconstructCommand(request.getPayloadJson());
final ChequeBookPendingRequestType type = ChequeBookPendingRequestType.valueOf(request.getRequestType());
final Long createdResourceId = switch (type) {
case CREATE_SERIES -> finalizeCreateSeries(replay, request.getMakerId());
case RECEIVE_STOCK -> finalizeReceiveStock(replay, request.getMakerId());
case REQUEST_BOOK -> finalizeRequestChequeBook(replay, request.getMakerId());
case DELETE_SERIES ->
finalizeDeleteSeries(replay.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue()));
};
final String activityLog = appendActivityLog(request.getActivityLog(), "APPROVED", checker, null);
request.approve(checker, createdResourceId, activityLog);
this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} approved by {}, created resource {}", pendingRequestId, checker.getUsername(),
createdResourceId);
return new CommandProcessingResultBuilder().withEntityId(createdResourceId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectPendingRequest(final Long pendingRequestId, final JsonCommand command) {
final ChequeBookPendingRequest request = this.chequeBookPendingRequestRepositoryWrapper
.findOneWithNotFoundDetection(pendingRequestId);
if (!request.isPending()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.not.pending",
"Cheque book pending request " + pendingRequestId + " has already been actioned.", pendingRequestId);
}
final AppUser checker = this.context.authenticatedUser();
if (request.isMadeBy(checker.getId())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.self.approval",
"You cannot reject a cheque book request you submitted yourself — a different user must action it.",
pendingRequestId);
}
final String reason = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.REASON.getValue());
if (reason == null || reason.isBlank()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.reject.reason.required",
"A reason is required to reject a cheque book pending request.", pendingRequestId);
}
// a rejected book request returns any reserved issuance charge to the customer — never left on hold
if (ChequeBookPendingRequestType.REQUEST_BOOK.name().equals(request.getRequestType())) {
final JsonCommand replay = reconstructCommand(request.getPayloadJson());
final ChequeAccountType accountType = ChequeAccountType
.fromString(replay.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()));
final Long accountId = replay.longValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue());
final Long seriesId = replay.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Integer noOfBooks = replay.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue());
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
final BigDecimal chargeAmount = this.chequeBookIssuanceChargeService.ratePerLeaf()
.multiply(BigDecimal.valueOf((long) noOfBooks * series.getLeavesPerBook()));
this.chequeBookIssuanceChargeService.releaseHold(accountType, accountId, chargeAmount);
}
final String activityLog = appendActivityLog(request.getActivityLog(), "REJECTED", checker, reason);
request.reject(checker, reason, activityLog);
this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} rejected by {}", pendingRequestId, checker.getUsername());
return new CommandProcessingResultBuilder().withEntityId(pendingRequestId).build();
}
/** Reconstructs a usable {@link JsonCommand} from a pending request's stored raw JSON, for replay at approval. */
private JsonCommand reconstructCommand(final String json) {
// entityName, resourceId, subresourceId, groupId, clientId, loanId, savingsId, transactionId, url,
// productId, creditBureauId, organisationCreditBureauId, jobName, loanExternalId - none apply here
return JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null, null, null, null, null, null, null,
null, null, null, null, null, null, null);
}
private String appendActivityLog(final String existingJson, final String action, final AppUser user, final String note) {
JsonArray log = new JsonArray();
if (existingJson != null && !existingJson.isBlank()) {
try {
log = this.fromApiJsonHelper.parse(existingJson).getAsJsonArray();
} catch (final Exception e) {
log = new JsonArray();
}
}
final JsonObject entry = new JsonObject();
entry.addProperty("timestamp", Instant.now().toString());
entry.addProperty("action", action);
entry.addProperty("user", user.getUsername());
if (note != null && !note.isBlank()) {
entry.addProperty("note", note);
}
log.add(entry);
return this.gson.toJson(log);
}
// ============================= Helpers =============================
private List<ChequeLeaf> resolveLeavesInRange(final ChequeBook book, final JsonCommand command) {
if (!book.isIssued()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.issued",
"Cheque book " + book.getId() + " has not been issued.", book.getId());
}
final Integer fromSeq = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue());
final Integer toSeq = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue());
final List<ChequeLeaf> leaves;
if (fromSeq == null && toSeq == null) {
leaves = this.chequeLeafRepository.findByChequeBookIdOrderByLeafSeqNoAsc(book.getId());
} else {
final int from = fromSeq != null ? fromSeq : 1;
final int to = toSeq != null ? toSeq : book.getTotalLeaves();
leaves = this.chequeLeafRepository.findByChequeBookIdAndLeafSeqNoBetweenOrderByLeafSeqNoAsc(book.getId(), from, to);
}
if (leaves.isEmpty()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.no.leaves.in.range",
"No leaves found in the given range for cheque book " + book.getId() + ".", book.getId());
}
return leaves;
}
/**
* Resolves the account (Savings or Current — not interchangeable in this codebase), checks it is active
* and not dormant/blocked, and checks the {@code chequeBook} facility flag (account-level config,
* falling back to product-level config when the account-level config is absent).
*/
private void assertEligible(final ChequeAccountType accountType, final Long accountId) {
if (accountType == ChequeAccountType.SAVINGS) {
assertSavingsEligible(accountId);
} else if (accountType == ChequeAccountType.CURRENT) {
assertCurrentEligible(accountId);
} else {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.type.invalid", "Unknown account type.",
accountId);
}
}
private void assertSavingsEligible(final Long accountId) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
if (!account.isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.not.active",
"Savings account " + accountId + " is not active.", accountId);
}
final SavingsAccountSubStatusEnum subStatus = SavingsAccountSubStatusEnum.fromInt(account.getSubStatus());
if (subStatus != null && (subStatus.isSubStatusDormant() || subStatus.isSubStatusAccountBlocked()
|| subStatus.isSubStatusDebitBlocked())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.blocked",
"Savings account " + accountId + " is dormant or blocked.", accountId);
}
Boolean chequeBookFlag = null;
final SavingsAccountFacilityConfig accountConfig = account.getFacilityConfig();
if (accountConfig != null) {
chequeBookFlag = accountConfig.getChequeBook();
} else if (account.savingsProduct() != null) {
final SavingsProductFacilityConfig productConfig = account.savingsProduct().getFacilityConfig();
if (productConfig != null) {
chequeBookFlag = productConfig.getChequeBook();
}
}
if (!Boolean.TRUE.equals(chequeBookFlag)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.facility.not.enabled",
"The cheque book facility is not enabled for savings account " + accountId + ".", accountId);
}
}
private void assertCurrentEligible(final Long accountId) {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
if (!account.isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.not.active",
"Current account " + accountId + " is not active.", accountId);
}
final Integer subStatusEnum = account.getSubStatusEnum();
// 200=Dormant, 300=Block
if (subStatusEnum != null && (subStatusEnum == 200 || subStatusEnum == 300)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.blocked",
"Current account " + accountId + " is dormant or blocked.", accountId);
}
Boolean chequeBookFlag = null;
final CurrentAccountFacilityConfig accountConfig = account.getFacilityConfig();
if (accountConfig != null) {
chequeBookFlag = accountConfig.isChequeBook();
} else {
final CurrentProductFacilityConfig productConfig = this.currentProductFacilityConfigRepository
.findByCurrentProductId(account.getProductId());
if (productConfig != null) {
chequeBookFlag = productConfig.getChequeBook();
}
}
if (!Boolean.TRUE.equals(chequeBookFlag)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.facility.not.enabled",
"The cheque book facility is not enabled for current account " + accountId + ".", accountId);
}
}
}
@@ -0,0 +1,59 @@
/**
* 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.chequebook.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* Whether a cheque series can be permanently removed is decided ATOMICALLY by the database's own foreign
* key constraints (stock lots/transfers/books referencing it), not by a separate "count references first"
* check — a count-then-act approach has a race: something could start referencing the series in the gap
* between the check and the delete (e.g. a concurrently-approved "receive stock" request). Attempting the
* real delete and catching a constraint violation makes the check and the action the same atomic step.
*
* This attempt runs in its own transaction (REQUIRES_NEW) so a rejection here never poisons the caller's
* transaction — Postgres marks a transaction aborted after a constraint violation, so the caller's
* subsequent deactivate-fallback would otherwise never be allowed to run.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ChequeSeriesHardDeleteAttempter {
private final ChequeSeriesRepository chequeSeriesRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean attemptDelete(final Long seriesId) {
try {
this.chequeSeriesRepository.deleteById(seriesId);
this.chequeSeriesRepository.flush();
return true;
} catch (final DataIntegrityViolationException e) {
log.info("Cheque series {} still has stock lots/transfers/books referencing it — deactivating instead of deleting",
seriesId);
return false;
}
}
}
@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.api;
public final class ClearingApiConstants {
private ClearingApiConstants() {}
// entity names — must match the CommandWrapperBuilder entityName + the seeded permissions
public static final String CLEARINGRETURNREASONCODE = "CLEARINGRETURNREASONCODE";
public static final String CLEARINGSETTLEMENTBATCH = "CLEARINGSETTLEMENTBATCH";
public static final String CLEARINGRECONCILIATION = "CLEARINGRECONCILIATION";
// resource names used for validation error scoping
public static final String RESOURCE_SETTLEMENT_BATCH = "clearingsettlementbatch";
public static final String RESOURCE_RECONCILIATION = "clearingreconciliation";
}
@@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.clearing.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public enum ClearingApiJsonInputParams {
// settlement batch create
CLEARING_TYPE("clearingType"), //
DIRECTION("direction"), //
SETTLEMENT_DATE("settlementDate"), //
SOURCE_REFERENCE("sourceReference"), //
SOURCE_BATCH_ID("sourceBatchId"), //
// reconciliation create
RECONCILIATION_DATE("reconciliationDate"), //
CBS_TOTAL("cbsTotal"), //
CLEARING_HOUSE_TOTAL("clearingHouseTotal"), //
DETAIL("detail"), //
// settlement reject
REASON("reason"), //
// common — the web client always sends these on command payloads
LOCALE("locale"), //
DATE_FORMAT("dateFormat"); //
private final String value;
ClearingApiJsonInputParams(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public static Set<String> settlementBatchCreateParams() {
return toSet(CLEARING_TYPE, DIRECTION, SETTLEMENT_DATE, SOURCE_REFERENCE, SOURCE_BATCH_ID, LOCALE, DATE_FORMAT);
}
public static Set<String> reconciliationParams() {
return toSet(CLEARING_TYPE, RECONCILIATION_DATE, SOURCE_REFERENCE, CBS_TOTAL, CLEARING_HOUSE_TOTAL, DETAIL, LOCALE, DATE_FORMAT);
}
public static Set<String> rejectSettlementParams() {
return toSet(REASON, LOCALE, DATE_FORMAT);
}
private static Set<String> toSet(final ClearingApiJsonInputParams... params) {
return Arrays.stream(params).map(ClearingApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new));
}
}
@@ -0,0 +1,84 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.clearing.data.ClearingReconciliationData;
import org.apache.fineract.portfolio.clearing.service.ClearingReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clearing/reconciliations")
@Component
@Tag(name = "Clearing Reconciliation", description = "Day-end reconciliation of the CBS ledger against the clearing house's "
+ "settlement report, for CTS and NACH.")
@RequiredArgsConstructor
public class ClearingReconciliationApiResource {
private final PlatformSecurityContext context;
private final ClearingReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List reconciliation runs")
public Page<ClearingReconciliationData> retrieveAll(@QueryParam("clearingType") final String clearingType,
@QueryParam("offset") @DefaultValue("0") final Integer offset,
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ClearingApiConstants.CLEARINGRECONCILIATION);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrieveReconciliations(clearingType, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a reconciliation run")
public ClearingReconciliationData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ClearingApiConstants.CLEARINGRECONCILIATION);
return this.readPlatformService.retrieveReconciliation(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a reconciliation",
description = "Body: { clearingType, reconciliationDate, sourceReference, cbsTotal, clearingHouseTotal, detail }.")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createClearingReconciliation().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,55 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.clearing.data.ClearingReturnReasonCodeData;
import org.apache.fineract.portfolio.clearing.service.ClearingReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clearing/return-reason-codes")
@Component
@Tag(name = "Clearing Return Reason Codes", description = "Standardized return/dishonour codes shared by the CTS and NACH "
+ "clearing modules.")
@RequiredArgsConstructor
public class ClearingReturnReasonCodeApiResource {
private final PlatformSecurityContext context;
private final ClearingReadPlatformService readPlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List clearing return reason codes",
description = "Optionally filtered by ?appliesTo=CTS|NACH (codes marked BOTH are always included).")
public List<ClearingReturnReasonCodeData> retrieveAll(
@QueryParam("appliesTo") @Parameter(description = "CTS | NACH") final String appliesTo) {
this.context.authenticatedUser().validateHasReadPermission(ClearingApiConstants.CLEARINGRETURNREASONCODE);
return this.readPlatformService.retrieveReturnReasonCodes(appliesTo);
}
}
@@ -0,0 +1,105 @@
/**
* 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.clearing.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
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.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.clearing.data.ClearingSettlementBatchData;
import org.apache.fineract.portfolio.clearing.service.ClearingReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clearing/settlement-batches")
@Component
@Tag(name = "Clearing Settlement Batches", description = "Net-settlement batches for CTS and NACH clearing sessions. "
+ "Posting a batch (a different user than the preparer) writes the summary GL entry.")
@RequiredArgsConstructor
public class ClearingSettlementBatchApiResource {
private final PlatformSecurityContext context;
private final ClearingReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List settlement batches")
public Page<ClearingSettlementBatchData> retrieveAll(@QueryParam("clearingType") final String clearingType,
@QueryParam("status") final String status, @QueryParam("offset") @DefaultValue("0") final Integer offset,
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ClearingApiConstants.CLEARINGSETTLEMENTBATCH);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrieveSettlementBatches(clearingType, status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a settlement batch")
public ClearingSettlementBatchData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ClearingApiConstants.CLEARINGSETTLEMENTBATCH);
return this.readPlatformService.retrieveSettlementBatch(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Prepare a settlement batch",
description = "Nets the passed/returned totals of a decisioned source batch. Body: { clearingType, sourceBatchId }.")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createClearingSettlementBatch().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a settlement batch", description = "Supported commands: post, reject.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "post | reject") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("post".equalsIgnoreCase(commandParam)) {
request = builder.postClearingSettlementBatch(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectClearingSettlementBatch(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ClearingReconciliationData {
private final Long id;
private final String clearingType;
private final LocalDate reconciliationDate;
private final String sourceReference;
private final BigDecimal cbsTotal;
private final BigDecimal clearingHouseTotal;
private final BigDecimal difference;
private final String status;
private final String detail;
private final Long runBy;
private final OffsetDateTime createdOn;
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ClearingReturnReasonCodeData {
private final Long id;
private final String code;
private final String description;
private final String category;
private final String appliesTo;
private final boolean active;
}
@@ -0,0 +1,53 @@
/**
* 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.clearing.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ClearingSettlementBatchData {
private final Long id;
private final String clearingType;
private final String direction;
private final LocalDate settlementDate;
private final String sourceReference;
private final Long sourceBatchId;
private final BigDecimal totalPassedAmount;
private final BigDecimal totalReturnedAmount;
private final BigDecimal netAmount;
private final Integer passedCount;
private final Integer returnedCount;
private final Long officeId;
private final String currencyCode;
private final String glTransactionId;
private final String status;
private final Long preparedBy;
private final OffsetDateTime preparedOn;
private final Long postedBy;
private final OffsetDateTime postedOn;
private final String rejectionReason;
}
@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/**
* A simulated customer notification (spec section 10.1 "Notification service"). Clearing decisioning writes a
* row here on pass/return/settlement; there is no real SMS/email gateway wired in, so {@code status} defaults
* to {@code SIMULATED}. This gives an auditable, demoable log without pretending to send real messages.
*/
@Entity
@Table(name = "m_clearing_notification")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ClearingNotification extends AbstractPersistableCustom<Long> {
@Column(name = "channel", nullable = false, length = 10)
private String channel;
@Column(name = "recipient", length = 150)
private String recipient;
@Column(name = "event", nullable = false, length = 30)
private String event;
@Column(name = "clearing_type", length = 10)
private String clearingType;
@Column(name = "ref_type", length = 40)
private String refType;
@Column(name = "ref_id")
private Long refId;
@Column(name = "message", nullable = false, length = 1000)
private String message;
@Column(name = "status", nullable = false, length = 15)
private String status;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ClearingNotification simulated(final String channel, final String recipient, final String event,
final String clearingType, final String refType, final Long refId, final String message) {
final ClearingNotification n = new ClearingNotification();
n.channel = channel;
n.recipient = recipient;
n.event = event;
n.clearingType = clearingType;
n.refType = refType;
n.refId = refId;
n.message = message;
n.status = "SIMULATED";
n.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return n;
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClearingNotificationRepository extends JpaRepository<ClearingNotification, Long> {
}
@@ -0,0 +1,89 @@
/**
* 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.clearing.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/**
* A day-end reconciliation run (spec sections 4.11 &amp; 8.9): compares the CBS's own settled total for a
* session against the figure reported by the clearing house, and records whether they matched. In this
* simulation the "clearing house report total" is supplied by the operator (there is no external NPCI feed).
*/
@Entity
@Table(name = "m_clearing_reconciliation")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ClearingReconciliation extends AbstractPersistableCustom<Long> {
@Column(name = "clearing_type", nullable = false, length = 10)
private String clearingType;
@Column(name = "reconciliation_date", nullable = false)
private LocalDate reconciliationDate;
@Column(name = "source_reference", length = 100)
private String sourceReference;
@Column(name = "cbs_total", nullable = false, scale = 6, precision = 19)
private BigDecimal cbsTotal;
@Column(name = "clearing_house_total", nullable = false, scale = 6, precision = 19)
private BigDecimal clearingHouseTotal;
@Column(name = "difference", nullable = false, scale = 6, precision = 19)
private BigDecimal difference;
@Column(name = "status", nullable = false, length = 15)
private String status;
@Column(name = "detail", length = 1000)
private String detail;
@Column(name = "run_by")
private Long runBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ClearingReconciliation run(final String clearingType, final LocalDate reconciliationDate, final String sourceReference,
final BigDecimal cbsTotal, final BigDecimal clearingHouseTotal, final String detail, final Long runBy) {
final ClearingReconciliation rec = new ClearingReconciliation();
rec.clearingType = clearingType;
rec.reconciliationDate = reconciliationDate;
rec.sourceReference = sourceReference;
rec.cbsTotal = cbsTotal == null ? BigDecimal.ZERO : cbsTotal;
rec.clearingHouseTotal = clearingHouseTotal == null ? BigDecimal.ZERO : clearingHouseTotal;
rec.difference = rec.cbsTotal.subtract(rec.clearingHouseTotal);
rec.status = rec.difference.signum() == 0 ? "MATCHED" : "MISMATCH";
rec.detail = detail;
rec.runBy = runBy;
rec.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return rec;
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClearingReconciliationRepository extends JpaRepository<ClearingReconciliation, Long> {
}
@@ -0,0 +1,56 @@
/**
* 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.clearing.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/**
* A standardized return/dishonour reason code shared by CTS and NACH decisioning (spec section 14). Kept as a
* seeded reference table rather than a Java enum so the set is admin-visible and extendable without a code
* change, and so decision rows can carry a stable FK-by-code.
*/
@Entity
@Table(name = "m_clearing_return_reason_code")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ClearingReturnReasonCode extends AbstractPersistableCustom<Long> {
@Column(name = "code", nullable = false, length = 10, unique = true)
private String code;
@Column(name = "description", nullable = false, length = 200)
private String description;
/** FINANCIAL / SIGNATURE / ACCOUNT / INSTRUMENT / MANDATE / TECHNICAL / OTHER — for grouping in the UI. */
@Column(name = "category", nullable = false, length = 20)
private String category;
/** CTS / NACH / BOTH — which clearing flow this code is valid for. */
@Column(name = "applies_to", nullable = false, length = 10)
private String appliesTo;
@Column(name = "is_active", nullable = false)
private boolean active;
}
@@ -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.clearing.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClearingReturnReasonCodeRepository extends JpaRepository<ClearingReturnReasonCode, Long> {
Optional<ClearingReturnReasonCode> findByCodeIgnoreCase(String code);
}
@@ -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.portfolio.clearing.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/**
* A net-settlement batch over one clearing session (spec sections 7 &amp; 8, "Settlement"). Shared by CTS and
* NACH via {@code clearingType}. Nets the passed vs returned amounts of a decisioned source batch and, when
* posted, writes a summary GL entry to the clearing suspense/settlement accounts.
*
* <p>
* Posting is the one two-person-control point in the clearing flow: {@code preparedBy} computes the batch, a
* different user {@code postedBy} authorises the GL posting. {@link #isPreparedBy(Long)} enforces this in code
* (not only via the {@code can_maker_checker} permission flag) because an {@code ALL_FUNCTIONS} role would
* otherwise bypass the native maker-checker hold in this tenant.
* </p>
*/
@Entity
@Table(name = "m_clearing_settlement_batch")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ClearingSettlementBatch extends AbstractPersistableCustom<Long> {
@Column(name = "clearing_type", nullable = false, length = 10)
private String clearingType;
@Column(name = "settlement_date", nullable = false)
private LocalDate settlementDate;
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "source_reference", length = 100)
private String sourceReference;
@Column(name = "source_batch_id")
private Long sourceBatchId;
@Column(name = "total_passed_amount", nullable = false, scale = 6, precision = 19)
private BigDecimal totalPassedAmount;
@Column(name = "total_returned_amount", nullable = false, scale = 6, precision = 19)
private BigDecimal totalReturnedAmount;
@Column(name = "net_amount", nullable = false, scale = 6, precision = 19)
private BigDecimal netAmount;
@Column(name = "passed_count", nullable = false)
private Integer passedCount;
@Column(name = "returned_count", nullable = false)
private Integer returnedCount;
@Column(name = "office_id")
private Long officeId;
@Column(name = "currency_code", length = 3)
private String currencyCode;
@Column(name = "gl_transaction_id", length = 100)
private String glTransactionId;
@Column(name = "status", nullable = false, length = 15)
private String status;
@Column(name = "prepared_by")
private Long preparedBy;
@Column(name = "prepared_on")
private OffsetDateTime preparedOn;
@Column(name = "posted_by")
private Long postedBy;
@Column(name = "posted_on")
private OffsetDateTime postedOn;
@Column(name = "rejection_reason", length = 500)
private String rejectionReason;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ClearingSettlementBatch prepare(final String clearingType, final String direction, final LocalDate settlementDate,
final String sourceReference, final Long sourceBatchId, final BigDecimal totalPassed, final BigDecimal totalReturned,
final Integer passedCount, final Integer returnedCount, final Long officeId, final String currencyCode,
final Long preparedBy) {
final ClearingSettlementBatch batch = new ClearingSettlementBatch();
batch.clearingType = clearingType;
batch.direction = direction;
batch.settlementDate = settlementDate;
batch.sourceReference = sourceReference;
batch.sourceBatchId = sourceBatchId;
batch.totalPassedAmount = totalPassed == null ? BigDecimal.ZERO : totalPassed;
batch.totalReturnedAmount = totalReturned == null ? BigDecimal.ZERO : totalReturned;
batch.netAmount = batch.totalPassedAmount.subtract(batch.totalReturnedAmount);
batch.passedCount = passedCount == null ? 0 : passedCount;
batch.returnedCount = returnedCount == null ? 0 : returnedCount;
batch.officeId = officeId;
batch.currencyCode = currencyCode;
batch.status = ClearingSettlementBatchStatus.PENDING.name();
batch.preparedBy = preparedBy;
batch.preparedOn = DateUtils.getOffsetDateTimeOfTenant();
batch.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return batch;
}
public boolean isPreparedBy(final Long userId) {
return this.preparedBy != null && this.preparedBy.equals(userId);
}
public boolean isPending() {
return ClearingSettlementBatchStatus.PENDING.name().equals(this.status);
}
private void assertPending() {
if (!isPending()) {
throw new GeneralPlatformDomainRuleException("error.msg.clearing.settlement.not.pending",
"Settlement batch " + getId() + " is not in PENDING status.", getId());
}
}
public void markPosted(final Long postedBy, final String glTransactionId) {
assertPending();
this.status = ClearingSettlementBatchStatus.POSTED.name();
this.postedBy = postedBy;
this.postedOn = DateUtils.getOffsetDateTimeOfTenant();
this.glTransactionId = glTransactionId;
}
public void reject(final Long rejectedBy, final String reason) {
assertPending();
this.status = ClearingSettlementBatchStatus.REJECTED.name();
this.postedBy = rejectedBy;
this.postedOn = DateUtils.getOffsetDateTimeOfTenant();
this.rejectionReason = reason;
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.clearing.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClearingSettlementBatchRepository extends JpaRepository<ClearingSettlementBatch, Long> {
}

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