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
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
383dfdc6e9
commit
b433edfdfe
+170
@@ -231,6 +231,176 @@ public class CommandWrapperBuilder {
|
||||
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 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
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4
@@ -154,6 +154,10 @@ public class SecurityConfig {
|
||||
// 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()
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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";
|
||||
}
|
||||
+70
@@ -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));
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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);
|
||||
}
|
||||
}
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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;
|
||||
}
|
||||
+53
@@ -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;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClearingNotificationRepository extends JpaRepository<ClearingNotification, Long> {
|
||||
|
||||
}
|
||||
+89
@@ -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 & 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;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClearingReconciliationRepository extends JpaRepository<ClearingReconciliation, Long> {
|
||||
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.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);
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.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 & 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;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClearingSettlementBatchRepository extends JpaRepository<ClearingSettlementBatch, Long> {
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.domain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.clearing.exception.ClearingSettlementBatchNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClearingSettlementBatchRepositoryWrapper {
|
||||
|
||||
private final ClearingSettlementBatchRepository repository;
|
||||
|
||||
public ClearingSettlementBatch findOneWithNotFoundDetection(final Long id) {
|
||||
return this.repository.findById(id).orElseThrow(() -> new ClearingSettlementBatchNotFoundException(id));
|
||||
}
|
||||
|
||||
public ClearingSettlementBatch saveAndFlush(final ClearingSettlementBatch batch) {
|
||||
return this.repository.saveAndFlush(batch);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.domain;
|
||||
|
||||
public enum ClearingSettlementBatchStatus {
|
||||
|
||||
PENDING, //
|
||||
POSTED, //
|
||||
REJECTED; //
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class ClearingReconciliationNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public ClearingReconciliationNotFoundException(final Long id) {
|
||||
super("error.msg.clearing.reconciliation.id.invalid", "Clearing reconciliation with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class ClearingSettlementBatchNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public ClearingSettlementBatchNotFoundException(final Long id) {
|
||||
super("error.msg.clearing.settlement.id.invalid", "Clearing settlement batch with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.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.clearing.service.ClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "CLEARINGRECONCILIATION", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateClearingReconciliationCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final ClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createReconciliation(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.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.clearing.service.ClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "CLEARINGSETTLEMENTBATCH", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateClearingSettlementBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final ClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.prepareSettlement(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.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.clearing.service.ClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "CLEARINGSETTLEMENTBATCH", action = "POST")
|
||||
@RequiredArgsConstructor
|
||||
public class PostClearingSettlementBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final ClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.postSettlement(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.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.clearing.service.ClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "CLEARINGSETTLEMENTBATCH", action = "REJECT")
|
||||
@RequiredArgsConstructor
|
||||
public class RejectClearingSettlementBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final ClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.rejectSettlement(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.clearing.npci.data.NpciInwardPushRequest;
|
||||
import org.apache.fineract.portfolio.clearing.npci.service.NpciInwardPushService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Receiver-side endpoint the real, separate NPCI clearing-house service pushes routed packages to —
|
||||
* mirrors {@code ImpsCbsApiResource}'s shape (no {@code PlatformSecurityContext}; NPCI calls this without
|
||||
* a Fineract Bearer token, security is enforced by the envelope's RSA signature/encryption inside the
|
||||
* handler instead). Whitelisted in {@code SecurityConfig} the same way the IMPS receiver is.
|
||||
*/
|
||||
@Path("/v1/npci")
|
||||
@Component
|
||||
@Tag(name = "NPCI Clearing House (Receiver)", description = "Inbound endpoint the real NPCI service pushes routed packages to.")
|
||||
@RequiredArgsConstructor
|
||||
public class NpciInwardPushApiResource {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NpciInwardPushApiResource.class);
|
||||
|
||||
private final NpciInwardPushService pushService;
|
||||
|
||||
@POST
|
||||
@Path("/inward-push")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "NPCI pushes a routed package destined for this bank")
|
||||
public Response inwardPush(final NpciInwardPushRequest request) {
|
||||
try {
|
||||
this.pushService.receivePush(request);
|
||||
return Response.ok(Map.of("status", "received")).build();
|
||||
} catch (final Exception e) {
|
||||
log.error("NPCI inward push handling failed for packageId={}: {}", request == null ? null : request.getId(), e.toString(), e);
|
||||
return Response.serverError().entity(Map.of("error", e.getMessage())).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.npci.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.apache.fineract.portfolio.clearing.npci.crypto.NpciCryptoUtil;
|
||||
import org.apache.fineract.portfolio.clearing.npci.crypto.NpciEnvelope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Outbound calls from this CBS to the real, separate NPCI clearing-house service — registering this bank's
|
||||
* own public key, fetching NPCI's public key, and submitting outward batches encrypted+signed for NPCI.
|
||||
* Mirrors {@code ImpsSwitchClient}'s shape: a self-contained lazily-built {@link OkHttpClient}, {@code @Value}
|
||||
* config, POST JSON / parse JSON.
|
||||
*
|
||||
* <p>Self-registration is lazy (on first outward send in this process) rather than an eager startup runner,
|
||||
* since a background bean initializer running before this tenant's request-scoped context is fully
|
||||
* established is fragile in a multi-tenant Fineract app — by the time an outward "send" command is being
|
||||
* processed, the same tenant-scoped repository access already works correctly elsewhere in that request.
|
||||
*/
|
||||
@Service
|
||||
public class NpciClearingClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NpciClearingClient.class);
|
||||
private static final MediaType JSON = MediaType.get("application/json");
|
||||
|
||||
@Value("${fineract.npci.base-url:}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${fineract.npci.bank-code:CBSHOME01}")
|
||||
private String bankCode;
|
||||
|
||||
@Value("${fineract.npci.bank-name:CBSnexora Home Bank}")
|
||||
private String bankName;
|
||||
|
||||
@Value("${fineract.npci.ifsc-prefix:CBSH}")
|
||||
private String ifscPrefix;
|
||||
|
||||
@Value("${fineract.npci.inward-callback-url:}")
|
||||
private String inwardCallbackUrl;
|
||||
|
||||
@Value("${fineract.npci.timeout-ms:10000}")
|
||||
private int timeoutMs;
|
||||
|
||||
private final NpciCryptoUtil cryptoUtil;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private OkHttpClient httpClient;
|
||||
|
||||
private volatile boolean registeredThisSession = false;
|
||||
private volatile String cachedNpciPublicKeyPem;
|
||||
|
||||
public NpciClearingClient(final NpciCryptoUtil cryptoUtil) {
|
||||
this.cryptoUtil = cryptoUtil;
|
||||
}
|
||||
|
||||
private OkHttpClient client() {
|
||||
if (this.httpClient == null) {
|
||||
this.httpClient = new OkHttpClient.Builder().connectTimeout(Duration.ofMillis(this.timeoutMs))
|
||||
.readTimeout(this.timeoutMs, TimeUnit.MILLISECONDS).writeTimeout(this.timeoutMs, TimeUnit.MILLISECONDS).build();
|
||||
}
|
||||
return this.httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends {@code ctsFileJson} to NPCI as {@code senderBankCode}'s outward batch: registers this bank with
|
||||
* NPCI first if not already done this process, fetches NPCI's public key, encrypts+signs, POSTs, and
|
||||
* returns NPCI's raw JSON response for storage/display. Throws on any failure — callers should let this
|
||||
* roll back the enclosing transaction rather than mark anything "sent" that was never transmitted.
|
||||
*/
|
||||
public String submitOutwardBatch(final String ctsFileJson) {
|
||||
requireBaseUrl();
|
||||
ensureRegisteredWithNpci();
|
||||
|
||||
final PublicKey npciPublicKey = this.cryptoUtil.publicKeyFromPem(fetchNpciPublicKeyPem());
|
||||
final NpciEnvelope envelope = this.cryptoUtil.encryptAndSignForNpci(ctsFileJson, npciPublicKey);
|
||||
|
||||
final Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("senderBankCode", this.bankCode);
|
||||
body.put("envelope", envelope);
|
||||
|
||||
final String url = trimTrailingSlash(this.baseUrl) + "/clearing/outward";
|
||||
log.info("Submitting outward batch to NPCI as {} -> POST {}", this.bankCode, url);
|
||||
final String responseBody = post(url, body);
|
||||
log.info("NPCI response for outward batch from {}: {}", this.bankCode, responseBody);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
/** NPCI's own public key, parsed and cached — used to verify signatures on anything NPCI sends this CBS. */
|
||||
public PublicKey fetchNpciPublicKey() {
|
||||
requireBaseUrl();
|
||||
return this.cryptoUtil.publicKeyFromPem(fetchNpciPublicKeyPem());
|
||||
}
|
||||
|
||||
private synchronized void ensureRegisteredWithNpci() {
|
||||
if (this.registeredThisSession) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("bankCode", this.bankCode);
|
||||
body.put("bankName", this.bankName);
|
||||
body.put("ifscPrefix", this.ifscPrefix);
|
||||
body.put("publicKeyPem", this.cryptoUtil.ownPublicKeyPem());
|
||||
if (this.inwardCallbackUrl != null && !this.inwardCallbackUrl.isBlank()) {
|
||||
body.put("inwardCallbackUrl", this.inwardCallbackUrl);
|
||||
}
|
||||
|
||||
final String url = trimTrailingSlash(this.baseUrl) + "/banks";
|
||||
log.info("Registering this bank ({}) with NPCI -> POST {}", this.bankCode, url);
|
||||
post(url, body);
|
||||
this.cryptoUtil.markRegisteredWithNpci();
|
||||
this.registeredThisSession = true;
|
||||
log.info("Registered with NPCI as {}", this.bankCode);
|
||||
} catch (final RuntimeException e) {
|
||||
log.warn("Could not register with NPCI (will retry on next send): {}", e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private String fetchNpciPublicKeyPem() {
|
||||
if (this.cachedNpciPublicKeyPem != null) {
|
||||
return this.cachedNpciPublicKeyPem;
|
||||
}
|
||||
final String url = trimTrailingSlash(this.baseUrl) + "/npci/public-key";
|
||||
try {
|
||||
final Request request = new Request.Builder().url(url).get().build();
|
||||
try (Response response = client().newCall(request).execute()) {
|
||||
final String responseBody = bodyAsString(response.body());
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("NPCI public-key fetch returned HTTP " + response.code() + ": " + responseBody);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, Object> parsed = this.objectMapper.readValue(responseBody, Map.class);
|
||||
final String pem = (String) parsed.get("publicKeyPem");
|
||||
if (pem == null || pem.isBlank()) {
|
||||
throw new IllegalStateException("NPCI public-key response did not contain publicKeyPem");
|
||||
}
|
||||
this.cachedNpciPublicKeyPem = pem;
|
||||
return pem;
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException("Failed to fetch NPCI's public key: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String post(final String url, final Object body) {
|
||||
try {
|
||||
final String requestJson = this.objectMapper.writeValueAsString(body);
|
||||
final Request request = new Request.Builder().url(url).post(RequestBody.create(requestJson, JSON)).build();
|
||||
try (Response response = client().newCall(request).execute()) {
|
||||
final String responseBody = bodyAsString(response.body());
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("NPCI call to " + url + " returned HTTP " + response.code() + ": " + responseBody);
|
||||
}
|
||||
return responseBody;
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException("NPCI call to " + url + " failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireBaseUrl() {
|
||||
if (this.baseUrl == null || this.baseUrl.isBlank()) {
|
||||
throw new IllegalStateException("fineract.npci.base-url is not set");
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimTrailingSlash(final String url) {
|
||||
return url.replaceAll("/+$", "");
|
||||
}
|
||||
|
||||
private static String bodyAsString(final ResponseBody body) throws IOException {
|
||||
return body == null ? "" : body.string();
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.npci.crypto;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.OAEPParameterSpec;
|
||||
import javax.crypto.spec.PSource;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.apache.fineract.portfolio.clearing.npci.domain.ClearingOwnIdentity;
|
||||
import org.apache.fineract.portfolio.clearing.npci.domain.ClearingOwnIdentityRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Owns this CBS's own RSA key pair (generated once on first use, persisted in {@link ClearingOwnIdentity})
|
||||
* and the hybrid RSA-2048-OAEP(SHA-256) + AES-256-GCM encrypt/sign scheme used to talk to the real,
|
||||
* separate NPCI clearing-house service. Wire-compatible with NPCI's own {@code CryptoService} — same
|
||||
* envelope shape, same algorithms — but this class only ever handles CBS's OWN key pair plus NPCI's
|
||||
* public key; no other bank's key material passes through here.
|
||||
*/
|
||||
@Component
|
||||
public class NpciCryptoUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NpciCryptoUtil.class);
|
||||
|
||||
private static final String RSA_ALGORITHM = "RSA";
|
||||
private static final String RSA_CIPHER = "RSA/ECB/OAEPPadding";
|
||||
private static final String AES_CIPHER = "AES/GCM/NoPadding";
|
||||
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
|
||||
private static final int GCM_TAG_LENGTH_BITS = 128;
|
||||
private static final int GCM_IV_LENGTH_BYTES = 12;
|
||||
|
||||
private final ClearingOwnIdentityRepository ownIdentityRepository;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public NpciCryptoUtil(final ClearingOwnIdentityRepository ownIdentityRepository) {
|
||||
this.ownIdentityRepository = ownIdentityRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ClearingOwnIdentity loadOrCreateOwnIdentity() {
|
||||
return this.ownIdentityRepository.findFirstByOrderByIdAsc().orElseGet(() -> {
|
||||
log.info("No CBS own key pair found for NPCI integration — generating a new RSA-2048 key pair (one-time)");
|
||||
final KeyPair keyPair = generateRsaKeyPair();
|
||||
final ClearingOwnIdentity identity = ClearingOwnIdentity.create(toPem(keyPair.getPublic()), toPem(keyPair.getPrivate()));
|
||||
final ClearingOwnIdentity saved = this.ownIdentityRepository.saveAndFlush(identity);
|
||||
log.info("CBS's own key pair generated and persisted (id={})", saved.getId());
|
||||
return saved;
|
||||
});
|
||||
}
|
||||
|
||||
public String ownPublicKeyPem() {
|
||||
return loadOrCreateOwnIdentity().getPublicKeyPem();
|
||||
}
|
||||
|
||||
public PrivateKey ownPrivateKey() {
|
||||
return privateKeyFromPem(loadOrCreateOwnIdentity().getPrivateKeyPem());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markRegisteredWithNpci() {
|
||||
final ClearingOwnIdentity identity = loadOrCreateOwnIdentity();
|
||||
identity.markRegisteredWithNpci();
|
||||
this.ownIdentityRepository.saveAndFlush(identity);
|
||||
}
|
||||
|
||||
public KeyPair generateRsaKeyPair() {
|
||||
try {
|
||||
final KeyPairGenerator generator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
|
||||
generator.initialize(2048, this.secureRandom);
|
||||
return generator.generateKeyPair();
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("RSA not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String toPem(final PublicKey key) {
|
||||
return pemBlock("PUBLIC KEY", key.getEncoded());
|
||||
}
|
||||
|
||||
public String toPem(final PrivateKey key) {
|
||||
return pemBlock("PRIVATE KEY", key.getEncoded());
|
||||
}
|
||||
|
||||
public PublicKey publicKeyFromPem(final String pem) {
|
||||
try {
|
||||
final byte[] der = Base64.getDecoder().decode(stripPem(pem));
|
||||
final KeyFactory factory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||
return factory.generatePublic(new X509EncodedKeySpec(der));
|
||||
} catch (final GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Invalid public key PEM", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKey privateKeyFromPem(final String pem) {
|
||||
try {
|
||||
final byte[] der = Base64.getDecoder().decode(stripPem(pem));
|
||||
final KeyFactory factory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||
return factory.generatePrivate(new PKCS8EncodedKeySpec(der));
|
||||
} catch (final GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Invalid private key PEM", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Encrypts {@code plaintext} for {@code recipientPublicKey} (NPCI's) and signs the ciphertext with CBS's own private key. */
|
||||
public NpciEnvelope encryptAndSignForNpci(final String plaintext, final PublicKey npciPublicKey) {
|
||||
try {
|
||||
final KeyGenerator aesGen = KeyGenerator.getInstance("AES");
|
||||
aesGen.init(256, this.secureRandom);
|
||||
final SecretKey aesKey = aesGen.generateKey();
|
||||
|
||||
final byte[] iv = new byte[GCM_IV_LENGTH_BYTES];
|
||||
this.secureRandom.nextBytes(iv);
|
||||
|
||||
final Cipher aesCipher = Cipher.getInstance(AES_CIPHER);
|
||||
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
final byte[] ciphertext = aesCipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final Cipher rsaCipher = Cipher.getInstance(RSA_CIPHER);
|
||||
rsaCipher.init(Cipher.ENCRYPT_MODE, npciPublicKey, oaepSha256());
|
||||
final byte[] encryptedKey = rsaCipher.doFinal(aesKey.getEncoded());
|
||||
|
||||
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
|
||||
signer.initSign(ownPrivateKey(), this.secureRandom);
|
||||
signer.update(ciphertext);
|
||||
final byte[] signature = signer.sign();
|
||||
|
||||
final Base64.Encoder b64 = Base64.getEncoder();
|
||||
return new NpciEnvelope(b64.encodeToString(encryptedKey), b64.encodeToString(iv), b64.encodeToString(ciphertext),
|
||||
b64.encodeToString(signature));
|
||||
} catch (final GeneralSecurityException e) {
|
||||
throw new IllegalStateException("Encryption for NPCI failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrypts an envelope addressed to this CBS with its own private key. Does not verify the signature. */
|
||||
public String decryptOwn(final NpciEnvelope envelope) {
|
||||
try {
|
||||
final Base64.Decoder b64 = Base64.getDecoder();
|
||||
final byte[] encryptedKey = b64.decode(envelope.getEncryptedKey());
|
||||
final byte[] iv = b64.decode(envelope.getIv());
|
||||
final byte[] ciphertext = b64.decode(envelope.getCiphertext());
|
||||
|
||||
final Cipher rsaCipher = Cipher.getInstance(RSA_CIPHER);
|
||||
rsaCipher.init(Cipher.DECRYPT_MODE, ownPrivateKey(), oaepSha256());
|
||||
final byte[] aesKeyBytes = rsaCipher.doFinal(encryptedKey);
|
||||
final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
|
||||
|
||||
final Cipher aesCipher = Cipher.getInstance(AES_CIPHER);
|
||||
aesCipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
final byte[] plaintext = aesCipher.doFinal(ciphertext);
|
||||
return new String(plaintext, StandardCharsets.UTF_8);
|
||||
} catch (final GeneralSecurityException e) {
|
||||
throw new IllegalStateException("Decryption failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Verifies the envelope's signature (over the ciphertext bytes) was made by {@code senderPublicKey} (NPCI's). */
|
||||
public boolean verify(final NpciEnvelope envelope, final PublicKey senderPublicKey) {
|
||||
try {
|
||||
final Base64.Decoder b64 = Base64.getDecoder();
|
||||
final byte[] ciphertext = b64.decode(envelope.getCiphertext());
|
||||
final byte[] signature = b64.decode(envelope.getSignature());
|
||||
|
||||
final Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM);
|
||||
verifier.initVerify(senderPublicKey);
|
||||
verifier.update(ciphertext);
|
||||
return verifier.verify(signature);
|
||||
} catch (final GeneralSecurityException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static OAEPParameterSpec oaepSha256() {
|
||||
return new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
|
||||
}
|
||||
|
||||
private static String pemBlock(final String label, final byte[] der) {
|
||||
final String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)).encodeToString(der);
|
||||
return "-----BEGIN " + label + "-----\n" + base64 + "\n-----END " + label + "-----\n";
|
||||
}
|
||||
|
||||
private static String stripPem(final String pem) {
|
||||
return pem.replaceAll("-----BEGIN [A-Z ]+-----", "").replaceAll("-----END [A-Z ]+-----", "").replaceAll("\\s", "");
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.crypto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Wire envelope shared with the NPCI clearing-house service: an AES key wrapped with the recipient's RSA
|
||||
* public key, the AES-GCM IV, the AES-GCM ciphertext, and a signature over the ciphertext made with the
|
||||
* sender's RSA private key. All four fields are base64. Field names must match NPCI's own {@code EnvelopeDto}
|
||||
* exactly (encryptedKey/iv/ciphertext/signature) — this is a shared wire contract, not just an internal shape.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class NpciEnvelope {
|
||||
|
||||
private String encryptedKey;
|
||||
private String iv;
|
||||
private String ciphertext;
|
||||
private String signature;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.fineract.portfolio.clearing.npci.crypto.NpciEnvelope;
|
||||
|
||||
/**
|
||||
* Wire shape NPCI pushes to this CBS's inward-callback URL — matches NPCI's own {@code RoutedPackageView}
|
||||
* field-for-field (id/sourceBankCode/destinationBankCode/sourceBatchReference/status/plaintextPackageJson/
|
||||
* envelope/createdOn); only {@code envelope} is actually cryptographically meaningful, the rest is metadata.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class NpciInwardPushRequest {
|
||||
|
||||
private Long id;
|
||||
private String sourceBankCode;
|
||||
private String destinationBankCode;
|
||||
private String sourceBatchReference;
|
||||
private String status;
|
||||
private String plaintextPackageJson;
|
||||
private NpciEnvelope envelope;
|
||||
private String createdOn;
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.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;
|
||||
|
||||
/**
|
||||
* This CBS's own RSA key pair for talking to the real, separate NPCI clearing-house service — a singleton
|
||||
* row, generated once on first use. {@code privateKeyPem} never leaves this process (no API exposes it);
|
||||
* only {@code publicKeyPem} is ever sent to NPCI, via bank self-registration.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_clearing_own_identity")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class ClearingOwnIdentity extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "public_key_pem", nullable = false, columnDefinition = "TEXT")
|
||||
private String publicKeyPem;
|
||||
|
||||
@Column(name = "private_key_pem", nullable = false, columnDefinition = "TEXT")
|
||||
private String privateKeyPem;
|
||||
|
||||
@Column(name = "registered_with_npci", nullable = false)
|
||||
private boolean registeredWithNpci;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
public static ClearingOwnIdentity create(final String publicKeyPem, final String privateKeyPem) {
|
||||
final ClearingOwnIdentity identity = new ClearingOwnIdentity();
|
||||
identity.publicKeyPem = publicKeyPem;
|
||||
identity.privateKeyPem = privateKeyPem;
|
||||
identity.registeredWithNpci = false;
|
||||
identity.createdOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return identity;
|
||||
}
|
||||
|
||||
public void markRegisteredWithNpci() {
|
||||
this.registeredWithNpci = true;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClearingOwnIdentityRepository extends JpaRepository<ClearingOwnIdentity, Long> {
|
||||
|
||||
/** Singleton row — there is ever only one. */
|
||||
Optional<ClearingOwnIdentity> findFirstByOrderByIdAsc();
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.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;
|
||||
|
||||
/**
|
||||
* One row per package NPCI has pushed to this CBS's inward-callback endpoint — decrypted with this bank's
|
||||
* own private key, NPCI's signature verified against NPCI's public key, and stored here for manual review.
|
||||
* Deliberately does NOT auto-create an {@code InwardClearingBatch}/item — inward stays a manual, reviewed
|
||||
* step for now; this is the raw record of what NPCI actually delivered.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_npci_inward_push_log")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class NpciInwardPushLog extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "npci_package_id")
|
||||
private Long npciPackageId;
|
||||
|
||||
@Column(name = "source_bank_code", length = 20)
|
||||
private String sourceBankCode;
|
||||
|
||||
@Column(name = "destination_bank_code", length = 20)
|
||||
private String destinationBankCode;
|
||||
|
||||
@Column(name = "source_batch_reference", length = 100)
|
||||
private String sourceBatchReference;
|
||||
|
||||
@Column(name = "signature_valid", nullable = false)
|
||||
private boolean signatureValid;
|
||||
|
||||
@Column(name = "decrypted_package_json", columnDefinition = "TEXT")
|
||||
private String decryptedPackageJson;
|
||||
|
||||
@Column(name = "received_on")
|
||||
private OffsetDateTime receivedOn;
|
||||
|
||||
public static NpciInwardPushLog create(final Long npciPackageId, final String sourceBankCode, final String destinationBankCode,
|
||||
final String sourceBatchReference, final boolean signatureValid, final String decryptedPackageJson) {
|
||||
final NpciInwardPushLog log = new NpciInwardPushLog();
|
||||
log.npciPackageId = npciPackageId;
|
||||
log.sourceBankCode = sourceBankCode;
|
||||
log.destinationBankCode = destinationBankCode;
|
||||
log.sourceBatchReference = sourceBatchReference;
|
||||
log.signatureValid = signatureValid;
|
||||
log.decryptedPackageJson = decryptedPackageJson;
|
||||
log.receivedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return log;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.npci.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface NpciInwardPushLogRepository extends JpaRepository<NpciInwardPushLog, Long> {
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.npci.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.PublicKey;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.chequebook.domain.ChequeBook;
|
||||
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeaf;
|
||||
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeafRepository;
|
||||
import org.apache.fineract.portfolio.clearing.npci.client.NpciClearingClient;
|
||||
import org.apache.fineract.portfolio.clearing.npci.crypto.NpciCryptoUtil;
|
||||
import org.apache.fineract.portfolio.clearing.npci.data.NpciInwardPushRequest;
|
||||
import org.apache.fineract.portfolio.clearing.npci.domain.NpciInwardPushLog;
|
||||
import org.apache.fineract.portfolio.clearing.npci.domain.NpciInwardPushLogRepository;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatch;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatchRepository;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatchRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingItem;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingItemRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Handles a package NPCI pushes to this CBS's inward-callback endpoint: decrypts it with this bank's own
|
||||
* private key, verifies NPCI's signature against NPCI's public key, records the result in
|
||||
* {@link NpciInwardPushLog} for audit, and — when the signature is valid — auto-creates a real
|
||||
* {@link InwardClearingBatch}/{@link InwardClearingItem} set (the same entities the manual "Receive Batch"
|
||||
* screen creates), so it shows up on the existing Inward screen and flows through the existing
|
||||
* validate/decide UI unchanged. An unsigned/invalid package is logged but never turned into clearing data.
|
||||
*/
|
||||
@Service
|
||||
public class NpciInwardPushService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NpciInwardPushService.class);
|
||||
|
||||
private final NpciCryptoUtil cryptoUtil;
|
||||
private final NpciClearingClient npciClearingClient;
|
||||
private final NpciInwardPushLogRepository pushLogRepository;
|
||||
private final InwardClearingBatchRepository batchRepository;
|
||||
private final InwardClearingBatchRepositoryWrapper batchRepositoryWrapper;
|
||||
private final InwardClearingItemRepository itemRepository;
|
||||
private final ChequeLeafRepository chequeLeafRepository;
|
||||
private final ContentStoreService contentStoreService;
|
||||
|
||||
public NpciInwardPushService(final NpciCryptoUtil cryptoUtil, final NpciClearingClient npciClearingClient,
|
||||
final NpciInwardPushLogRepository pushLogRepository, final InwardClearingBatchRepository batchRepository,
|
||||
final InwardClearingBatchRepositoryWrapper batchRepositoryWrapper, final InwardClearingItemRepository itemRepository,
|
||||
final ChequeLeafRepository chequeLeafRepository, final ContentStoreService contentStoreService) {
|
||||
this.cryptoUtil = cryptoUtil;
|
||||
this.npciClearingClient = npciClearingClient;
|
||||
this.pushLogRepository = pushLogRepository;
|
||||
this.batchRepository = batchRepository;
|
||||
this.batchRepositoryWrapper = batchRepositoryWrapper;
|
||||
this.itemRepository = itemRepository;
|
||||
this.chequeLeafRepository = chequeLeafRepository;
|
||||
this.contentStoreService = contentStoreService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void receivePush(final NpciInwardPushRequest request) {
|
||||
log.info("Received NPCI inward push: packageId={} sourceBank={} batchRef={}", request.getId(), request.getSourceBankCode(),
|
||||
request.getSourceBatchReference());
|
||||
|
||||
final String decrypted = this.cryptoUtil.decryptOwn(request.getEnvelope());
|
||||
|
||||
final PublicKey npciPublicKey = this.npciClearingClient.fetchNpciPublicKey();
|
||||
final boolean signatureValid = this.cryptoUtil.verify(request.getEnvelope(), npciPublicKey);
|
||||
if (!signatureValid) {
|
||||
log.warn("NPCI inward push packageId={} FAILED signature verification — stored anyway, flagged for review",
|
||||
request.getId());
|
||||
}
|
||||
|
||||
final NpciInwardPushLog savedLog = this.pushLogRepository.save(NpciInwardPushLog.create(request.getId(),
|
||||
request.getSourceBankCode(), request.getDestinationBankCode(), request.getSourceBatchReference(), signatureValid,
|
||||
decrypted));
|
||||
log.info("Stored NPCI inward push as log id={} (signatureValid={})", savedLog.getId(), signatureValid);
|
||||
|
||||
if (!signatureValid) {
|
||||
log.warn("Skipping auto-batch creation for packageId={} — signature invalid", request.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
final String batchReference = "NPCI-PKG-" + request.getId();
|
||||
if (this.batchRepository.existsByBatchReferenceIgnoreCase(batchReference)) {
|
||||
log.info("Inward batch {} already exists (re-push) — skipping duplicate creation", batchReference);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
createInwardBatch(batchReference, decrypted, request);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to auto-create inward batch from NPCI push packageId={}: {}", request.getId(), e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createInwardBatch(final String batchReference, final String decrypted, final NpciInwardPushRequest request) {
|
||||
final JsonObject root = JsonParser.parseString(decrypted).getAsJsonObject();
|
||||
final JsonObject header = root.has("inwardPackageHeader") ? root.getAsJsonObject("inwardPackageHeader") : null;
|
||||
final String originalBatchReference = header != null && header.has("originalBatchReference")
|
||||
? header.get("originalBatchReference").getAsString()
|
||||
: request.getSourceBatchReference();
|
||||
final String sourceBankCode = header != null && header.has("sourceBankCode") ? header.get("sourceBankCode").getAsString()
|
||||
: request.getSourceBankCode();
|
||||
final JsonArray instruments = root.has("instruments") ? root.getAsJsonArray("instruments") : new JsonArray();
|
||||
|
||||
final LocalDate sessionDate = DateUtils.getBusinessLocalDate();
|
||||
final InwardClearingBatch batch = InwardClearingBatch.receive(batchReference, sessionDate, "MICR",
|
||||
"NPCI push — original batch " + originalBatchReference, sourceBankCode, null, null, null);
|
||||
final InwardClearingBatch savedBatch = this.batchRepositoryWrapper.saveAndFlush(batch);
|
||||
|
||||
final List<InwardClearingItem> items = new ArrayList<>();
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
for (final JsonElement el : instruments) {
|
||||
final InwardClearingItem item = buildItem(savedBatch, el.getAsJsonObject());
|
||||
items.add(item);
|
||||
totalAmount = totalAmount.add(item.getAmount() == null ? BigDecimal.ZERO : item.getAmount());
|
||||
}
|
||||
this.itemRepository.saveAll(items);
|
||||
// flush explicitly — saveAll() alone does not guarantee IDENTITY-generated ids are populated yet, and
|
||||
// attachImages() below needs a real item id to build each image's storage path.
|
||||
this.itemRepository.flush();
|
||||
|
||||
// images are attached after the first save+flush so each item has a real id for the storage path
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
attachImages(items.get(i), instruments.get(i).getAsJsonObject());
|
||||
}
|
||||
this.itemRepository.saveAll(items);
|
||||
|
||||
savedBatch.setTotals(items.size(), totalAmount);
|
||||
this.batchRepositoryWrapper.saveAndFlush(savedBatch);
|
||||
log.info("Auto-created inward clearing batch {} ({} item(s), total {}) from NPCI push packageId={}", savedBatch.getId(),
|
||||
items.size(), totalAmount, request.getId());
|
||||
}
|
||||
|
||||
private InwardClearingItem buildItem(final InwardClearingBatch batch, final JsonObject instrument) {
|
||||
final String chequeNumber = getString(instrument, "instrumentNumber");
|
||||
final String micrCode = getString(instrument, "micrCode");
|
||||
final String ifsc = getString(instrument, "draweeBranchIfsc");
|
||||
final String payeeName = getString(instrument, "payeeName");
|
||||
final BigDecimal amount = instrument.has("amount") && !instrument.get("amount").isJsonNull()
|
||||
? instrument.get("amount").getAsBigDecimal()
|
||||
: BigDecimal.ZERO;
|
||||
final String amountInWords = getString(instrument, "amountInWords");
|
||||
final LocalDate instrumentDate = parseDate(getString(instrument, "instrumentDate"));
|
||||
final LocalDate presentedDate = DateUtils.getBusinessLocalDate();
|
||||
|
||||
final ResolvedAccount resolved = resolveAccount(chequeNumber);
|
||||
|
||||
final InwardClearingItem item = InwardClearingItem.receive(batch, resolved.chequeLeafId, resolved.accountType,
|
||||
resolved.accountId, chequeNumber, micrCode, ifsc, payeeName, amount, amountInWords, instrumentDate, presentedDate);
|
||||
|
||||
final JsonObject images = instrument.has("images") ? instrument.getAsJsonObject("images") : null;
|
||||
final JsonObject frontImage = images != null && images.has("frontImage") ? images.getAsJsonObject("frontImage") : null;
|
||||
final JsonObject backImage = images != null && images.has("backImage") ? images.getAsJsonObject("backImage") : null;
|
||||
item.captureImages(getString(frontImage, "imageRef"), getString(backImage, "imageRef"), getString(instrument, "iqaStatus"),
|
||||
getString(instrument, "iqaDetail"));
|
||||
|
||||
// presentingBranchCode is the DEPOSITING bank's own branch (from the instrument's own depositBranchCode),
|
||||
// not the sender bank's code — the batch already carries sourceBankCode separately via presentingBankCode.
|
||||
final String presentingBranchCode = getString(instrument, "depositBranchCode");
|
||||
item.recordInwardMeta(getString(instrument, "serialNumber"), presentingBranchCode, batch.getReturnTatDeadline());
|
||||
|
||||
final Boolean instrumentStopPaymentFlag = instrument.has("stopPaymentFlag") && !instrument.get("stopPaymentFlag").isJsonNull()
|
||||
? instrument.get("stopPaymentFlag").getAsBoolean()
|
||||
: null;
|
||||
item.recordSourceDetails(getString(instrument, "ctsReferenceNumber"), instrumentStopPaymentFlag,
|
||||
parseDate(getString(instrument, "depositDate")));
|
||||
return item;
|
||||
}
|
||||
|
||||
/** Resolves which of THIS bank's own accounts a cheque number belongs to, via its cheque leaf, if one matches. */
|
||||
private ResolvedAccount resolveAccount(final String chequeNumber) {
|
||||
final ResolvedAccount resolved = new ResolvedAccount();
|
||||
if (chequeNumber == null) {
|
||||
return resolved;
|
||||
}
|
||||
try {
|
||||
final Long serial = Long.valueOf(chequeNumber.trim());
|
||||
final ChequeLeaf leaf = this.chequeLeafRepository.findByLeafSerialNo(serial).orElse(null);
|
||||
if (leaf != null) {
|
||||
resolved.chequeLeafId = leaf.getId();
|
||||
final ChequeBook book = leaf.getChequeBook();
|
||||
if (book != null) {
|
||||
resolved.accountType = book.getAccountType();
|
||||
resolved.accountId = book.getAccountId();
|
||||
}
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
// non-numeric cheque number, or no matching leaf — left unresolved; Validate will flag "No drawn-on
|
||||
// account linked" and a human reconciles it, same as a real bank would for an unmatched instrument.
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** Decodes and stores an instrument's embedded cheque image bytes, if any were included. Best-effort only. */
|
||||
private void attachImages(final InwardClearingItem item, final JsonObject instrument) {
|
||||
if (!instrument.has("images")) {
|
||||
return;
|
||||
}
|
||||
final JsonObject images = instrument.getAsJsonObject("images");
|
||||
attachImageSide(item, "front", images.has("frontImage") ? images.getAsJsonObject("frontImage") : null);
|
||||
attachImageSide(item, "back", images.has("backImage") ? images.getAsJsonObject("backImage") : null);
|
||||
}
|
||||
|
||||
private void attachImageSide(final InwardClearingItem item, final String side, final JsonObject imageBlock) {
|
||||
if (imageBlock == null || !"base64".equalsIgnoreCase(getString(imageBlock, "encoding")) || !imageBlock.has("data")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final byte[] bytes = Base64.getDecoder().decode(imageBlock.get("data").getAsString());
|
||||
String mimeType = getString(imageBlock, "format");
|
||||
if (mimeType == null || mimeType.isBlank()) {
|
||||
mimeType = "image/png";
|
||||
}
|
||||
final String storagePath = "images" + this.contentStoreService.getDelimiter() + "ctsclearing-inward"
|
||||
+ this.contentStoreService.getDelimiter() + item.getId() + this.contentStoreService.getDelimiter() + side
|
||||
+ extensionForMime(mimeType);
|
||||
final String savedPath = this.contentStoreService.upload(storagePath, new ByteArrayInputStream(bytes), mimeType);
|
||||
item.recordImageStore(side, mimeType + "|" + savedPath);
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to store {} image for auto-created inward item {} from NPCI push: {}", side, item.getId(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static String extensionForMime(final String mimeType) {
|
||||
final String lower = mimeType.toLowerCase();
|
||||
if (lower.contains("jpeg") || lower.contains("jpg")) {
|
||||
return ".jpg";
|
||||
}
|
||||
if (lower.contains("gif")) {
|
||||
return ".gif";
|
||||
}
|
||||
return ".png";
|
||||
}
|
||||
|
||||
private static String getString(final JsonObject obj, final String key) {
|
||||
return obj != null && obj.has(key) && !obj.get(key).isJsonNull() ? obj.get(key).getAsString() : null;
|
||||
}
|
||||
|
||||
private static LocalDate parseDate(final String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value);
|
||||
} catch (final RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ResolvedAccount {
|
||||
|
||||
private Long chequeLeafId;
|
||||
private String accountType;
|
||||
private Long accountId;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.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.clearing.api.ClearingApiConstants;
|
||||
import org.apache.fineract.portfolio.clearing.api.ClearingApiJsonInputParams;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class ClearingCommandFromApiJsonDeserializer {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForCreateSettlementBatch(final String json) {
|
||||
final JsonElement element = parse(json, ClearingApiJsonInputParams.settlementBatchCreateParams());
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClearingApiConstants.RESOURCE_SETTLEMENT_BATCH);
|
||||
|
||||
final String clearingType = this.fromApiJsonHelper.extractStringNamed(ClearingApiJsonInputParams.CLEARING_TYPE.getValue(), element);
|
||||
// NB: isOneOfTheseStringValues lowercases the input before comparing, so the expected set must be lowercase
|
||||
v.reset().parameter(ClearingApiJsonInputParams.CLEARING_TYPE.getValue()).value(clearingType).notBlank()
|
||||
.isOneOfTheseStringValues("cts", "nach");
|
||||
|
||||
final Long sourceBatchId = this.fromApiJsonHelper.extractLongNamed(ClearingApiJsonInputParams.SOURCE_BATCH_ID.getValue(), element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.SOURCE_BATCH_ID.getValue()).value(sourceBatchId).notNull().longGreaterThanZero();
|
||||
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForReconciliation(final String json) {
|
||||
final JsonElement element = parse(json, ClearingApiJsonInputParams.reconciliationParams());
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClearingApiConstants.RESOURCE_RECONCILIATION);
|
||||
|
||||
final String clearingType = this.fromApiJsonHelper.extractStringNamed(ClearingApiJsonInputParams.CLEARING_TYPE.getValue(), element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.CLEARING_TYPE.getValue()).value(clearingType).notBlank()
|
||||
.isOneOfTheseStringValues("cts", "nach");
|
||||
|
||||
final LocalDate reconciliationDate = this.fromApiJsonHelper
|
||||
.extractLocalDateNamed(ClearingApiJsonInputParams.RECONCILIATION_DATE.getValue(), element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.RECONCILIATION_DATE.getValue()).value(reconciliationDate).notNull();
|
||||
|
||||
final BigDecimal cbsTotal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(ClearingApiJsonInputParams.CBS_TOTAL.getValue(),
|
||||
element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.CBS_TOTAL.getValue()).value(cbsTotal).notNull();
|
||||
|
||||
final BigDecimal clearingHouseTotal = this.fromApiJsonHelper
|
||||
.extractBigDecimalWithLocaleNamed(ClearingApiJsonInputParams.CLEARING_HOUSE_TOTAL.getValue(), element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.CLEARING_HOUSE_TOTAL.getValue()).value(clearingHouseTotal).notNull();
|
||||
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForRejectSettlement(final String json) {
|
||||
final JsonElement element = parse(json, ClearingApiJsonInputParams.rejectSettlementParams());
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClearingApiConstants.RESOURCE_SETTLEMENT_BATCH);
|
||||
|
||||
final String reason = this.fromApiJsonHelper.extractStringNamed(ClearingApiJsonInputParams.REASON.getValue(), element);
|
||||
v.reset().parameter(ClearingApiJsonInputParams.REASON.getValue()).value(reason).notBlank().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Single home for the "which GL account backs this thing" lookups the clearing modules need. Extracts the
|
||||
* control-GL query that was previously copy-pasted in {@code ChequeBookIssuanceChargeService} and
|
||||
* {@code ClerkDisbursementRequestApiResource} into one shared bean (a follow-up change can have those two
|
||||
* delegate here so there is exactly one copy), and adds the clearing-suspense/settlement GL lookup keyed off
|
||||
* {@code m_clearing_gl_config}.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClearingGlAccountResolverService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** The customer account's control GL (SAVINGS or CURRENT), or null when no product mapping exists. */
|
||||
public Long resolveControlGlAccountId(final String accountType, final Long accountId) {
|
||||
if (accountId == null || accountType == null) {
|
||||
return null;
|
||||
}
|
||||
if ("SAVINGS".equalsIgnoreCase(accountType)) {
|
||||
return querySingleLong("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", accountId);
|
||||
}
|
||||
if ("CURRENT".equalsIgnoreCase(accountType)) {
|
||||
return querySingleLong("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", accountId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The GL account configured for a clearing role (e.g. {@code CTS_INWARD_SUSPENSE}), or null if unmapped. */
|
||||
public Long resolveClearingGlAccountId(final String configKey) {
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate
|
||||
.queryForList("SELECT gl_account_id FROM m_clearing_gl_config WHERE config_key = ? LIMIT 1", configKey);
|
||||
if (rows.isEmpty() || rows.get(0).get("gl_account_id") == null) {
|
||||
return null;
|
||||
}
|
||||
return ((Number) rows.get(0).get("gl_account_id")).longValue();
|
||||
}
|
||||
|
||||
private Long querySingleLong(final String sql, final Object... args) {
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, args);
|
||||
return rows.isEmpty() ? null : ((Number) rows.get(0).get("gl_account_id")).longValue();
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.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 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.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Posts manual journal entries for the clearing modules through Fineract's standard {@code createJournalEntry}
|
||||
* command pipeline — the same mechanism {@code ChequeBookIssuanceChargeService.postChargeJournalEntry} uses.
|
||||
* Centralised here so CTS per-item debits, NACH postings, and settlement summaries all share one balanced,
|
||||
* audited posting path (and one place to link a GL row back to a customer account for reporting).
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ClearingJournalService {
|
||||
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final FineractRequestContextHolder fineractRequestContextHolder;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* Posts a single-debit / single-credit journal entry and returns the resulting GL transaction id (or null
|
||||
* on failure). {@code linkAccountId} is optional — when supplied, the debit row is linked to that customer
|
||||
* account so it shows an "Account No" in the Journal Entries screen.
|
||||
*/
|
||||
public String postJournalEntry(final Long officeId, final String currencyCode, final LocalDate txnDate, final Long debitGlId,
|
||||
final Long creditGlId, final BigDecimal amount, final String comments, final Long linkAccountId) {
|
||||
if (amount == null || amount.signum() <= 0 || debitGlId == null || creditGlId == null || officeId == null) {
|
||||
log.warn("Skipping clearing journal entry - missing office/GL/amount (office={}, dr={}, cr={}, amt={}).", officeId, debitGlId,
|
||||
creditGlId, amount);
|
||||
return null;
|
||||
}
|
||||
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", comments);
|
||||
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("Clearing journal entry did not return a result ({}).", comments);
|
||||
return null;
|
||||
}
|
||||
final String glTxnId = result.getTransactionId();
|
||||
if (glTxnId != null && linkAccountId != null) {
|
||||
linkDebitEntryToAccount(glTxnId, linkAccountId);
|
||||
}
|
||||
return glTxnId;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingNotification;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingNotificationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Queues simulated customer notifications for clearing events. A single write path used by both CTS and NACH
|
||||
* decisioning; if a real SMS/email gateway is ever wired in, this is the one place to change.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ClearingNotificationService {
|
||||
|
||||
private final ClearingNotificationRepository repository;
|
||||
|
||||
public void notifyCustomer(final String channel, final String recipient, final String event, final String clearingType,
|
||||
final String refType, final Long refId, final String message) {
|
||||
final ClearingNotification notification = ClearingNotification.simulated(channel, recipient, event, clearingType, refType, refId,
|
||||
message);
|
||||
this.repository.save(notification);
|
||||
log.info("[CLEARING-NOTIFY][{}] {} -> {} : {}", clearingType, event, recipient, message);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.clearing.data.ClearingReconciliationData;
|
||||
import org.apache.fineract.portfolio.clearing.data.ClearingReturnReasonCodeData;
|
||||
import org.apache.fineract.portfolio.clearing.data.ClearingSettlementBatchData;
|
||||
|
||||
public interface ClearingReadPlatformService {
|
||||
|
||||
List<ClearingReturnReasonCodeData> retrieveReturnReasonCodes(String appliesTo);
|
||||
|
||||
Page<ClearingSettlementBatchData> retrieveSettlementBatches(String clearingType, String status, Integer offset, Integer limit);
|
||||
|
||||
ClearingSettlementBatchData retrieveSettlementBatch(Long id);
|
||||
|
||||
Page<ClearingReconciliationData> retrieveReconciliations(String clearingType, Integer offset, Integer limit);
|
||||
|
||||
ClearingReconciliationData retrieveReconciliation(Long id);
|
||||
}
|
||||
+192
@@ -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.clearing.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.clearing.data.ClearingReconciliationData;
|
||||
import org.apache.fineract.portfolio.clearing.data.ClearingReturnReasonCodeData;
|
||||
import org.apache.fineract.portfolio.clearing.data.ClearingSettlementBatchData;
|
||||
import org.apache.fineract.portfolio.clearing.exception.ClearingReconciliationNotFoundException;
|
||||
import org.apache.fineract.portfolio.clearing.exception.ClearingSettlementBatchNotFoundException;
|
||||
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 ClearingReadPlatformServiceImpl implements ClearingReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ReturnReasonCodeMapper returnReasonCodeMapper = new ReturnReasonCodeMapper();
|
||||
private final SettlementBatchMapper settlementBatchMapper = new SettlementBatchMapper();
|
||||
private final ReconciliationMapper reconciliationMapper = new ReconciliationMapper();
|
||||
|
||||
@Override
|
||||
public List<ClearingReturnReasonCodeData> retrieveReturnReasonCodes(final String appliesTo) {
|
||||
final StringBuilder where = new StringBuilder(" where is_active = true");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(appliesTo)) {
|
||||
where.append(" and (applies_to = ? or applies_to = 'BOTH')");
|
||||
args.add(appliesTo.trim().toUpperCase());
|
||||
}
|
||||
final String sql = "select " + this.returnReasonCodeMapper.schema() + where + " order by code asc";
|
||||
return this.jdbcTemplate.query(sql, this.returnReasonCodeMapper, args.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ClearingSettlementBatchData> retrieveSettlementBatches(final String clearingType, 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(clearingType)) {
|
||||
where.append(" and clearing_type = ?");
|
||||
args.add(clearingType.trim().toUpperCase());
|
||||
}
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
where.append(" and status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
}
|
||||
|
||||
final String countSql = "select count(*) from (select " + this.settlementBatchMapper.schema() + where + ") x";
|
||||
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final StringBuilder pageSql = new StringBuilder("select ").append(this.settlementBatchMapper.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<ClearingSettlementBatchData> batches = this.jdbcTemplate.query(pageSql.toString(), this.settlementBatchMapper,
|
||||
pageArgs.toArray());
|
||||
return new Page<>(batches, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClearingSettlementBatchData retrieveSettlementBatch(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.settlementBatchMapper.schema() + " where id = ?";
|
||||
return this.jdbcTemplate.queryForObject(sql, this.settlementBatchMapper, id);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ClearingSettlementBatchNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ClearingReconciliationData> retrieveReconciliations(final String clearingType, final Integer offset, final Integer limit) {
|
||||
final StringBuilder where = new StringBuilder(" where 1=1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(clearingType)) {
|
||||
where.append(" and clearing_type = ?");
|
||||
args.add(clearingType.trim().toUpperCase());
|
||||
}
|
||||
|
||||
final String countSql = "select count(*) from (select " + this.reconciliationMapper.schema() + where + ") x";
|
||||
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final StringBuilder pageSql = new StringBuilder("select ").append(this.reconciliationMapper.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<ClearingReconciliationData> reconciliations = this.jdbcTemplate.query(pageSql.toString(), this.reconciliationMapper,
|
||||
pageArgs.toArray());
|
||||
return new Page<>(reconciliations, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClearingReconciliationData retrieveReconciliation(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.reconciliationMapper.schema() + " where id = ?";
|
||||
return this.jdbcTemplate.queryForObject(sql, this.reconciliationMapper, id);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ClearingReconciliationNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ReturnReasonCodeMapper implements RowMapper<ClearingReturnReasonCodeData> {
|
||||
|
||||
String schema() {
|
||||
return " id as id, code as code, description as description, category as category, applies_to as applies_to, "
|
||||
+ "is_active as is_active from m_clearing_return_reason_code";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClearingReturnReasonCodeData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return ClearingReturnReasonCodeData.builder().id(rs.getLong("id")).code(rs.getString("code"))
|
||||
.description(rs.getString("description")).category(rs.getString("category")).appliesTo(rs.getString("applies_to"))
|
||||
.active(rs.getBoolean("is_active")).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SettlementBatchMapper implements RowMapper<ClearingSettlementBatchData> {
|
||||
|
||||
String schema() {
|
||||
return " id as id, clearing_type as clearing_type, direction as direction, settlement_date as settlement_date, "
|
||||
+ "source_reference as source_reference, source_batch_id as source_batch_id, "
|
||||
+ "total_passed_amount as total_passed_amount, total_returned_amount as total_returned_amount, "
|
||||
+ "net_amount as net_amount, passed_count as passed_count, returned_count as returned_count, "
|
||||
+ "office_id as office_id, currency_code as currency_code, gl_transaction_id as gl_transaction_id, "
|
||||
+ "status as status, prepared_by as prepared_by, prepared_on as prepared_on, posted_by as posted_by, "
|
||||
+ "posted_on as posted_on, rejection_reason as rejection_reason from m_clearing_settlement_batch";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClearingSettlementBatchData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return ClearingSettlementBatchData.builder().id(rs.getLong("id")).clearingType(rs.getString("clearing_type"))
|
||||
.direction(rs.getString("direction")).settlementDate(rs.getObject("settlement_date", LocalDate.class))
|
||||
.sourceReference(rs.getString("source_reference")).sourceBatchId((Long) rs.getObject("source_batch_id"))
|
||||
.totalPassedAmount(rs.getBigDecimal("total_passed_amount"))
|
||||
.totalReturnedAmount(rs.getBigDecimal("total_returned_amount")).netAmount(rs.getBigDecimal("net_amount"))
|
||||
.passedCount((Integer) rs.getObject("passed_count")).returnedCount((Integer) rs.getObject("returned_count"))
|
||||
.officeId((Long) rs.getObject("office_id")).currencyCode(rs.getString("currency_code"))
|
||||
.glTransactionId(rs.getString("gl_transaction_id")).status(rs.getString("status"))
|
||||
.preparedBy((Long) rs.getObject("prepared_by")).preparedOn(rs.getObject("prepared_on", OffsetDateTime.class))
|
||||
.postedBy((Long) rs.getObject("posted_by")).postedOn(rs.getObject("posted_on", OffsetDateTime.class))
|
||||
.rejectionReason(rs.getString("rejection_reason")).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ReconciliationMapper implements RowMapper<ClearingReconciliationData> {
|
||||
|
||||
String schema() {
|
||||
return " id as id, clearing_type as clearing_type, reconciliation_date as reconciliation_date, "
|
||||
+ "source_reference as source_reference, cbs_total as cbs_total, clearing_house_total as clearing_house_total, "
|
||||
+ "difference as difference, status as status, detail as detail, run_by as run_by, created_on as created_on "
|
||||
+ "from m_clearing_reconciliation";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClearingReconciliationData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return ClearingReconciliationData.builder().id(rs.getLong("id")).clearingType(rs.getString("clearing_type"))
|
||||
.reconciliationDate(rs.getObject("reconciliation_date", LocalDate.class))
|
||||
.sourceReference(rs.getString("source_reference")).cbsTotal(rs.getBigDecimal("cbs_total"))
|
||||
.clearingHouseTotal(rs.getBigDecimal("clearing_house_total")).difference(rs.getBigDecimal("difference"))
|
||||
.status(rs.getString("status")).detail(rs.getString("detail")).runBy((Long) rs.getObject("run_by"))
|
||||
.createdOn(rs.getObject("created_on", OffsetDateTime.class)).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingReconciliation;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingReconciliationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Records a reconciliation run comparing the CBS's own settled total for a session against the clearing house's
|
||||
* reported figure (spec sections 4.11 & 8.9). The clearing-house total is operator-supplied in this
|
||||
* simulation. Any non-zero difference is flagged as a MISMATCH for follow-up.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClearingReconciliationService {
|
||||
|
||||
private final ClearingReconciliationRepository repository;
|
||||
|
||||
public ClearingReconciliation reconcile(final String clearingType, final LocalDate reconciliationDate, final String sourceReference,
|
||||
final BigDecimal cbsTotal, final BigDecimal clearingHouseTotal, final String detail, final Long runBy) {
|
||||
final ClearingReconciliation reconciliation = ClearingReconciliation.run(clearingType, reconciliationDate, sourceReference,
|
||||
cbsTotal, clearingHouseTotal, detail, runBy);
|
||||
return this.repository.save(reconciliation);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingSettlementBatch;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingSettlementBatchRepositoryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Net-settlement engine shared by CTS and NACH (spec sections 4.9 & 8.8). {@link #prepare} computes a
|
||||
* PENDING settlement batch from a decisioned session's totals; {@link #post} authorises it (a different user
|
||||
* than the preparer) and writes the summary GL entry squaring off the clearing suspense account against the
|
||||
* settlement (RTGS) account; {@link #reject} discards it.
|
||||
*
|
||||
* <p>
|
||||
* Follows the "Model A" design: individual passed items already posted their own customer-debit/suspense-credit
|
||||
* entries at decisioning time, so this summary entry moves the accumulated suspense balance to the settlement
|
||||
* account — it does not re-debit customers. If the suspense/settlement GL accounts are not configured, the
|
||||
* batch is still recorded (as a reconciliation control record) but no GL entry is posted, mirroring the
|
||||
* graceful degradation of {@code ChequeBookIssuanceChargeService}.
|
||||
* </p>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ClearingSettlementService {
|
||||
|
||||
private final ClearingSettlementBatchRepositoryWrapper settlementRepositoryWrapper;
|
||||
private final ClearingGlAccountResolverService glResolver;
|
||||
private final ClearingJournalService journalService;
|
||||
|
||||
private static final String SETTLEMENT_RTGS = "CLEARING_SETTLEMENT_RTGS";
|
||||
|
||||
public 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 = ClearingSettlementBatch.prepare(clearingType, direction, settlementDate, sourceReference,
|
||||
sourceBatchId, totalPassed, totalReturned, passedCount, returnedCount, officeId, currencyCode, preparedBy);
|
||||
return this.settlementRepositoryWrapper.saveAndFlush(batch);
|
||||
}
|
||||
|
||||
public ClearingSettlementBatch post(final Long settlementBatchId, final Long postedByUserId) {
|
||||
final ClearingSettlementBatch batch = this.settlementRepositoryWrapper.findOneWithNotFoundDetection(settlementBatchId);
|
||||
if (!batch.isPending()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.clearing.settlement.not.pending",
|
||||
"Settlement batch " + settlementBatchId + " is not PENDING.", settlementBatchId);
|
||||
}
|
||||
// Two-person control enforced in code, not just via can_maker_checker (ALL_FUNCTIONS bypasses the flag).
|
||||
if (batch.isPreparedBy(postedByUserId)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.clearing.settlement.self.post",
|
||||
"The user who prepared settlement batch " + settlementBatchId + " cannot also post it.", settlementBatchId);
|
||||
}
|
||||
|
||||
final String suspenseKey = suspenseKeyFor(batch.getClearingType());
|
||||
final Long suspenseGl = this.glResolver.resolveClearingGlAccountId(suspenseKey);
|
||||
final Long settlementGl = this.glResolver.resolveClearingGlAccountId(SETTLEMENT_RTGS);
|
||||
String glTxnId = null;
|
||||
final BigDecimal netPassed = batch.getTotalPassedAmount();
|
||||
if (suspenseGl != null && settlementGl != null && netPassed != null && netPassed.signum() > 0) {
|
||||
// DR clearing suspense (drawing down the accumulated passed-item credits), CR settlement/RTGS.
|
||||
glTxnId = this.journalService.postJournalEntry(batch.getOfficeId(), batch.getCurrencyCode(), DateUtils.getBusinessLocalDate(),
|
||||
suspenseGl, settlementGl, netPassed,
|
||||
batch.getClearingType() + " net settlement - session " + batch.getSourceReference(), null);
|
||||
} else {
|
||||
log.warn("Settlement batch {} posted without a GL entry - suspense ({}) or settlement ({}) GL not configured, or zero net.",
|
||||
settlementBatchId, suspenseGl, settlementGl);
|
||||
}
|
||||
batch.markPosted(postedByUserId, glTxnId);
|
||||
return this.settlementRepositoryWrapper.saveAndFlush(batch);
|
||||
}
|
||||
|
||||
public ClearingSettlementBatch reject(final Long settlementBatchId, final Long rejectedByUserId, final String reason) {
|
||||
final ClearingSettlementBatch batch = this.settlementRepositoryWrapper.findOneWithNotFoundDetection(settlementBatchId);
|
||||
batch.reject(rejectedByUserId, reason);
|
||||
return this.settlementRepositoryWrapper.saveAndFlush(batch);
|
||||
}
|
||||
|
||||
private String suspenseKeyFor(final String clearingType) {
|
||||
if ("NACH".equalsIgnoreCase(clearingType)) {
|
||||
return "NACH_SUSPENSE";
|
||||
}
|
||||
return "CTS_INWARD_SUSPENSE";
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface ClearingWritePlatformService {
|
||||
|
||||
CommandProcessingResult prepareSettlement(JsonCommand command);
|
||||
|
||||
CommandProcessingResult postSettlement(Long settlementBatchId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult rejectSettlement(Long settlementBatchId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult createReconciliation(JsonCommand command);
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.clearing.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
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.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.clearing.api.ClearingApiJsonInputParams;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingReconciliation;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingSettlementBatch;
|
||||
import org.apache.fineract.portfolio.clearing.serialization.ClearingCommandFromApiJsonDeserializer;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ClearingWritePlatformServiceImpl implements ClearingWritePlatformService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClearingCommandFromApiJsonDeserializer validator;
|
||||
private final ClearingSettlementService settlementService;
|
||||
private final ClearingReconciliationService reconciliationService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final String DEFAULT_CURRENCY = "INR";
|
||||
|
||||
// ============================= Settlement =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult prepareSettlement(final JsonCommand command) {
|
||||
this.validator.validateForCreateSettlementBatch(command.json());
|
||||
final String clearingType = command.stringValueOfParameterNamed(ClearingApiJsonInputParams.CLEARING_TYPE.getValue())
|
||||
.trim().toUpperCase();
|
||||
final Long sourceBatchId = command.longValueOfParameterNamed(ClearingApiJsonInputParams.SOURCE_BATCH_ID.getValue());
|
||||
final Long currentUserId = this.context.authenticatedUser().getId();
|
||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
|
||||
final boolean nach = "NACH".equals(clearingType);
|
||||
final String itemTable = nach ? "m_nach_transaction" : "m_inward_clearing_item";
|
||||
final String batchTable = nach ? "m_nach_batch" : "m_inward_clearing_batch";
|
||||
|
||||
final SettlementTotals totals = computeTotals(itemTable, sourceBatchId);
|
||||
final SourceBatchInfo batchInfo = loadSourceBatch(batchTable, sourceBatchId);
|
||||
final String currency = batchInfo.currencyCode == null ? DEFAULT_CURRENCY : batchInfo.currencyCode;
|
||||
|
||||
final ClearingSettlementBatch batch = this.settlementService.prepare(clearingType, "INWARD", businessDate,
|
||||
batchInfo.batchReference, sourceBatchId, totals.passedSum, totals.returnedSum, totals.passedCount, totals.returnedCount,
|
||||
batchInfo.officeId, currency, currentUserId);
|
||||
|
||||
// tie every source item to this settlement batch, and mark the source batch SETTLED
|
||||
this.jdbcTemplate.update("update " + itemTable + " set settlement_batch_id = ? where batch_id = ?", batch.getId(), sourceBatchId);
|
||||
this.jdbcTemplate.update("update " + batchTable + " set status = 'SETTLED', settled_on = now() where id = ?", sourceBatchId);
|
||||
|
||||
log.info("Prepared {} settlement batch {} for source batch {} (passed {}, returned {})", clearingType, batch.getId(),
|
||||
sourceBatchId, totals.passedSum, totals.returnedSum);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(batch.getId()).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult postSettlement(final Long settlementBatchId, final JsonCommand command) {
|
||||
final Long currentUserId = this.context.authenticatedUser().getId();
|
||||
this.settlementService.post(settlementBatchId, currentUserId);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(settlementBatchId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectSettlement(final Long settlementBatchId, final JsonCommand command) {
|
||||
this.validator.validateForRejectSettlement(command.json());
|
||||
final String reason = command.stringValueOfParameterNamed(ClearingApiJsonInputParams.REASON.getValue());
|
||||
final Long currentUserId = this.context.authenticatedUser().getId();
|
||||
this.settlementService.reject(settlementBatchId, currentUserId, reason);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(settlementBatchId).build();
|
||||
}
|
||||
|
||||
// ============================= Reconciliation =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createReconciliation(final JsonCommand command) {
|
||||
this.validator.validateForReconciliation(command.json());
|
||||
final String clearingType = command.stringValueOfParameterNamed(ClearingApiJsonInputParams.CLEARING_TYPE.getValue())
|
||||
.trim().toUpperCase();
|
||||
final LocalDate reconciliationDate = command
|
||||
.localDateValueOfParameterNamed(ClearingApiJsonInputParams.RECONCILIATION_DATE.getValue());
|
||||
final String sourceReference = command
|
||||
.stringValueOfParameterNamed(ClearingApiJsonInputParams.SOURCE_REFERENCE.getValue());
|
||||
final BigDecimal cbsTotal = command.bigDecimalValueOfParameterNamed(ClearingApiJsonInputParams.CBS_TOTAL.getValue());
|
||||
final BigDecimal clearingHouseTotal = command
|
||||
.bigDecimalValueOfParameterNamed(ClearingApiJsonInputParams.CLEARING_HOUSE_TOTAL.getValue());
|
||||
final String detail = command.stringValueOfParameterNamed(ClearingApiJsonInputParams.DETAIL.getValue());
|
||||
final Long currentUserId = this.context.authenticatedUser().getId();
|
||||
|
||||
final ClearingReconciliation reconciliation = this.reconciliationService.reconcile(clearingType, reconciliationDate,
|
||||
sourceReference, cbsTotal, clearingHouseTotal, detail, currentUserId);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(reconciliation.getId()).build();
|
||||
}
|
||||
|
||||
// ============================= Helpers =============================
|
||||
|
||||
private SettlementTotals computeTotals(final String itemTable, final Long sourceBatchId) {
|
||||
final SettlementTotals totals = new SettlementTotals();
|
||||
// itemTable is a fixed, code-selected constant (never user input) — safe to interpolate
|
||||
final String sql = "select decision, coalesce(sum(amount), 0) as amt, count(*) as cnt from " + itemTable
|
||||
+ " where batch_id = ? group by decision";
|
||||
this.jdbcTemplate.query(sql, rs -> {
|
||||
final String decision = rs.getString("decision");
|
||||
final BigDecimal amt = rs.getBigDecimal("amt");
|
||||
final int cnt = rs.getInt("cnt");
|
||||
if ("PASS".equalsIgnoreCase(decision)) {
|
||||
totals.passedSum = amt;
|
||||
totals.passedCount = cnt;
|
||||
} else if ("RETURN".equalsIgnoreCase(decision)) {
|
||||
totals.returnedSum = amt;
|
||||
totals.returnedCount = cnt;
|
||||
}
|
||||
}, sourceBatchId);
|
||||
return totals;
|
||||
}
|
||||
|
||||
private SourceBatchInfo loadSourceBatch(final String batchTable, final Long sourceBatchId) {
|
||||
try {
|
||||
// neither source batch table carries a currency column — currency defaults to INR downstream
|
||||
final String sql = "select office_id, batch_reference from " + batchTable + " where id = ?";
|
||||
return this.jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
|
||||
final SourceBatchInfo info = new SourceBatchInfo();
|
||||
info.officeId = (Long) rs.getObject("office_id");
|
||||
info.batchReference = rs.getString("batch_reference");
|
||||
info.currencyCode = null;
|
||||
return info;
|
||||
}, sourceBatchId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.clearing.settlement.source.batch.invalid",
|
||||
"The source batch " + sourceBatchId + " for settlement does not exist.", sourceBatchId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SettlementTotals {
|
||||
|
||||
private BigDecimal passedSum = BigDecimal.ZERO;
|
||||
private BigDecimal returnedSum = BigDecimal.ZERO;
|
||||
private int passedCount = 0;
|
||||
private int returnedCount = 0;
|
||||
}
|
||||
|
||||
private static final class SourceBatchInfo {
|
||||
|
||||
private Long officeId;
|
||||
private String batchReference;
|
||||
private String currencyCode;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.api;
|
||||
|
||||
public final class CtsClearingApiConstants {
|
||||
|
||||
private CtsClearingApiConstants() {}
|
||||
|
||||
// permission entity names (align with the CommandWrapperBuilder entity names)
|
||||
public static final String ENTITY_INWARD_BATCH = "INWARDCLEARINGBATCH";
|
||||
public static final String ENTITY_INWARD_ITEM = "INWARDCLEARINGITEM";
|
||||
public static final String ENTITY_OUTWARD_PRESENTMENT = "OUTWARDCLEARINGPRESENTMENT";
|
||||
public static final String ENTITY_POSITIVE_PAY = "POSITIVEPAYREGISTRATION";
|
||||
|
||||
// validation resource names
|
||||
public static final String RESOURCE_INWARD_BATCH = "inwardclearingbatch";
|
||||
public static final String RESOURCE_INWARD_ITEM = "inwardclearingitem";
|
||||
public static final String RESOURCE_OUTWARD_PRESENTMENT = "outwardclearingpresentment";
|
||||
|
||||
// batch create params
|
||||
public static final String BATCH_REFERENCE = "batchReference";
|
||||
public static final String SESSION_DATE = "sessionDate";
|
||||
public static final String PRESENTMENT_TYPE = "presentmentType";
|
||||
public static final String SOURCE_FILE = "sourceFile";
|
||||
public static final String PRESENTING_BANK_CODE = "presentingBankCode";
|
||||
public static final String PRESENTING_BANK_NAME = "presentingBankName";
|
||||
public static final String OFFICE_ID = "officeId";
|
||||
public static final String ITEMS = "items";
|
||||
|
||||
// item fields (also used inside each element of items[])
|
||||
public static final String CHEQUE_NUMBER = "chequeNumber";
|
||||
public static final String MICR_CODE = "micrCode";
|
||||
public static final String IFSC = "ifsc";
|
||||
public static final String PAYEE_NAME = "payeeName";
|
||||
public static final String AMOUNT = "amount";
|
||||
public static final String AMOUNT_IN_WORDS = "amountInWords";
|
||||
public static final String INSTRUMENT_DATE = "instrumentDate";
|
||||
public static final String PRESENTED_DATE = "presentedDate";
|
||||
public static final String ACCOUNT_TYPE = "accountType";
|
||||
public static final String ACCOUNT_ID = "accountId";
|
||||
public static final String ORIGINAL_SERIAL_NUMBER = "originalSerialNumber";
|
||||
public static final String PRESENTING_BRANCH_CODE = "presentingBranchCode";
|
||||
public static final String CTS_REFERENCE_NUMBER = "ctsReferenceNumber";
|
||||
public static final String DEPOSIT_DATE = "depositDate";
|
||||
|
||||
// cheque image + IQA (image quality assessment) params, per item
|
||||
public static final String FRONT_IMAGE_REF = "frontImageRef";
|
||||
public static final String BACK_IMAGE_REF = "backImageRef";
|
||||
public static final String IQA_STATUS = "iqaStatus";
|
||||
public static final String IQA_DETAIL = "iqaDetail";
|
||||
|
||||
// item decision params
|
||||
public static final String NOTE = "note";
|
||||
public static final String RETURN_REASON_CODE = "returnReasonCode";
|
||||
public static final String REASON = "reason";
|
||||
|
||||
// outward create params
|
||||
public static final String DEPOSITING_ACCOUNT_TYPE = "depositingAccountType";
|
||||
public static final String DEPOSITING_ACCOUNT_ID = "depositingAccountId";
|
||||
public static final String DRAWER_BANK_MICR = "drawerBankMicr";
|
||||
public static final String DRAWER_IFSC = "drawerIfsc";
|
||||
public static final String DRAWER_BANK_NAME = "drawerBankName";
|
||||
public static final String DEPOSITED_DATE = "depositedDate";
|
||||
|
||||
// outward capture params (stop-payment + image/IQA — captured at deposit time, not hardcoded)
|
||||
public static final String STOP_PAYMENT_FLAG = "stopPaymentFlag";
|
||||
public static final String DRAWER_ACCOUNT_NUMBER = "drawerAccountNumber";
|
||||
public static final String GRID_ZONE_CODE = "gridZoneCode";
|
||||
public static final String GRID_ZONE_NAME = "gridZoneName";
|
||||
|
||||
// outward action params
|
||||
public static final String DISHONOUR_REASON_CODE = "dishonourReasonCode";
|
||||
|
||||
// positive pay params
|
||||
public static final String RESOURCE_POSITIVE_PAY = "positivepayregistration";
|
||||
public static final String CHANNEL = "channel";
|
||||
|
||||
// common
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.api;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
|
||||
/** Small helpers shared by the inward-item and outward-presentment image upload/serve endpoints. */
|
||||
public final class CtsImageUtil {
|
||||
|
||||
private CtsImageUtil() {}
|
||||
|
||||
public static void validateSide(final String side) {
|
||||
if (!"front".equalsIgnoreCase(side) && !"back".equalsIgnoreCase(side)) {
|
||||
throw new UnrecognizedQueryParamException("side", side);
|
||||
}
|
||||
}
|
||||
|
||||
public static String extractExt(final String filename) {
|
||||
if (filename == null) {
|
||||
return ".png";
|
||||
}
|
||||
final int dot = filename.lastIndexOf('.');
|
||||
return dot >= 0 ? filename.substring(dot).toLowerCase() : ".png";
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.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.time.LocalDate;
|
||||
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.ctsclearing.data.InwardClearingBatchData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.InwardClearingItemData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/ctsclearing/inward-batches")
|
||||
@Component
|
||||
@Tag(name = "CTS Inward Clearing Batches", description = "Received (simulated) inward clearing sessions of cheques drawn on this bank, "
|
||||
+ "their validation and pass/return decisioning.")
|
||||
@RequiredArgsConstructor
|
||||
public class InwardClearingBatchApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CtsClearingReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List inward clearing batches")
|
||||
public Page<InwardClearingBatchData> retrieveAll(@QueryParam("status") final String status,
|
||||
@QueryParam("fromDate") @Parameter(description = "Session date lower bound (yyyy-MM-dd)") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "Session date upper bound (yyyy-MM-dd)") final String toDate,
|
||||
@QueryParam("batchReference") final String batchReference,
|
||||
@QueryParam("presentingBankName") final String presentingBankName,
|
||||
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_BATCH);
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.readPlatformService.retrieveAllBatches(status, fromDate != null ? LocalDate.parse(fromDate) : null,
|
||||
toDate != null ? LocalDate.parse(toDate) : null, batchReference, presentingBankName, offset, safeLimit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an inward clearing batch")
|
||||
public InwardClearingBatchData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_BATCH);
|
||||
return this.readPlatformService.retrieveBatch(id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}/items")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List the items of an inward clearing batch")
|
||||
public List<InwardClearingItemData> retrieveItems(@PathParam("id") @Parameter(description = "id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_BATCH);
|
||||
return this.readPlatformService.retrieveItemsByBatch(id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Receive an inward clearing batch")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInwardClearingBatch().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 an inward clearing batch", description = "Supported commands: validate.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("command") @Parameter(description = "validate") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
final CommandWrapper request;
|
||||
if ("validate".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.validateInwardClearingBatch(id).build();
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.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.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 jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
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.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.InwardClearingItemData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingReadPlatformService;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/ctsclearing/inward-items")
|
||||
@Component
|
||||
@Tag(name = "CTS Inward Clearing Items", description = "Individual cheques within an inward clearing batch and their pass/return decision.")
|
||||
@RequiredArgsConstructor
|
||||
public class InwardClearingItemApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CtsClearingReadPlatformService readPlatformService;
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final ContentStoreService contentStoreService;
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an inward clearing item")
|
||||
public InwardClearingItemData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_ITEM);
|
||||
return this.readPlatformService.retrieveItem(id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{id}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Run a command on an inward clearing item", description = "Supported commands: pass, return.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("command") @Parameter(description = "pass | return") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
final CommandWrapper request;
|
||||
if ("pass".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.passInwardClearingItem(id).build();
|
||||
} else if ("return".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.returnInwardClearingItem(id).build();
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a real scanned cheque image (front or back) for this item. Stored via {@code ContentStoreService}
|
||||
* (the same filesystem/S3 abstraction already used elsewhere in this fork, e.g. election candidate
|
||||
* symbols) — only the resulting "mimeType|storagePath" is kept on the item, never the raw bytes in the DB.
|
||||
*/
|
||||
@POST
|
||||
@Path("{id}/image")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Upload the front or back cheque image for an inward item")
|
||||
public Response uploadImage(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("side") @Parameter(description = "front | back") final String side,
|
||||
@FormDataParam("file") final InputStream fileStream, @FormDataParam("file") final FormDataContentDisposition fileDetail,
|
||||
@FormDataParam("file") final FormDataBodyPart filePart) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_ITEM);
|
||||
CtsImageUtil.validateSide(side);
|
||||
final String mimeType = filePart != null && filePart.getMediaType() != null ? filePart.getMediaType().toString() : "image/png";
|
||||
final String ext = CtsImageUtil.extractExt(fileDetail != null ? fileDetail.getFileName() : null);
|
||||
final String storagePath = "images" + this.contentStoreService.getDelimiter() + "ctsclearing-inward"
|
||||
+ this.contentStoreService.getDelimiter() + id + this.contentStoreService.getDelimiter() + side.toLowerCase() + ext;
|
||||
final String savedPath = this.contentStoreService.upload(storagePath, fileStream, mimeType);
|
||||
this.writePlatformService.recordInwardItemImage(id, side, mimeType + "|" + savedPath);
|
||||
return Response.ok(Map.of("resourceId", id)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the front/back cheque image as a base64 data URL ({@code data:<mimeType>;base64,<bytes>}), ready
|
||||
* to bind directly to an <code><img></code> tag once fetched through an authenticated HTTP call —
|
||||
* NOT meant to be used as a raw <code><img src></code> URL (the API requires auth headers a plain
|
||||
* image tag can't send).
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/image")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(summary = "Retrieve the front or back cheque image for an inward item as a base64 data URL")
|
||||
public Response getImage(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("side") @Parameter(description = "front | back") final String side) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_INWARD_ITEM);
|
||||
CtsImageUtil.validateSide(side);
|
||||
final String store = this.readPlatformService.retrieveInwardItemImageStore(id, side);
|
||||
if (store == null || store.isBlank() || !store.contains("|")) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
final String[] parts = store.split("\\|", 2);
|
||||
try {
|
||||
final byte[] bytes = this.contentStoreService.download(parts[1]).readAllBytes();
|
||||
final String dataUrl = "data:" + parts[0] + ";base64," + Base64.getEncoder().encodeToString(bytes);
|
||||
return Response.ok(dataUrl).build();
|
||||
} catch (final Exception e) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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 jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
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.contentstore.service.ContentStoreService;
|
||||
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.ctsclearing.data.OutwardClearingBatchSummaryData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.OutwardClearingPresentmentData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingReadPlatformService;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/ctsclearing/outward-presentments")
|
||||
@Component
|
||||
@Tag(name = "CTS Outward Clearing Presentments", description = "Cheques our customers deposited, drawn on other banks, captured for onward "
|
||||
+ "presentation to the clearing house.")
|
||||
@RequiredArgsConstructor
|
||||
public class OutwardClearingPresentmentApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CtsClearingReadPlatformService readPlatformService;
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final ContentStoreService contentStoreService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List outward clearing presentments")
|
||||
public Page<OutwardClearingPresentmentData> retrieveAll(@QueryParam("status") final String status,
|
||||
@QueryParam("fromDate") @Parameter(description = "Deposited-date lower bound (yyyy-MM-dd)") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "Deposited-date upper bound (yyyy-MM-dd)") final String toDate,
|
||||
@QueryParam("chequeNumber") final String chequeNumber, @QueryParam("drawerBankName") final String drawerBankName,
|
||||
@QueryParam("payeeName") final String payeeName, @QueryParam("batchReference") final String batchReference,
|
||||
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_OUTWARD_PRESENTMENT);
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.readPlatformService.retrieveAllOutward(status, fromDate != null ? LocalDate.parse(fromDate) : null,
|
||||
toDate != null ? LocalDate.parse(toDate) : null, chequeNumber, drawerBankName, payeeName, batchReference, offset,
|
||||
safeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* A "batch" is every outward presentment that shares one batchReference — the real clearing-session
|
||||
* grouping produced either by the 2-hourly scheduled job or the manual "Send Pending Batch" fallback.
|
||||
*/
|
||||
@GET
|
||||
@Path("batches")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List outward clearing sessions (batches)")
|
||||
public Page<OutwardClearingBatchSummaryData> retrieveBatches(
|
||||
@QueryParam("fromDate") @Parameter(description = "Sessions sent on/after this date (yyyy-MM-dd)") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "Sessions sent on/before this date (yyyy-MM-dd)") final String toDate,
|
||||
@QueryParam("batchReference") final String batchReference, @QueryParam("offset") @DefaultValue("0") final Integer offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_OUTWARD_PRESENTMENT);
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.readPlatformService.retrieveOutwardBatches(fromDate != null ? LocalDate.parse(fromDate) : null,
|
||||
toDate != null ? LocalDate.parse(toDate) : null, batchReference, offset, safeLimit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an outward clearing presentment")
|
||||
public OutwardClearingPresentmentData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_OUTWARD_PRESENTMENT);
|
||||
return this.readPlatformService.retrieveOutward(id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Capture an outward clearing presentment")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createOutwardClearingPresentment().withJson(apiRequestBodyAsJson)
|
||||
.build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("send-pending-batch")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Send every pending (CAPTURED) outward presentment to NPCI as one combined CTS batch file",
|
||||
description = "Bundles all currently CAPTURED cheques into a single batchHeader/instruments/batchFooter file, "
|
||||
+ "signs and encrypts it once, and marks every included cheque SENT. Mirrors what the (disabled by "
|
||||
+ "default) clearing-session scheduled job would do; safe to call with nothing pending (no-op).")
|
||||
public CommandProcessingResult sendPendingBatch(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().sendPendingOutwardClearingBatch().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 an outward clearing presentment", description = "Supported commands: send, settle, dishonour.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("command") @Parameter(description = "send | settle | dishonour") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
final CommandWrapper request;
|
||||
if ("send".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.sendOutwardClearingPresentment(id).build();
|
||||
} else if ("settle".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.settleOutwardClearingPresentment(id).build();
|
||||
} else if ("dishonour".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.dishonourOutwardClearingPresentment(id).build();
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a real scanned cheque image (front or back) for this presentment — stored via
|
||||
* {@code ContentStoreService}, same convention as {@link InwardClearingItemApiResource#uploadImage}. Once
|
||||
* uploaded, the image is embedded (base64) in the CTS file sent to NPCI when this cheque is sent.
|
||||
*/
|
||||
@POST
|
||||
@Path("{id}/image")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Upload the front or back cheque image for an outward presentment")
|
||||
public Response uploadImage(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("side") @Parameter(description = "front | back") final String side,
|
||||
@FormDataParam("file") final InputStream fileStream, @FormDataParam("file") final FormDataContentDisposition fileDetail,
|
||||
@FormDataParam("file") final FormDataBodyPart filePart) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_OUTWARD_PRESENTMENT);
|
||||
CtsImageUtil.validateSide(side);
|
||||
final String mimeType = filePart != null && filePart.getMediaType() != null ? filePart.getMediaType().toString() : "image/png";
|
||||
final String ext = CtsImageUtil.extractExt(fileDetail != null ? fileDetail.getFileName() : null);
|
||||
final String storagePath = "images" + this.contentStoreService.getDelimiter() + "ctsclearing-outward"
|
||||
+ this.contentStoreService.getDelimiter() + id + this.contentStoreService.getDelimiter() + side.toLowerCase() + ext;
|
||||
final String savedPath = this.contentStoreService.upload(storagePath, fileStream, mimeType);
|
||||
this.writePlatformService.recordOutwardPresentmentImage(id, side, mimeType + "|" + savedPath);
|
||||
return Response.ok(Map.of("resourceId", id)).build();
|
||||
}
|
||||
|
||||
/** Returns the front/back cheque image as a base64 data URL — see {@link InwardClearingItemApiResource#getImage}. */
|
||||
@GET
|
||||
@Path("{id}/image")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(summary = "Retrieve the front or back cheque image for an outward presentment as a base64 data URL")
|
||||
public Response getImage(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("side") @Parameter(description = "front | back") final String side) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_OUTWARD_PRESENTMENT);
|
||||
CtsImageUtil.validateSide(side);
|
||||
final String store = this.readPlatformService.retrieveOutwardPresentmentImageStore(id, side);
|
||||
if (store == null || store.isBlank() || !store.contains("|")) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
final String[] parts = store.split("\\|", 2);
|
||||
try {
|
||||
final byte[] bytes = this.contentStoreService.download(parts[1]).readAllBytes();
|
||||
final String dataUrl = "data:" + parts[0] + ";base64," + Base64.getEncoder().encodeToString(bytes);
|
||||
return Response.ok(dataUrl).build();
|
||||
} catch (final Exception e) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.data.PositivePayRegistrationData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/ctsclearing/positive-pay")
|
||||
@Component
|
||||
@Tag(name = "Positive Pay (RBI PPS)", description = "Pre-registration of high-value cheque particulars; inward clearing matches a "
|
||||
+ "presented high-value cheque against an active registration.")
|
||||
@RequiredArgsConstructor
|
||||
public class PositivePayApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CtsClearingReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Positive Pay registrations")
|
||||
public Page<PositivePayRegistrationData> retrieveAll(@QueryParam("accountId") final Long accountId,
|
||||
@QueryParam("status") final String status, @QueryParam("offset") @DefaultValue("0") final Integer offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_POSITIVE_PAY);
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.readPlatformService.retrievePositivePayRegistrations(accountId, status, offset, safeLimit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a Positive Pay registration")
|
||||
public PositivePayRegistrationData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(CtsClearingApiConstants.ENTITY_POSITIVE_PAY);
|
||||
return this.readPlatformService.retrievePositivePayRegistration(id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Register a high-value cheque under Positive Pay")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createPositivePayRegistration().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 Positive Pay registration", description = "Supported commands: cancel.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("command") @Parameter(description = "cancel") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
final CommandWrapper request;
|
||||
if ("cancel".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.cancelPositivePayRegistration(id).build();
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+53
@@ -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.ctsclearing.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 InwardClearingBatchData {
|
||||
|
||||
private final Long id;
|
||||
private final String batchReference;
|
||||
private final LocalDate sessionDate;
|
||||
private final String presentmentType;
|
||||
private final String sourceFile;
|
||||
private final Integer totalItemCount;
|
||||
private final BigDecimal totalAmount;
|
||||
private final String status;
|
||||
private final Long officeId;
|
||||
private final String officeName;
|
||||
private final Long receivedBy;
|
||||
private final OffsetDateTime receivedOn;
|
||||
private final OffsetDateTime validatedOn;
|
||||
private final OffsetDateTime decisionedOn;
|
||||
private final OffsetDateTime settledOn;
|
||||
private final OffsetDateTime createdOn;
|
||||
|
||||
private final String presentingBankCode;
|
||||
private final String presentingBankName;
|
||||
private final LocalDate returnTatDeadline;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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 InwardClearingItemData {
|
||||
|
||||
private final Long id;
|
||||
private final Long batchId;
|
||||
private final Long chequeLeafId;
|
||||
private final String accountType;
|
||||
private final Long accountId;
|
||||
private final String accountNo;
|
||||
private final String clientName;
|
||||
private final String chequeNumber;
|
||||
private final String micrCode;
|
||||
private final String ifsc;
|
||||
private final String payeeName;
|
||||
private final BigDecimal amount;
|
||||
private final String amountInWords;
|
||||
private final LocalDate instrumentDate;
|
||||
private final LocalDate presentedDate;
|
||||
private final String frontImageRef;
|
||||
private final String backImageRef;
|
||||
private final String iqaStatus;
|
||||
private final String iqaDetail;
|
||||
private final Boolean accountCheckPassed;
|
||||
private final Boolean balanceCheckPassed;
|
||||
private final Boolean signatureOk;
|
||||
private final Boolean duplicateCheckPassed;
|
||||
private final Boolean stopPaymentCheckPassed;
|
||||
private final Boolean amountWordsFiguresMatch;
|
||||
private final Boolean staleOrPostdated;
|
||||
private final Boolean positivePayCheck;
|
||||
private final String validationDetail;
|
||||
private final String decision;
|
||||
private final String returnReasonCode;
|
||||
private final String decisionReason;
|
||||
private final Long decisionedBy;
|
||||
private final OffsetDateTime decisionedOn;
|
||||
private final String itemStatus;
|
||||
private final Boolean holdPlaced;
|
||||
private final BigDecimal holdAmount;
|
||||
private final Long settlementBatchId;
|
||||
private final String originalSerialNumber;
|
||||
private final String presentingBranchCode;
|
||||
private final LocalDate returnDeadline;
|
||||
private final String ctsReferenceNumber;
|
||||
private final Boolean instrumentStopPaymentFlag;
|
||||
private final LocalDate depositDate;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* One row per outward clearing session ("batch") — every {@code OutwardClearingPresentment} that shares one
|
||||
* {@code batchReference}, grouped at read time. There is no dedicated batch table: the session boundary is
|
||||
* whichever cheques were still CAPTURED the moment the 2-hourly clearing job (or the manual "Send Pending
|
||||
* Batch" fallback) fired.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class OutwardClearingBatchSummaryData {
|
||||
|
||||
private final String batchReference;
|
||||
private final OffsetDateTime sessionStartedOn;
|
||||
private final OffsetDateTime sessionEndedOn;
|
||||
private final Integer itemCount;
|
||||
private final BigDecimal totalAmount;
|
||||
private final Integer sentCount;
|
||||
private final Integer settledCount;
|
||||
private final Integer dishonouredCount;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.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 OutwardClearingPresentmentData {
|
||||
|
||||
private final Long id;
|
||||
private final String depositingAccountType;
|
||||
private final Long depositingAccountId;
|
||||
private final String accountNo;
|
||||
private final String clientName;
|
||||
private final String drawerBankMicr;
|
||||
private final String drawerIfsc;
|
||||
private final String drawerBankName;
|
||||
private final String chequeNumber;
|
||||
private final BigDecimal amount;
|
||||
private final String payeeName;
|
||||
private final LocalDate depositedDate;
|
||||
private final LocalDate instrumentDate;
|
||||
private final Boolean stopPaymentFlag;
|
||||
private final String frontImageRef;
|
||||
private final String backImageRef;
|
||||
private final String iqaStatus;
|
||||
private final String iqaDetail;
|
||||
private final String drawerAccountNumber;
|
||||
private final String gridZoneCode;
|
||||
private final String gridZoneName;
|
||||
private final String status;
|
||||
private final String batchReference;
|
||||
private final String ctsFileJson;
|
||||
private final String npciResponseJson;
|
||||
private final OffsetDateTime sentOn;
|
||||
private final String dishonourReasonCode;
|
||||
private final Long officeId;
|
||||
private final OffsetDateTime settledOn;
|
||||
private final Long createdBy;
|
||||
private final OffsetDateTime createdOn;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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 PositivePayRegistrationData {
|
||||
|
||||
private final Long id;
|
||||
private final String accountType;
|
||||
private final Long accountId;
|
||||
private final String accountNo;
|
||||
private final String clientName;
|
||||
private final String chequeNumber;
|
||||
private final LocalDate instrumentDate;
|
||||
private final BigDecimal amount;
|
||||
private final String payeeName;
|
||||
private final String channel;
|
||||
private final String status;
|
||||
private final Long registeredBy;
|
||||
private final OffsetDateTime registeredOn;
|
||||
private final String cancelledReason;
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
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 received (simulated) inward clearing session — a set of cheques drawn on THIS bank presented by the
|
||||
* clearing house. {@code officeId} is a plain column rather than a JPA relation, mirroring
|
||||
* {@code ChequeBook.accountId}'s "plain column to avoid a cross-module entity dependency" precedent.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_inward_clearing_batch")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class InwardClearingBatch extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "batch_reference", nullable = false, length = 100)
|
||||
private String batchReference;
|
||||
|
||||
@Column(name = "session_date", nullable = false)
|
||||
private LocalDate sessionDate;
|
||||
|
||||
@Column(name = "presentment_type", nullable = false, length = 10)
|
||||
private String presentmentType;
|
||||
|
||||
@Column(name = "source_file", length = 200)
|
||||
private String sourceFile;
|
||||
|
||||
/** Who presented this batch to us, as captured on receipt — optional, since we cannot verify it ourselves. */
|
||||
@Column(name = "presenting_bank_code", length = 15)
|
||||
private String presentingBankCode;
|
||||
|
||||
@Column(name = "presenting_bank_name", length = 150)
|
||||
private String presentingBankName;
|
||||
|
||||
/** RBI CTS return TAT (T+1) for every item in this batch, unless a specific item overrides it. */
|
||||
@Column(name = "return_tat_deadline")
|
||||
private LocalDate returnTatDeadline;
|
||||
|
||||
@Column(name = "total_item_count", nullable = false)
|
||||
private Integer totalItemCount;
|
||||
|
||||
@Column(name = "total_amount", nullable = false)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 15)
|
||||
private String status;
|
||||
|
||||
@Column(name = "office_id")
|
||||
private Long officeId;
|
||||
|
||||
@Column(name = "received_by")
|
||||
private Long receivedBy;
|
||||
|
||||
@Column(name = "received_on")
|
||||
private OffsetDateTime receivedOn;
|
||||
|
||||
@Column(name = "validated_on")
|
||||
private OffsetDateTime validatedOn;
|
||||
|
||||
@Column(name = "decisioned_on")
|
||||
private OffsetDateTime decisionedOn;
|
||||
|
||||
@Column(name = "settled_on")
|
||||
private OffsetDateTime settledOn;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
@Version
|
||||
@Column(name = "version")
|
||||
private Integer version;
|
||||
|
||||
public static InwardClearingBatch receive(final String batchReference, final LocalDate sessionDate, final String presentmentType,
|
||||
final String sourceFile, final String presentingBankCode, final String presentingBankName, final Long officeId,
|
||||
final Long receivedBy) {
|
||||
final InwardClearingBatch batch = new InwardClearingBatch();
|
||||
batch.batchReference = batchReference;
|
||||
batch.sessionDate = sessionDate;
|
||||
batch.presentmentType = presentmentType == null ? "MICR" : presentmentType;
|
||||
batch.sourceFile = sourceFile;
|
||||
batch.presentingBankCode = presentingBankCode;
|
||||
batch.presentingBankName = presentingBankName;
|
||||
batch.officeId = officeId;
|
||||
batch.receivedBy = receivedBy;
|
||||
batch.totalItemCount = 0;
|
||||
batch.totalAmount = BigDecimal.ZERO;
|
||||
batch.status = InwardClearingBatchStatus.RECEIVED.name();
|
||||
batch.receivedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
batch.createdOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
// RBI CTS return TAT: T+1 from the clearing session date
|
||||
batch.returnTatDeadline = sessionDate == null ? null : sessionDate.plusDays(1);
|
||||
return batch;
|
||||
}
|
||||
|
||||
/** Set once the items have been inserted, so the batch summary matches the sum of its items. */
|
||||
public void setTotals(final int totalItemCount, final BigDecimal totalAmount) {
|
||||
this.totalItemCount = totalItemCount;
|
||||
this.totalAmount = totalAmount == null ? BigDecimal.ZERO : totalAmount;
|
||||
}
|
||||
|
||||
public boolean isReceived() {
|
||||
return InwardClearingBatchStatus.RECEIVED.name().equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isValidated() {
|
||||
return InwardClearingBatchStatus.VALIDATED.name().equals(this.status);
|
||||
}
|
||||
|
||||
public void markValidated() {
|
||||
this.status = InwardClearingBatchStatus.VALIDATED.name();
|
||||
this.validatedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public void markDecisioned() {
|
||||
this.status = InwardClearingBatchStatus.DECISIONED.name();
|
||||
this.decisionedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public void markSettled() {
|
||||
if (!InwardClearingBatchStatus.DECISIONED.name().equals(this.status)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.batch.not.decisioned",
|
||||
"Inward clearing batch " + getId() + " is not in DECISIONED status.", getId());
|
||||
}
|
||||
this.status = InwardClearingBatchStatus.SETTLED.name();
|
||||
this.settledOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InwardClearingBatchRepository extends JpaRepository<InwardClearingBatch, Long> {
|
||||
|
||||
boolean existsByBatchReferenceIgnoreCase(String batchReference);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.InwardClearingBatchNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InwardClearingBatchRepositoryWrapper {
|
||||
|
||||
private final InwardClearingBatchRepository repository;
|
||||
|
||||
public InwardClearingBatch findOneWithNotFoundDetection(final Long id) {
|
||||
return this.repository.findById(id).orElseThrow(() -> new InwardClearingBatchNotFoundException(id));
|
||||
}
|
||||
|
||||
public InwardClearingBatch saveAndFlush(final InwardClearingBatch batch) {
|
||||
return this.repository.saveAndFlush(batch);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
/**
|
||||
* Lifecycle of an inward clearing batch: RECEIVED (items captured) -> VALIDATED (rules run, holds placed)
|
||||
* -> DECISIONED (every item passed or returned) -> SETTLED (netting done — out of scope of this module,
|
||||
* kept for completeness).
|
||||
*/
|
||||
public enum InwardClearingBatchStatus {
|
||||
|
||||
RECEIVED, //
|
||||
VALIDATED, //
|
||||
DECISIONED, //
|
||||
SETTLED; //
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.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.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;
|
||||
|
||||
/**
|
||||
* One cheque within an inward clearing batch, carrying its captured MICR data, the result of each validation
|
||||
* rule and the eventual pass/return decision. {@code chequeLeafId} and {@code accountId} are plain columns
|
||||
* (no JPA relation) — the account may be a {@code SavingsAccount} or a {@code CurrentAccount}, and the leaf
|
||||
* lives in the {@code chequebook} module — mirroring {@code ChequeBook}'s plain-column precedent.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_inward_clearing_item")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class InwardClearingItem extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "batch_id", nullable = false)
|
||||
private InwardClearingBatch batch;
|
||||
|
||||
@Column(name = "cheque_leaf_id")
|
||||
private Long chequeLeafId;
|
||||
|
||||
@Column(name = "account_type", length = 10)
|
||||
private String accountType;
|
||||
|
||||
@Column(name = "account_id")
|
||||
private Long accountId;
|
||||
|
||||
@Column(name = "cheque_number", nullable = false, length = 25)
|
||||
private String chequeNumber;
|
||||
|
||||
@Column(name = "micr_code", length = 20)
|
||||
private String micrCode;
|
||||
|
||||
@Column(name = "ifsc", length = 15)
|
||||
private String ifsc;
|
||||
|
||||
@Column(name = "payee_name", length = 150)
|
||||
private String payeeName;
|
||||
|
||||
@Column(name = "amount", nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "amount_in_words", length = 300)
|
||||
private String amountInWords;
|
||||
|
||||
@Column(name = "instrument_date")
|
||||
private LocalDate instrumentDate;
|
||||
|
||||
@Column(name = "presented_date")
|
||||
private LocalDate presentedDate;
|
||||
|
||||
/** This cheque's serial number within Bank A's original outward batch, if supplied on receipt. */
|
||||
@Column(name = "original_serial_number", length = 20)
|
||||
private String originalSerialNumber;
|
||||
|
||||
/** The branch of the presenting (collecting) bank that originally deposited this cheque, if supplied. */
|
||||
@Column(name = "presenting_branch_code", length = 20)
|
||||
private String presentingBranchCode;
|
||||
|
||||
/** RBI CTS return TAT (T+1) — the deadline by which a RETURN decision must be dispatched. */
|
||||
@Column(name = "return_deadline")
|
||||
private LocalDate returnDeadline;
|
||||
|
||||
/** The sending bank's own CTS-2010 reference number for this instrument, if supplied on receipt. */
|
||||
@Column(name = "cts_reference_number", length = 50)
|
||||
private String ctsReferenceNumber;
|
||||
|
||||
/** Stop-payment flag as captured on the DEPOSITING side, if supplied — descriptive only; the authoritative
|
||||
* check is {@code stopPaymentCheckPassed}, resolved against this bank's own {@code ChequeLeaf}. */
|
||||
@Column(name = "instrument_stop_payment_flag")
|
||||
private Boolean instrumentStopPaymentFlag;
|
||||
|
||||
/** The date the depositing bank's customer actually deposited this cheque, if supplied on receipt. */
|
||||
@Column(name = "deposit_date")
|
||||
private LocalDate depositDate;
|
||||
|
||||
@Column(name = "front_image_ref", length = 300)
|
||||
private String frontImageRef;
|
||||
|
||||
@Column(name = "back_image_ref", length = 300)
|
||||
private String backImageRef;
|
||||
|
||||
/** Real uploaded image, as "mimeType|storagePath" (via ContentStoreService) — null until a file is uploaded. */
|
||||
@Column(name = "front_image_store", length = 500)
|
||||
private String frontImageStore;
|
||||
|
||||
@Column(name = "back_image_store", length = 500)
|
||||
private String backImageStore;
|
||||
|
||||
/** Image Quality Assessment outcome: PASS / FAIL / PENDING / NA. */
|
||||
@Column(name = "iqa_status", length = 10)
|
||||
private String iqaStatus;
|
||||
|
||||
@Column(name = "iqa_detail", length = 500)
|
||||
private String iqaDetail;
|
||||
|
||||
@Column(name = "account_check_passed")
|
||||
private Boolean accountCheckPassed;
|
||||
|
||||
@Column(name = "balance_check_passed")
|
||||
private Boolean balanceCheckPassed;
|
||||
|
||||
@Column(name = "signature_ok")
|
||||
private Boolean signatureOk;
|
||||
|
||||
@Column(name = "duplicate_check_passed")
|
||||
private Boolean duplicateCheckPassed;
|
||||
|
||||
@Column(name = "stop_payment_check_passed")
|
||||
private Boolean stopPaymentCheckPassed;
|
||||
|
||||
@Column(name = "amount_words_figures_match")
|
||||
private Boolean amountWordsFiguresMatch;
|
||||
|
||||
@Column(name = "stale_or_postdated")
|
||||
private Boolean staleOrPostdated;
|
||||
|
||||
/** Positive Pay match result for a high-value cheque: TRUE match, FALSE mismatch/unregistered, null = not applicable. */
|
||||
@Column(name = "positive_pay_check")
|
||||
private Boolean positivePayCheck;
|
||||
|
||||
@Column(name = "validation_detail", length = 1000)
|
||||
private String validationDetail;
|
||||
|
||||
@Column(name = "decision", nullable = false, length = 10)
|
||||
private String decision;
|
||||
|
||||
@Column(name = "return_reason_code", length = 10)
|
||||
private String returnReasonCode;
|
||||
|
||||
@Column(name = "decision_reason", length = 500)
|
||||
private String decisionReason;
|
||||
|
||||
@Column(name = "decisioned_by")
|
||||
private Long decisionedBy;
|
||||
|
||||
@Column(name = "decisioned_on")
|
||||
private OffsetDateTime decisionedOn;
|
||||
|
||||
@Column(name = "item_status", nullable = false, length = 15)
|
||||
private String itemStatus;
|
||||
|
||||
@Column(name = "hold_placed", nullable = false)
|
||||
private boolean holdPlaced;
|
||||
|
||||
@Column(name = "hold_amount")
|
||||
private BigDecimal holdAmount;
|
||||
|
||||
@Column(name = "settlement_batch_id")
|
||||
private Long settlementBatchId;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
@Version
|
||||
@Column(name = "version")
|
||||
private Integer version;
|
||||
|
||||
public static InwardClearingItem receive(final InwardClearingBatch batch, final Long chequeLeafId, final String accountType,
|
||||
final Long accountId, final String chequeNumber, final String micrCode, final String ifsc, final String payeeName,
|
||||
final BigDecimal amount, final String amountInWords, final LocalDate instrumentDate, final LocalDate presentedDate) {
|
||||
final InwardClearingItem item = new InwardClearingItem();
|
||||
item.batch = batch;
|
||||
item.chequeLeafId = chequeLeafId;
|
||||
item.accountType = accountType;
|
||||
item.accountId = accountId;
|
||||
item.chequeNumber = chequeNumber;
|
||||
item.micrCode = micrCode;
|
||||
item.ifsc = ifsc;
|
||||
item.payeeName = payeeName;
|
||||
item.amount = amount == null ? BigDecimal.ZERO : amount;
|
||||
item.amountInWords = amountInWords;
|
||||
item.instrumentDate = instrumentDate;
|
||||
item.presentedDate = presentedDate;
|
||||
item.decision = InwardClearingItemDecision.PENDING.name();
|
||||
item.itemStatus = InwardClearingItemStatus.RECEIVED.name();
|
||||
item.holdPlaced = false;
|
||||
item.iqaStatus = "NA";
|
||||
item.createdOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return item;
|
||||
}
|
||||
|
||||
/** Records the source-bank traceability fields NPCI's inward package carries for this item, if supplied. */
|
||||
public void recordInwardMeta(final String originalSerialNumber, final String presentingBranchCode, final LocalDate returnDeadline) {
|
||||
this.originalSerialNumber = originalSerialNumber;
|
||||
this.presentingBranchCode = presentingBranchCode;
|
||||
this.returnDeadline = returnDeadline;
|
||||
}
|
||||
|
||||
/** Records additional descriptive fields carried on the instrument from the depositing/sending side, if supplied. */
|
||||
public void recordSourceDetails(final String ctsReferenceNumber, final Boolean instrumentStopPaymentFlag,
|
||||
final LocalDate depositDate) {
|
||||
this.ctsReferenceNumber = ctsReferenceNumber;
|
||||
this.instrumentStopPaymentFlag = instrumentStopPaymentFlag;
|
||||
this.depositDate = depositDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the scanned front/back image references and the image-quality (IQA) outcome received with the
|
||||
* cheque (spec section 5). {@code iqaStatus} defaults to NA when the caller supplies no image.
|
||||
*/
|
||||
public void captureImages(final String frontImageRef, final String backImageRef, final String iqaStatus, final String iqaDetail) {
|
||||
this.frontImageRef = frontImageRef;
|
||||
this.backImageRef = backImageRef;
|
||||
this.iqaStatus = iqaStatus == null || iqaStatus.isBlank() ? "NA" : iqaStatus.trim().toUpperCase();
|
||||
this.iqaDetail = iqaDetail;
|
||||
}
|
||||
|
||||
/** True when the cheque image failed the quality assessment — a valid ground for a technical return (code 11). */
|
||||
public boolean isImageQualityFailed() {
|
||||
return "FAIL".equalsIgnoreCase(this.iqaStatus);
|
||||
}
|
||||
|
||||
/** Records where a real uploaded front/back image lives, as "mimeType|storagePath". */
|
||||
public void recordImageStore(final String side, final String mimeTypeAndPath) {
|
||||
if ("front".equalsIgnoreCase(side)) {
|
||||
this.frontImageStore = mimeTypeAndPath;
|
||||
} else {
|
||||
this.backImageStore = mimeTypeAndPath;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Validation =============================
|
||||
|
||||
/** Records the outcome of the automatic validation rules run by {@code InwardClearingValidationService}. */
|
||||
public void recordValidation(final Boolean accountCheckPassed, final Boolean balanceCheckPassed, final Boolean duplicateCheckPassed,
|
||||
final Boolean stopPaymentCheckPassed, final Boolean amountWordsFiguresMatch, final Boolean staleOrPostdated,
|
||||
final String validationDetail) {
|
||||
this.accountCheckPassed = accountCheckPassed;
|
||||
this.balanceCheckPassed = balanceCheckPassed;
|
||||
this.duplicateCheckPassed = duplicateCheckPassed;
|
||||
this.stopPaymentCheckPassed = stopPaymentCheckPassed;
|
||||
this.amountWordsFiguresMatch = amountWordsFiguresMatch;
|
||||
this.staleOrPostdated = staleOrPostdated;
|
||||
this.validationDetail = validationDetail;
|
||||
}
|
||||
|
||||
/** Records the Positive Pay match outcome (RBI PPS) for a high-value cheque. */
|
||||
public void recordPositivePay(final Boolean positivePayCheck) {
|
||||
this.positivePayCheck = positivePayCheck;
|
||||
}
|
||||
|
||||
public void placeHold(final BigDecimal holdAmount) {
|
||||
this.holdPlaced = true;
|
||||
this.holdAmount = holdAmount;
|
||||
}
|
||||
|
||||
/** Clears the hold flag once the placed hold has been consumed (pass) or released (return). */
|
||||
public void clearHold() {
|
||||
this.holdPlaced = false;
|
||||
}
|
||||
|
||||
public void markValidated() {
|
||||
this.itemStatus = InwardClearingItemStatus.VALIDATED.name();
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return InwardClearingItemDecision.PENDING.name().equals(this.decision);
|
||||
}
|
||||
|
||||
public boolean isValidated() {
|
||||
return InwardClearingItemStatus.VALIDATED.name().equals(this.itemStatus);
|
||||
}
|
||||
|
||||
private void assertDecidable() {
|
||||
if (!isValidated()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.item.not.validated",
|
||||
"Inward clearing item " + getId() + " has not been validated.", getId());
|
||||
}
|
||||
if (!isPending()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.item.already.decided",
|
||||
"Inward clearing item " + getId() + " has already been decided.", getId());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Decision =============================
|
||||
|
||||
public void pass(final String reasonNote, final Long userId) {
|
||||
assertDecidable();
|
||||
this.decision = InwardClearingItemDecision.PASS.name();
|
||||
this.decisionReason = reasonNote;
|
||||
this.decisionedBy = userId;
|
||||
this.decisionedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.itemStatus = InwardClearingItemStatus.PASSED.name();
|
||||
}
|
||||
|
||||
public void returnItem(final String returnReasonCode, final String reason, final Long userId) {
|
||||
assertDecidable();
|
||||
this.decision = InwardClearingItemDecision.RETURN.name();
|
||||
this.returnReasonCode = returnReasonCode;
|
||||
this.decisionReason = reason;
|
||||
this.decisionedBy = userId;
|
||||
this.decisionedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.itemStatus = InwardClearingItemStatus.RETURNED.name();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
/** The back-office decision on a single inward item: PENDING until a user passes or returns it. */
|
||||
public enum InwardClearingItemDecision {
|
||||
|
||||
PENDING, //
|
||||
PASS, //
|
||||
RETURN; //
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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 InwardClearingItemRepository extends JpaRepository<InwardClearingItem, Long> {
|
||||
|
||||
List<InwardClearingItem> findByBatchIdOrderByIdAsc(Long batchId);
|
||||
|
||||
long countByBatchId(Long batchId);
|
||||
|
||||
long countByBatchIdAndDecision(Long batchId, String decision);
|
||||
|
||||
/**
|
||||
* Any OTHER item (in any batch) with the same cheque number + account + amount that was already PASSED —
|
||||
* used by the duplicate-presentment check.
|
||||
*/
|
||||
@Query("select count(i) from InwardClearingItem i where i.chequeNumber = :chequeNumber and i.accountId = :accountId "
|
||||
+ "and i.amount = :amount and i.decision = 'PASS' and i.id <> :itemId")
|
||||
long countPassedDuplicates(@Param("chequeNumber") String chequeNumber, @Param("accountId") Long accountId,
|
||||
@Param("amount") BigDecimal amount, @Param("itemId") Long itemId);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.InwardClearingItemNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InwardClearingItemRepositoryWrapper {
|
||||
|
||||
private final InwardClearingItemRepository repository;
|
||||
|
||||
public InwardClearingItem findOneWithNotFoundDetection(final Long id) {
|
||||
return this.repository.findById(id).orElseThrow(() -> new InwardClearingItemNotFoundException(id));
|
||||
}
|
||||
|
||||
public InwardClearingItem saveAndFlush(final InwardClearingItem item) {
|
||||
return this.repository.saveAndFlush(item);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
/** Per-item lifecycle: RECEIVED -> VALIDATED -> PASSED / RETURNED (-> SETTLED once netted). */
|
||||
public enum InwardClearingItemStatus {
|
||||
|
||||
RECEIVED, //
|
||||
VALIDATED, //
|
||||
PASSED, //
|
||||
RETURNED, //
|
||||
SETTLED; //
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
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 cheque one of our own customers deposited, drawn on ANOTHER bank, captured for onward presentation to the
|
||||
* clearing house. A flat entity with a manually-advanced status. {@code depositingAccountId} is a plain
|
||||
* column (Savings or Current), mirroring {@code ChequeBook}'s plain-column precedent.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_outward_clearing_presentment")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class OutwardClearingPresentment extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "depositing_account_type", nullable = false, length = 10)
|
||||
private String depositingAccountType;
|
||||
|
||||
@Column(name = "depositing_account_id", nullable = false)
|
||||
private Long depositingAccountId;
|
||||
|
||||
@Column(name = "drawer_bank_micr", length = 20)
|
||||
private String drawerBankMicr;
|
||||
|
||||
@Column(name = "drawer_ifsc", length = 15)
|
||||
private String drawerIfsc;
|
||||
|
||||
@Column(name = "drawer_bank_name", length = 150)
|
||||
private String drawerBankName;
|
||||
|
||||
@Column(name = "cheque_number", nullable = false, length = 25)
|
||||
private String chequeNumber;
|
||||
|
||||
@Column(name = "amount", nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "payee_name", length = 150)
|
||||
private String payeeName;
|
||||
|
||||
@Column(name = "deposited_date", nullable = false)
|
||||
private LocalDate depositedDate;
|
||||
|
||||
@Column(name = "instrument_date")
|
||||
private LocalDate instrumentDate;
|
||||
|
||||
/** Captured at deposit time, drives the CTS outward file's stopPaymentFlag instead of a hardcoded false. */
|
||||
@Column(name = "stop_payment_flag", nullable = false)
|
||||
private boolean stopPaymentFlag;
|
||||
|
||||
@Column(name = "front_image_ref", length = 300)
|
||||
private String frontImageRef;
|
||||
|
||||
@Column(name = "back_image_ref", length = 300)
|
||||
private String backImageRef;
|
||||
|
||||
/** Image Quality Assessment outcome: PASS / FAIL / PENDING / NA. */
|
||||
@Column(name = "iqa_status", length = 10)
|
||||
private String iqaStatus;
|
||||
|
||||
@Column(name = "iqa_detail", length = 500)
|
||||
private String iqaDetail;
|
||||
|
||||
/** Real uploaded image, as "mimeType|storagePath" (via ContentStoreService) — null until a file is uploaded. */
|
||||
@Column(name = "front_image_store", length = 500)
|
||||
private String frontImageStore;
|
||||
|
||||
@Column(name = "back_image_store", length = 500)
|
||||
private String backImageStore;
|
||||
|
||||
/** The drawer's account number at the drawee bank (the account being debited) — a distinct field from
|
||||
* {@code depositingAccountId}, which is OUR customer's account that received the deposit. */
|
||||
@Column(name = "drawer_account_number", length = 34)
|
||||
private String drawerAccountNumber;
|
||||
|
||||
@Column(name = "grid_zone_code", length = 10)
|
||||
private String gridZoneCode;
|
||||
|
||||
@Column(name = "grid_zone_name", length = 100)
|
||||
private String gridZoneName;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 25)
|
||||
private String status;
|
||||
|
||||
@Column(name = "batch_reference", length = 100)
|
||||
private String batchReference;
|
||||
|
||||
/** The full, human-readable CTS-2010 outward file (batchHeader + instrument + batchFooter) that was sent. */
|
||||
@Column(name = "cts_file_json", columnDefinition = "TEXT")
|
||||
private String ctsFileJson;
|
||||
|
||||
/** NPCI's real routing response (signatureValid, routedTo, unroutable) for this batch, from the actual NPCI service. */
|
||||
@Column(name = "npci_response_json", columnDefinition = "TEXT")
|
||||
private String npciResponseJson;
|
||||
|
||||
@Column(name = "sent_on")
|
||||
private OffsetDateTime sentOn;
|
||||
|
||||
@Column(name = "dishonour_reason_code", length = 10)
|
||||
private String dishonourReasonCode;
|
||||
|
||||
@Column(name = "office_id")
|
||||
private Long officeId;
|
||||
|
||||
@Column(name = "settled_on")
|
||||
private OffsetDateTime settledOn;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
@Version
|
||||
@Column(name = "version")
|
||||
private Integer version;
|
||||
|
||||
public static OutwardClearingPresentment capture(final String depositingAccountType, final Long depositingAccountId,
|
||||
final String drawerBankMicr, final String drawerIfsc, final String drawerBankName, final String chequeNumber,
|
||||
final BigDecimal amount, final String payeeName, final LocalDate depositedDate, final LocalDate instrumentDate,
|
||||
final Long officeId, final Long createdBy, final Boolean stopPaymentFlag, final String frontImageRef,
|
||||
final String backImageRef, final String iqaStatus, final String iqaDetail, final String drawerAccountNumber,
|
||||
final String gridZoneCode, final String gridZoneName) {
|
||||
final OutwardClearingPresentment presentment = new OutwardClearingPresentment();
|
||||
presentment.depositingAccountType = depositingAccountType;
|
||||
presentment.depositingAccountId = depositingAccountId;
|
||||
presentment.drawerBankMicr = drawerBankMicr;
|
||||
presentment.drawerIfsc = drawerIfsc;
|
||||
presentment.drawerBankName = drawerBankName;
|
||||
presentment.chequeNumber = chequeNumber;
|
||||
presentment.amount = amount == null ? BigDecimal.ZERO : amount;
|
||||
presentment.payeeName = payeeName;
|
||||
presentment.depositedDate = depositedDate;
|
||||
presentment.instrumentDate = instrumentDate;
|
||||
presentment.officeId = officeId;
|
||||
presentment.createdBy = createdBy;
|
||||
presentment.stopPaymentFlag = Boolean.TRUE.equals(stopPaymentFlag);
|
||||
presentment.frontImageRef = frontImageRef;
|
||||
presentment.backImageRef = backImageRef;
|
||||
presentment.iqaStatus = iqaStatus == null || iqaStatus.isBlank() ? "NA" : iqaStatus.trim().toUpperCase();
|
||||
presentment.iqaDetail = iqaDetail;
|
||||
presentment.drawerAccountNumber = drawerAccountNumber;
|
||||
presentment.gridZoneCode = gridZoneCode;
|
||||
presentment.gridZoneName = gridZoneName;
|
||||
presentment.status = OutwardClearingPresentmentStatus.CAPTURED.name();
|
||||
presentment.createdOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return presentment;
|
||||
}
|
||||
|
||||
private void assertStatus(final OutwardClearingPresentmentStatus expected, final String errorCode) {
|
||||
if (!expected.name().equals(this.status)) {
|
||||
throw new GeneralPlatformDomainRuleException(errorCode,
|
||||
"Outward clearing presentment " + getId() + " is not in " + expected.name() + " status.", getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void send(final String batchReference, final String ctsFileJson) {
|
||||
assertStatus(OutwardClearingPresentmentStatus.CAPTURED, "error.msg.outward.clearing.presentment.not.captured");
|
||||
this.status = OutwardClearingPresentmentStatus.SENT_TO_CLEARING_HOUSE.name();
|
||||
this.batchReference = batchReference;
|
||||
this.ctsFileJson = ctsFileJson;
|
||||
this.sentOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
/** Records NPCI's real routing response for the batch this presentment was just sent in. */
|
||||
public void recordNpciResponse(final String npciResponseJson) {
|
||||
this.npciResponseJson = npciResponseJson;
|
||||
}
|
||||
|
||||
public void settle() {
|
||||
assertStatus(OutwardClearingPresentmentStatus.SENT_TO_CLEARING_HOUSE, "error.msg.outward.clearing.presentment.not.sent");
|
||||
this.status = OutwardClearingPresentmentStatus.SETTLED.name();
|
||||
this.settledOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public void dishonour(final String dishonourReasonCode) {
|
||||
assertStatus(OutwardClearingPresentmentStatus.SENT_TO_CLEARING_HOUSE, "error.msg.outward.clearing.presentment.not.sent");
|
||||
this.status = OutwardClearingPresentmentStatus.DISHONOURED.name();
|
||||
this.dishonourReasonCode = dishonourReasonCode;
|
||||
}
|
||||
|
||||
/** Records where a real uploaded front/back image lives, as "mimeType|storagePath". */
|
||||
public void recordImageStore(final String side, final String mimeTypeAndPath) {
|
||||
if ("front".equalsIgnoreCase(side)) {
|
||||
this.frontImageStore = mimeTypeAndPath;
|
||||
} else {
|
||||
this.backImageStore = mimeTypeAndPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface OutwardClearingPresentmentRepository extends JpaRepository<OutwardClearingPresentment, Long> {
|
||||
|
||||
List<OutwardClearingPresentment> findByStatusOrderByIdAsc(String status);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.OutwardClearingPresentmentNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OutwardClearingPresentmentRepositoryWrapper {
|
||||
|
||||
private final OutwardClearingPresentmentRepository repository;
|
||||
|
||||
public OutwardClearingPresentment findOneWithNotFoundDetection(final Long id) {
|
||||
return this.repository.findById(id).orElseThrow(() -> new OutwardClearingPresentmentNotFoundException(id));
|
||||
}
|
||||
|
||||
public OutwardClearingPresentment saveAndFlush(final OutwardClearingPresentment presentment) {
|
||||
return this.repository.saveAndFlush(presentment);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
/**
|
||||
* Manually-advanced lifecycle of an outward presentment (a cheque our customer deposited, drawn on another
|
||||
* bank): CAPTURED -> SENT_TO_CLEARING_HOUSE -> SETTLED, or -> DISHONOURED if the drawer bank bounces it.
|
||||
*/
|
||||
public enum OutwardClearingPresentmentStatus {
|
||||
|
||||
CAPTURED, //
|
||||
SENT_TO_CLEARING_HOUSE, //
|
||||
SETTLED, //
|
||||
DISHONOURED; //
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
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 Positive Pay registration: the drawer pre-declares a high-value cheque's key particulars (number, date,
|
||||
* amount, payee) so inward clearing can confirm the presented cheque matches what the drawer authorised
|
||||
* (RBI Positive Pay System). {@code accountId} is a plain column (savings or current), following
|
||||
* {@code ChequeBook}'s precedent.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_positive_pay_registration")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class PositivePayRegistration extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "account_type", nullable = false, length = 10)
|
||||
private String accountType;
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
private Long accountId;
|
||||
|
||||
@Column(name = "cheque_number", nullable = false, length = 25)
|
||||
private String chequeNumber;
|
||||
|
||||
@Column(name = "instrument_date")
|
||||
private LocalDate instrumentDate;
|
||||
|
||||
@Column(name = "amount", nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "payee_name", length = 150)
|
||||
private String payeeName;
|
||||
|
||||
@Column(name = "channel", length = 20)
|
||||
private String channel;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 15)
|
||||
private String status;
|
||||
|
||||
@Column(name = "registered_by")
|
||||
private Long registeredBy;
|
||||
|
||||
@Column(name = "registered_on")
|
||||
private OffsetDateTime registeredOn;
|
||||
|
||||
@Column(name = "cancelled_reason", length = 500)
|
||||
private String cancelledReason;
|
||||
|
||||
@Column(name = "cancelled_by")
|
||||
private Long cancelledBy;
|
||||
|
||||
@Column(name = "cancelled_on")
|
||||
private OffsetDateTime cancelledOn;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
@Version
|
||||
@Column(name = "version")
|
||||
private Integer version;
|
||||
|
||||
public static PositivePayRegistration register(final String accountType, final Long accountId, final String chequeNumber,
|
||||
final LocalDate instrumentDate, final BigDecimal amount, final String payeeName, final String channel,
|
||||
final Long registeredBy) {
|
||||
final PositivePayRegistration pps = new PositivePayRegistration();
|
||||
pps.accountType = accountType;
|
||||
pps.accountId = accountId;
|
||||
pps.chequeNumber = chequeNumber;
|
||||
pps.instrumentDate = instrumentDate;
|
||||
pps.amount = amount;
|
||||
pps.payeeName = payeeName;
|
||||
pps.channel = channel == null || channel.isBlank() ? "BRANCH" : channel;
|
||||
pps.status = PositivePayStatus.ACTIVE.name();
|
||||
pps.registeredBy = registeredBy;
|
||||
pps.registeredOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
pps.createdOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return pps;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return PositivePayStatus.ACTIVE.name().equals(this.status);
|
||||
}
|
||||
|
||||
public void cancel(final String reason, final Long userId) {
|
||||
if (!isActive()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.positivepay.not.active",
|
||||
"Positive Pay registration " + getId() + " is not ACTIVE.", getId());
|
||||
}
|
||||
this.status = PositivePayStatus.CANCELLED.name();
|
||||
this.cancelledReason = reason;
|
||||
this.cancelledBy = userId;
|
||||
this.cancelledOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
/** Whether the presented cheque's particulars match this registration (amount exact; payee/date if both present). */
|
||||
public boolean matches(final BigDecimal presentedAmount, final LocalDate presentedInstrumentDate, final String presentedPayee) {
|
||||
if (presentedAmount == null || this.amount.compareTo(presentedAmount) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.instrumentDate != null && presentedInstrumentDate != null && !this.instrumentDate.equals(presentedInstrumentDate)) {
|
||||
return false;
|
||||
}
|
||||
if (this.payeeName != null && !this.payeeName.isBlank() && presentedPayee != null && !presentedPayee.isBlank()
|
||||
&& !this.payeeName.trim().equalsIgnoreCase(presentedPayee.trim())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PositivePayRegistrationRepository extends JpaRepository<PositivePayRegistration, Long> {
|
||||
|
||||
List<PositivePayRegistration> findByAccountIdAndChequeNumberAndStatus(Long accountId, String chequeNumber, String status);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.PositivePayRegistrationNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PositivePayRegistrationRepositoryWrapper {
|
||||
|
||||
private final PositivePayRegistrationRepository repository;
|
||||
|
||||
public PositivePayRegistration findOneWithNotFoundDetection(final Long id) {
|
||||
return this.repository.findById(id).orElseThrow(() -> new PositivePayRegistrationNotFoundException(id));
|
||||
}
|
||||
|
||||
public PositivePayRegistration saveAndFlush(final PositivePayRegistration registration) {
|
||||
return this.repository.saveAndFlush(registration);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.domain;
|
||||
|
||||
public enum PositivePayStatus {
|
||||
|
||||
ACTIVE, //
|
||||
CANCELLED, //
|
||||
CONSUMED; //
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InwardClearingBatchNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InwardClearingBatchNotFoundException(final Long id) {
|
||||
super("error.msg.inward.clearing.batch.id.invalid", "Inward clearing batch with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InwardClearingItemNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InwardClearingItemNotFoundException(final Long id) {
|
||||
super("error.msg.inward.clearing.item.id.invalid", "Inward clearing item with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class OutwardClearingPresentmentNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public OutwardClearingPresentmentNotFoundException(final Long id) {
|
||||
super("error.msg.outward.clearing.presentment.id.invalid",
|
||||
"Outward clearing presentment with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class PositivePayRegistrationNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public PositivePayRegistrationNotFoundException(final Long id) {
|
||||
super("error.msg.positivepay.id.invalid", "Positive Pay registration with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "POSITIVEPAYREGISTRATION", action = "CANCEL")
|
||||
@RequiredArgsConstructor
|
||||
public class CancelPositivePayRegistrationCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.cancelPositivePay(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "OUTWARDCLEARINGPRESENTMENT", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateOutwardClearingPresentmentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createOutward(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "POSITIVEPAYREGISTRATION", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreatePositivePayRegistrationCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createPositivePay(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "OUTWARDCLEARINGPRESENTMENT", action = "DISHONOUR")
|
||||
@RequiredArgsConstructor
|
||||
public class DishonourOutwardClearingPresentmentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.dishonourOutward(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INWARDCLEARINGITEM", action = "PASS")
|
||||
@RequiredArgsConstructor
|
||||
public class PassInwardClearingItemCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.passItem(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INWARDCLEARINGBATCH", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class ReceiveInwardClearingBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.receiveBatch(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INWARDCLEARINGITEM", action = "RETURN")
|
||||
@RequiredArgsConstructor
|
||||
public class ReturnInwardClearingItemCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.returnItem(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "OUTWARDCLEARINGPRESENTMENT", action = "SEND")
|
||||
@RequiredArgsConstructor
|
||||
public class SendOutwardClearingPresentmentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.sendOutward(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "OUTWARDCLEARINGPRESENTMENT", action = "SENDBATCH")
|
||||
@RequiredArgsConstructor
|
||||
public class SendPendingOutwardClearingBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.sendPendingOutwardBatch(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "OUTWARDCLEARINGPRESENTMENT", action = "SETTLE")
|
||||
@RequiredArgsConstructor
|
||||
public class SettleOutwardClearingPresentmentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.settleOutward(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.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.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INWARDCLEARINGBATCH", action = "VALIDATE")
|
||||
@RequiredArgsConstructor
|
||||
public class ValidateInwardClearingBatchCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CtsClearingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.validateBatch(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.job;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/** Registers the (seeded-inactive) "CTS Outward Clearing Batch Send" Spring Batch job — same shape as {@code NeftBatchProcessJobConfig}. */
|
||||
@Configuration
|
||||
public class OutwardClearingBatchJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private CtsClearingWritePlatformService service;
|
||||
|
||||
@Bean
|
||||
public OutwardClearingBatchTasklet outwardClearingBatchTasklet() {
|
||||
return new OutwardClearingBatchTasklet(service);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step outwardClearingBatchStep() {
|
||||
return new StepBuilder(JobName.OUTWARD_CLEARING_BATCH_SEND.name(), jobRepository)
|
||||
.tasklet(outwardClearingBatchTasklet(), transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job outwardClearingBatchJob() {
|
||||
return new JobBuilder(JobName.OUTWARD_CLEARING_BATCH_SEND.name(), jobRepository).start(outwardClearingBatchStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.ctsclearing.job;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.ctsclearing.service.CtsClearingWritePlatformService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
/**
|
||||
* Clearing-session batch job: bundles every still-CAPTURED outward presentment into one CTS file and sends it
|
||||
* to NPCI, exactly like the manual "Send Pending Batch to NPCI" button (both call
|
||||
* {@link CtsClearingWritePlatformService#sendPendingOutwardBatch}, so there is one copy of the business logic).
|
||||
* Seeded INACTIVE by default (see the accompanying Liquibase changelog) — an operator opts in later from the
|
||||
* standard Admin > System > Manage Jobs screen once ready to automate outward sends.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class OutwardClearingBatchTasklet implements Tasklet {
|
||||
|
||||
private final CtsClearingWritePlatformService service;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) {
|
||||
final CommandProcessingResult result = this.service.sendPendingOutwardBatch(null);
|
||||
final Long sent = result == null ? null : result.getResourceId();
|
||||
log.info("[Job:OUTWARD_CLEARING_BATCH_SEND] cheques sent to NPCI in this run: {}", sent == null ? 0 : sent);
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.serialization;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import 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.ctsclearing.api.CtsClearingApiConstants;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class CtsClearingCommandFromApiJsonDeserializer {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
private static final Set<String> BATCH_CREATE_PARAMS = new HashSet<>(Arrays.asList(CtsClearingApiConstants.BATCH_REFERENCE,
|
||||
CtsClearingApiConstants.SESSION_DATE, CtsClearingApiConstants.PRESENTMENT_TYPE, CtsClearingApiConstants.SOURCE_FILE,
|
||||
CtsClearingApiConstants.PRESENTING_BANK_CODE, CtsClearingApiConstants.PRESENTING_BANK_NAME, CtsClearingApiConstants.OFFICE_ID,
|
||||
CtsClearingApiConstants.ITEMS, CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
private static final Set<String> ITEM_PASS_PARAMS = new HashSet<>(
|
||||
Arrays.asList(CtsClearingApiConstants.NOTE, CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
private static final Set<String> ITEM_RETURN_PARAMS = new HashSet<>(Arrays.asList(CtsClearingApiConstants.RETURN_REASON_CODE,
|
||||
CtsClearingApiConstants.REASON, CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
private static final Set<String> OUTWARD_CREATE_PARAMS = new HashSet<>(Arrays.asList(CtsClearingApiConstants.DEPOSITING_ACCOUNT_TYPE,
|
||||
CtsClearingApiConstants.DEPOSITING_ACCOUNT_ID, CtsClearingApiConstants.DRAWER_BANK_MICR, CtsClearingApiConstants.DRAWER_IFSC,
|
||||
CtsClearingApiConstants.DRAWER_BANK_NAME, CtsClearingApiConstants.CHEQUE_NUMBER, CtsClearingApiConstants.AMOUNT,
|
||||
CtsClearingApiConstants.PAYEE_NAME, CtsClearingApiConstants.DEPOSITED_DATE, CtsClearingApiConstants.INSTRUMENT_DATE,
|
||||
CtsClearingApiConstants.STOP_PAYMENT_FLAG, CtsClearingApiConstants.FRONT_IMAGE_REF, CtsClearingApiConstants.BACK_IMAGE_REF,
|
||||
CtsClearingApiConstants.IQA_STATUS, CtsClearingApiConstants.IQA_DETAIL, CtsClearingApiConstants.DRAWER_ACCOUNT_NUMBER,
|
||||
CtsClearingApiConstants.GRID_ZONE_CODE, CtsClearingApiConstants.GRID_ZONE_NAME, CtsClearingApiConstants.OFFICE_ID,
|
||||
CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
private static final Set<String> OUTWARD_DISHONOUR_PARAMS = new HashSet<>(Arrays.asList(CtsClearingApiConstants.DISHONOUR_REASON_CODE,
|
||||
CtsClearingApiConstants.REASON, CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
public void validateForReceiveBatch(final String json) {
|
||||
final JsonElement element = parse(json, BATCH_CREATE_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_INWARD_BATCH);
|
||||
|
||||
final String batchReference = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.BATCH_REFERENCE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.BATCH_REFERENCE).value(batchReference).notBlank().notExceedingLengthOf(100);
|
||||
|
||||
v.reset().parameter(CtsClearingApiConstants.SESSION_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(
|
||||
CtsClearingApiConstants.SESSION_DATE, element)).notNull();
|
||||
|
||||
final String presentmentType = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTMENT_TYPE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.PRESENTMENT_TYPE).value(presentmentType).ignoreIfNull().notExceedingLengthOf(10);
|
||||
|
||||
final String presentingBankCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BANK_CODE,
|
||||
element);
|
||||
v.reset().parameter(CtsClearingApiConstants.PRESENTING_BANK_CODE).value(presentingBankCode).ignoreIfNull()
|
||||
.notExceedingLengthOf(15);
|
||||
|
||||
final String presentingBankName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BANK_NAME,
|
||||
element);
|
||||
v.reset().parameter(CtsClearingApiConstants.PRESENTING_BANK_NAME).value(presentingBankName).ignoreIfNull()
|
||||
.notExceedingLengthOf(150);
|
||||
|
||||
final JsonArray items = this.fromApiJsonHelper.extractJsonArrayNamed(CtsClearingApiConstants.ITEMS, element);
|
||||
if (items == null || items.size() == 0) {
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS).value(items)
|
||||
.failWithCode("cannot.be.empty", "At least one clearing item must be supplied.");
|
||||
} else {
|
||||
final Locale locale = localeOrDefault(element);
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
validateItemElement(items.get(i), locale, v, i);
|
||||
}
|
||||
}
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
private void validateItemElement(final JsonElement itemElement, final Locale locale, final DataValidatorBuilder v, final int index) {
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, itemElement);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.CHEQUE_NUMBER)
|
||||
.value(chequeNumber).notBlank().notExceedingLengthOf(25);
|
||||
|
||||
final java.math.BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, itemElement,
|
||||
locale);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.AMOUNT).value(amount).notNull()
|
||||
.positiveAmount();
|
||||
|
||||
final String accountType = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ACCOUNT_TYPE, itemElement);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.ACCOUNT_TYPE).value(accountType)
|
||||
.notBlank().isOneOfTheseStringValues("savings", "current");
|
||||
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.ACCOUNT_ID, itemElement);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.ACCOUNT_ID).value(accountId)
|
||||
.notNull().longGreaterThanZero();
|
||||
|
||||
final String originalSerialNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ORIGINAL_SERIAL_NUMBER,
|
||||
itemElement);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.ORIGINAL_SERIAL_NUMBER)
|
||||
.value(originalSerialNumber).ignoreIfNull().notExceedingLengthOf(20);
|
||||
|
||||
final String presentingBranchCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BRANCH_CODE,
|
||||
itemElement);
|
||||
v.reset().parameter(CtsClearingApiConstants.ITEMS + "[" + index + "]." + CtsClearingApiConstants.PRESENTING_BRANCH_CODE)
|
||||
.value(presentingBranchCode).ignoreIfNull().notExceedingLengthOf(20);
|
||||
}
|
||||
|
||||
public void validateForPassItem(final String json) {
|
||||
final JsonElement element = parse(json, ITEM_PASS_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_INWARD_ITEM);
|
||||
final String note = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.NOTE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.NOTE).value(note).ignoreIfNull().notExceedingLengthOf(500);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForReturnItem(final String json) {
|
||||
final JsonElement element = parse(json, ITEM_RETURN_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_INWARD_ITEM);
|
||||
|
||||
final String returnReasonCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.RETURN_REASON_CODE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.RETURN_REASON_CODE).value(returnReasonCode).notBlank().notExceedingLengthOf(10);
|
||||
|
||||
final String reason = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.REASON, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.REASON).value(reason).ignoreIfNull().notExceedingLengthOf(500);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForCreateOutward(final String json) {
|
||||
final JsonElement element = parse(json, OUTWARD_CREATE_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_OUTWARD_PRESENTMENT);
|
||||
|
||||
final String accountType = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DEPOSITING_ACCOUNT_TYPE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DEPOSITING_ACCOUNT_TYPE).value(accountType).notBlank()
|
||||
.isOneOfTheseStringValues("savings", "current");
|
||||
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.DEPOSITING_ACCOUNT_ID, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DEPOSITING_ACCOUNT_ID).value(accountId).notNull().longGreaterThanZero();
|
||||
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.CHEQUE_NUMBER).value(chequeNumber).notBlank().notExceedingLengthOf(25);
|
||||
|
||||
final Locale locale = localeOrDefault(element);
|
||||
final java.math.BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, element, locale);
|
||||
v.reset().parameter(CtsClearingApiConstants.AMOUNT).value(amount).notNull().positiveAmount();
|
||||
|
||||
v.reset().parameter(CtsClearingApiConstants.DEPOSITED_DATE)
|
||||
.value(this.fromApiJsonHelper.extractLocalDateNamed(CtsClearingApiConstants.DEPOSITED_DATE, element)).notNull();
|
||||
|
||||
final String drawerBankName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_BANK_NAME, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DRAWER_BANK_NAME).value(drawerBankName).ignoreIfNull().notExceedingLengthOf(150);
|
||||
|
||||
final String drawerIfsc = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_IFSC, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DRAWER_IFSC).value(drawerIfsc).ignoreIfNull().notExceedingLengthOf(15);
|
||||
|
||||
final String drawerBankMicr = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_BANK_MICR, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DRAWER_BANK_MICR).value(drawerBankMicr).ignoreIfNull().notExceedingLengthOf(20);
|
||||
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForDishonourOutward(final String json) {
|
||||
final JsonElement element = parse(json, OUTWARD_DISHONOUR_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_OUTWARD_PRESENTMENT);
|
||||
final String reasonCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DISHONOUR_REASON_CODE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.DISHONOUR_REASON_CODE).value(reasonCode).notBlank().notExceedingLengthOf(10);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
private static final Set<String> POSITIVE_PAY_CREATE_PARAMS = new HashSet<>(Arrays.asList(
|
||||
CtsClearingApiConstants.DEPOSITING_ACCOUNT_TYPE, CtsClearingApiConstants.ACCOUNT_TYPE, CtsClearingApiConstants.ACCOUNT_ID,
|
||||
CtsClearingApiConstants.CHEQUE_NUMBER, CtsClearingApiConstants.INSTRUMENT_DATE, CtsClearingApiConstants.AMOUNT,
|
||||
CtsClearingApiConstants.PAYEE_NAME, CtsClearingApiConstants.CHANNEL, CtsClearingApiConstants.LOCALE,
|
||||
CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
private static final Set<String> POSITIVE_PAY_CANCEL_PARAMS = new HashSet<>(
|
||||
Arrays.asList(CtsClearingApiConstants.REASON, CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT));
|
||||
|
||||
public void validateForCreatePositivePay(final String json) {
|
||||
final JsonElement element = parse(json, POSITIVE_PAY_CREATE_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_POSITIVE_PAY);
|
||||
|
||||
final String accountType = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ACCOUNT_TYPE, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.ACCOUNT_TYPE).value(accountType).notBlank()
|
||||
.isOneOfTheseStringValues("savings", "current");
|
||||
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.ACCOUNT_ID, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.ACCOUNT_ID).value(accountId).notNull().longGreaterThanZero();
|
||||
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.CHEQUE_NUMBER).value(chequeNumber).notBlank().notExceedingLengthOf(25);
|
||||
|
||||
final Locale locale = localeOrDefault(element);
|
||||
final java.math.BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, element, locale);
|
||||
v.reset().parameter(CtsClearingApiConstants.AMOUNT).value(amount).notNull().positiveAmount();
|
||||
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
public void validateForCancelPositivePay(final String json) {
|
||||
final JsonElement element = parse(json, POSITIVE_PAY_CANCEL_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(CtsClearingApiConstants.RESOURCE_POSITIVE_PAY);
|
||||
final String reason = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.REASON, element);
|
||||
v.reset().parameter(CtsClearingApiConstants.REASON).value(reason).ignoreIfNull().notExceedingLengthOf(500);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
/** SEND / SETTLE carry no body of their own — accept an empty/locale-only payload. */
|
||||
public void validateNoBody(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return;
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
||||
new HashSet<>(Arrays.asList(CtsClearingApiConstants.LOCALE, CtsClearingApiConstants.DATE_FORMAT)));
|
||||
}
|
||||
|
||||
private Locale localeOrDefault(final JsonElement element) {
|
||||
final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject());
|
||||
return locale == null ? Locale.ENGLISH : locale;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -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.ctsclearing.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.InwardClearingBatchData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.InwardClearingItemData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.OutwardClearingBatchSummaryData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.OutwardClearingPresentmentData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.PositivePayRegistrationData;
|
||||
|
||||
public interface CtsClearingReadPlatformService {
|
||||
|
||||
Page<InwardClearingBatchData> retrieveAllBatches(String status, LocalDate fromSessionDate, LocalDate toSessionDate,
|
||||
String batchReference, String presentingBankName, Integer offset, Integer limit);
|
||||
|
||||
InwardClearingBatchData retrieveBatch(Long id);
|
||||
|
||||
List<InwardClearingItemData> retrieveItemsByBatch(Long batchId);
|
||||
|
||||
InwardClearingItemData retrieveItem(Long id);
|
||||
|
||||
Page<OutwardClearingPresentmentData> retrieveAllOutward(String status, LocalDate fromDepositedDate, LocalDate toDepositedDate,
|
||||
String chequeNumber, String drawerBankName, String payeeName, String batchReference, Integer offset, Integer limit);
|
||||
|
||||
OutwardClearingPresentmentData retrieveOutward(Long id);
|
||||
|
||||
/** One row per outward clearing session ("batch") — cheques grouped by the shared batchReference they were sent under. */
|
||||
Page<OutwardClearingBatchSummaryData> retrieveOutwardBatches(LocalDate fromDate, LocalDate toDate, String batchReference,
|
||||
Integer offset, Integer limit);
|
||||
|
||||
Page<PositivePayRegistrationData> retrievePositivePayRegistrations(Long accountId, String status, Integer offset, Integer limit);
|
||||
|
||||
PositivePayRegistrationData retrievePositivePayRegistration(Long id);
|
||||
|
||||
/** Returns the stored "mimeType|storagePath" for an inward item's front/back image, or null if none uploaded. */
|
||||
String retrieveInwardItemImageStore(Long itemId, String side);
|
||||
|
||||
/** Returns the stored "mimeType|storagePath" for an outward presentment's front/back image, or null if none uploaded. */
|
||||
String retrieveOutwardPresentmentImageStore(Long presentmentId, String side);
|
||||
}
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.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.ctsclearing.data.InwardClearingBatchData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.InwardClearingItemData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.OutwardClearingBatchSummaryData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.OutwardClearingPresentmentData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.data.PositivePayRegistrationData;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.InwardClearingBatchNotFoundException;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.InwardClearingItemNotFoundException;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.OutwardClearingPresentmentNotFoundException;
|
||||
import org.apache.fineract.portfolio.ctsclearing.exception.PositivePayRegistrationNotFoundException;
|
||||
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 CtsClearingReadPlatformServiceImpl implements CtsClearingReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final SavingsAccountRepository savingsAccountRepository;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final BatchMapper batchMapper = new BatchMapper();
|
||||
private final ItemMapper itemMapper = new ItemMapper();
|
||||
private final OutwardMapper outwardMapper = new OutwardMapper();
|
||||
private final OutwardBatchSummaryMapper outwardBatchSummaryMapper = new OutwardBatchSummaryMapper();
|
||||
private final PositivePayMapper positivePayMapper = new PositivePayMapper();
|
||||
|
||||
@Override
|
||||
public Page<InwardClearingBatchData> retrieveAllBatches(final String status, final LocalDate fromSessionDate,
|
||||
final LocalDate toSessionDate, final String batchReference, final String presentingBankName, 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 b.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
}
|
||||
if (fromSessionDate != null) {
|
||||
where.append(" and b.session_date >= ?");
|
||||
args.add(java.sql.Date.valueOf(fromSessionDate));
|
||||
}
|
||||
if (toSessionDate != null) {
|
||||
where.append(" and b.session_date <= ?");
|
||||
args.add(java.sql.Date.valueOf(toSessionDate));
|
||||
}
|
||||
if (StringUtils.isNotBlank(batchReference)) {
|
||||
where.append(" and lower(b.batch_reference) like lower(?)");
|
||||
args.add("%" + batchReference.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(presentingBankName)) {
|
||||
where.append(" and lower(b.presenting_bank_name) like lower(?)");
|
||||
args.add("%" + presentingBankName.trim() + "%");
|
||||
}
|
||||
final String countSql = "select count(*) from (select " + this.batchMapper.schema() + where + ") x";
|
||||
final Long total = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final StringBuilder pageSql = new StringBuilder("select ").append(this.batchMapper.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<InwardClearingBatchData> batches = this.jdbcTemplate.query(pageSql.toString(), this.batchMapper, pageArgs.toArray());
|
||||
return new Page<>(batches, total == null ? 0 : total.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InwardClearingBatchData retrieveBatch(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.batchMapper.schema() + " where b.id = ?";
|
||||
return this.jdbcTemplate.queryForObject(sql, this.batchMapper, id);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new InwardClearingBatchNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InwardClearingItemData> retrieveItemsByBatch(final Long batchId) {
|
||||
final String sql = "select " + this.itemMapper.schema() + " where i.batch_id = ? order by i.id asc";
|
||||
final List<InwardClearingItemData> items = this.jdbcTemplate.query(sql, this.itemMapper, batchId);
|
||||
return items.stream().map(this::withAccountInfo).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InwardClearingItemData retrieveItem(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.itemMapper.schema() + " where i.id = ?";
|
||||
final InwardClearingItemData item = this.jdbcTemplate.queryForObject(sql, this.itemMapper, id);
|
||||
return withAccountInfo(item);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new InwardClearingItemNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<OutwardClearingPresentmentData> retrieveAllOutward(final String status, final LocalDate fromDepositedDate,
|
||||
final LocalDate toDepositedDate, final String chequeNumber, final String drawerBankName, final String payeeName,
|
||||
final String batchReference, 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 p.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
}
|
||||
if (fromDepositedDate != null) {
|
||||
where.append(" and p.deposited_date >= ?");
|
||||
args.add(java.sql.Date.valueOf(fromDepositedDate));
|
||||
}
|
||||
if (toDepositedDate != null) {
|
||||
where.append(" and p.deposited_date <= ?");
|
||||
args.add(java.sql.Date.valueOf(toDepositedDate));
|
||||
}
|
||||
if (StringUtils.isNotBlank(chequeNumber)) {
|
||||
where.append(" and lower(p.cheque_number) like lower(?)");
|
||||
args.add("%" + chequeNumber.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(drawerBankName)) {
|
||||
where.append(" and lower(p.drawer_bank_name) like lower(?)");
|
||||
args.add("%" + drawerBankName.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(payeeName)) {
|
||||
where.append(" and lower(p.payee_name) like lower(?)");
|
||||
args.add("%" + payeeName.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(batchReference)) {
|
||||
where.append(" and p.batch_reference = ?");
|
||||
args.add(batchReference.trim());
|
||||
}
|
||||
final String countSql = "select count(*) from (select " + this.outwardMapper.schema() + where + ") x";
|
||||
final Long total = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final StringBuilder pageSql = new StringBuilder("select ").append(this.outwardMapper.schema()).append(where)
|
||||
.append(" order by p.id desc limit ? offset ?");
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(limit);
|
||||
pageArgs.add(offset);
|
||||
final List<OutwardClearingPresentmentData> rows = this.jdbcTemplate.query(pageSql.toString(), this.outwardMapper,
|
||||
pageArgs.toArray());
|
||||
final List<OutwardClearingPresentmentData> withInfo = rows.stream().map(this::withAccountInfo).toList();
|
||||
return new Page<>(withInfo, total == null ? 0 : total.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutwardClearingPresentmentData retrieveOutward(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.outwardMapper.schema() + " where p.id = ?";
|
||||
final OutwardClearingPresentmentData row = this.jdbcTemplate.queryForObject(sql, this.outwardMapper, id);
|
||||
return withAccountInfo(row);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new OutwardClearingPresentmentNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<OutwardClearingBatchSummaryData> retrieveOutwardBatches(final LocalDate fromDate, final LocalDate toDate,
|
||||
final String batchReference, final Integer offset, final Integer limit) {
|
||||
final StringBuilder where = new StringBuilder(" where batch_reference is not null");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (fromDate != null) {
|
||||
where.append(" and date(sent_on) >= ?");
|
||||
args.add(java.sql.Date.valueOf(fromDate));
|
||||
}
|
||||
if (toDate != null) {
|
||||
where.append(" and date(sent_on) <= ?");
|
||||
args.add(java.sql.Date.valueOf(toDate));
|
||||
}
|
||||
if (StringUtils.isNotBlank(batchReference)) {
|
||||
where.append(" and lower(batch_reference) like lower(?)");
|
||||
args.add("%" + batchReference.trim() + "%");
|
||||
}
|
||||
|
||||
final String countSql = "select count(*) from (select batch_reference from m_outward_clearing_presentment " + where
|
||||
+ " group by batch_reference) x";
|
||||
final Long total = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final String groupedSchema = "batch_reference, min(sent_on) as session_started_on, max(sent_on) as session_ended_on, "
|
||||
+ "count(*) as item_count, sum(amount) as total_amount, "
|
||||
+ "sum(case when status = 'SENT_TO_CLEARING_HOUSE' then 1 else 0 end) as sent_count, "
|
||||
+ "sum(case when status = 'SETTLED' then 1 else 0 end) as settled_count, "
|
||||
+ "sum(case when status = 'DISHONOURED' then 1 else 0 end) as dishonoured_count "
|
||||
+ "from m_outward_clearing_presentment" + where + " group by batch_reference";
|
||||
|
||||
final String pageSql = "select " + groupedSchema + " order by min(sent_on) desc limit ? offset ?";
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(limit);
|
||||
pageArgs.add(offset);
|
||||
final List<OutwardClearingBatchSummaryData> rows = this.jdbcTemplate.query(pageSql, this.outwardBatchSummaryMapper,
|
||||
pageArgs.toArray());
|
||||
return new Page<>(rows, total == null ? 0 : total.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<PositivePayRegistrationData> retrievePositivePayRegistrations(final Long accountId, final String status,
|
||||
final Integer offset, final Integer limit) {
|
||||
final StringBuilder where = new StringBuilder(" where 1=1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (accountId != null) {
|
||||
where.append(" and pp.account_id = ?");
|
||||
args.add(accountId);
|
||||
}
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
where.append(" and pp.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
}
|
||||
final String countSql = "select count(*) from (select " + this.positivePayMapper.schema() + where + ") x";
|
||||
final Long total = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final StringBuilder pageSql = new StringBuilder("select ").append(this.positivePayMapper.schema()).append(where)
|
||||
.append(" order by pp.id desc limit ? offset ?");
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(limit);
|
||||
pageArgs.add(offset);
|
||||
final List<PositivePayRegistrationData> rows = this.jdbcTemplate.query(pageSql.toString(), this.positivePayMapper,
|
||||
pageArgs.toArray());
|
||||
final List<PositivePayRegistrationData> withInfo = rows.stream().map(this::withAccountInfo).toList();
|
||||
return new Page<>(withInfo, total == null ? 0 : total.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PositivePayRegistrationData retrievePositivePayRegistration(final Long id) {
|
||||
try {
|
||||
final String sql = "select " + this.positivePayMapper.schema() + " where pp.id = ?";
|
||||
final PositivePayRegistrationData row = this.jdbcTemplate.queryForObject(sql, this.positivePayMapper, id);
|
||||
return withAccountInfo(row);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new PositivePayRegistrationNotFoundException(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveInwardItemImageStore(final Long itemId, final String side) {
|
||||
final String column = "front".equalsIgnoreCase(side) ? "front_image_store" : "back_image_store";
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject("select " + column + " from m_inward_clearing_item where id = ?", String.class,
|
||||
itemId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveOutwardPresentmentImageStore(final Long presentmentId, final String side) {
|
||||
final String column = "front".equalsIgnoreCase(side) ? "front_image_store" : "back_image_store";
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject("select " + column + " from m_outward_clearing_presentment where id = ?",
|
||||
String.class, presentmentId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Account display (best effort) =============================
|
||||
|
||||
private InwardClearingItemData withAccountInfo(final InwardClearingItemData item) {
|
||||
final AccountDisplay display = resolveAccountDisplay(item.getAccountType(), item.getAccountId());
|
||||
return InwardClearingItemData.builder().id(item.getId()).batchId(item.getBatchId()).chequeLeafId(item.getChequeLeafId())
|
||||
.accountType(item.getAccountType()).accountId(item.getAccountId()).accountNo(display.accountNo)
|
||||
.clientName(display.clientName).chequeNumber(item.getChequeNumber()).micrCode(item.getMicrCode()).ifsc(item.getIfsc())
|
||||
.payeeName(item.getPayeeName()).amount(item.getAmount()).amountInWords(item.getAmountInWords())
|
||||
.instrumentDate(item.getInstrumentDate()).presentedDate(item.getPresentedDate())
|
||||
.frontImageRef(item.getFrontImageRef()).backImageRef(item.getBackImageRef()).iqaStatus(item.getIqaStatus())
|
||||
.iqaDetail(item.getIqaDetail())
|
||||
.accountCheckPassed(item.getAccountCheckPassed()).balanceCheckPassed(item.getBalanceCheckPassed())
|
||||
.signatureOk(item.getSignatureOk()).duplicateCheckPassed(item.getDuplicateCheckPassed())
|
||||
.stopPaymentCheckPassed(item.getStopPaymentCheckPassed()).amountWordsFiguresMatch(item.getAmountWordsFiguresMatch())
|
||||
.staleOrPostdated(item.getStaleOrPostdated()).positivePayCheck(item.getPositivePayCheck())
|
||||
.validationDetail(item.getValidationDetail()).decision(item.getDecision())
|
||||
.returnReasonCode(item.getReturnReasonCode()).decisionReason(item.getDecisionReason()).decisionedBy(item.getDecisionedBy())
|
||||
.decisionedOn(item.getDecisionedOn()).itemStatus(item.getItemStatus()).holdPlaced(item.getHoldPlaced())
|
||||
.holdAmount(item.getHoldAmount()).settlementBatchId(item.getSettlementBatchId())
|
||||
.originalSerialNumber(item.getOriginalSerialNumber()).presentingBranchCode(item.getPresentingBranchCode())
|
||||
.returnDeadline(item.getReturnDeadline()).ctsReferenceNumber(item.getCtsReferenceNumber())
|
||||
.instrumentStopPaymentFlag(item.getInstrumentStopPaymentFlag()).depositDate(item.getDepositDate()).build();
|
||||
}
|
||||
|
||||
private OutwardClearingPresentmentData withAccountInfo(final OutwardClearingPresentmentData row) {
|
||||
final AccountDisplay display = resolveAccountDisplay(row.getDepositingAccountType(), row.getDepositingAccountId());
|
||||
return OutwardClearingPresentmentData.builder().id(row.getId()).depositingAccountType(row.getDepositingAccountType())
|
||||
.depositingAccountId(row.getDepositingAccountId()).accountNo(display.accountNo).clientName(display.clientName)
|
||||
.drawerBankMicr(row.getDrawerBankMicr()).drawerIfsc(row.getDrawerIfsc()).drawerBankName(row.getDrawerBankName())
|
||||
.chequeNumber(row.getChequeNumber()).amount(row.getAmount()).payeeName(row.getPayeeName())
|
||||
.depositedDate(row.getDepositedDate()).instrumentDate(row.getInstrumentDate())
|
||||
.stopPaymentFlag(row.getStopPaymentFlag()).frontImageRef(row.getFrontImageRef()).backImageRef(row.getBackImageRef())
|
||||
.iqaStatus(row.getIqaStatus()).iqaDetail(row.getIqaDetail()).drawerAccountNumber(row.getDrawerAccountNumber())
|
||||
.gridZoneCode(row.getGridZoneCode()).gridZoneName(row.getGridZoneName()).status(row.getStatus())
|
||||
.batchReference(row.getBatchReference()).ctsFileJson(row.getCtsFileJson())
|
||||
.npciResponseJson(row.getNpciResponseJson())
|
||||
.sentOn(row.getSentOn())
|
||||
.dishonourReasonCode(row.getDishonourReasonCode())
|
||||
.officeId(row.getOfficeId()).settledOn(row.getSettledOn()).createdBy(row.getCreatedBy()).createdOn(row.getCreatedOn())
|
||||
.build();
|
||||
}
|
||||
|
||||
private PositivePayRegistrationData withAccountInfo(final PositivePayRegistrationData row) {
|
||||
final AccountDisplay display = resolveAccountDisplay(row.getAccountType(), row.getAccountId());
|
||||
return PositivePayRegistrationData.builder().id(row.getId()).accountType(row.getAccountType()).accountId(row.getAccountId())
|
||||
.accountNo(display.accountNo).clientName(display.clientName).chequeNumber(row.getChequeNumber())
|
||||
.instrumentDate(row.getInstrumentDate()).amount(row.getAmount()).payeeName(row.getPayeeName()).channel(row.getChannel())
|
||||
.status(row.getStatus()).registeredBy(row.getRegisteredBy()).registeredOn(row.getRegisteredOn())
|
||||
.cancelledReason(row.getCancelledReason()).build();
|
||||
}
|
||||
|
||||
private AccountDisplay resolveAccountDisplay(final String accountType, final Long accountId) {
|
||||
if (accountType == null || accountId == null) {
|
||||
return new AccountDisplay(null, null);
|
||||
}
|
||||
if ("SAVINGS".equalsIgnoreCase(accountType)) {
|
||||
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 ("CURRENT".equalsIgnoreCase(accountType)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Row mappers =============================
|
||||
|
||||
private static final class BatchMapper implements RowMapper<InwardClearingBatchData> {
|
||||
|
||||
String schema() {
|
||||
return " b.id as id, b.batch_reference as batch_reference, b.session_date as session_date, "
|
||||
+ "b.presentment_type as presentment_type, b.source_file as source_file, b.total_item_count as total_item_count, "
|
||||
+ "b.total_amount as total_amount, b.status as status, b.office_id as office_id, o.name as office_name, "
|
||||
+ "b.received_by as received_by, b.received_on as received_on, b.validated_on as validated_on, "
|
||||
+ "b.decisioned_on as decisioned_on, b.settled_on as settled_on, b.created_on as created_on, "
|
||||
+ "b.presenting_bank_code as presenting_bank_code, b.presenting_bank_name as presenting_bank_name, "
|
||||
+ "b.return_tat_deadline as return_tat_deadline "
|
||||
+ "from m_inward_clearing_batch b left join m_office o on o.id = b.office_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public InwardClearingBatchData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return InwardClearingBatchData.builder().id(rs.getLong("id")).batchReference(rs.getString("batch_reference"))
|
||||
.sessionDate(rs.getObject("session_date", LocalDate.class)).presentmentType(rs.getString("presentment_type"))
|
||||
.sourceFile(rs.getString("source_file")).totalItemCount((Integer) rs.getObject("total_item_count"))
|
||||
.totalAmount(rs.getBigDecimal("total_amount")).status(rs.getString("status")).officeId((Long) rs.getObject("office_id"))
|
||||
.officeName(rs.getString("office_name")).receivedBy((Long) rs.getObject("received_by"))
|
||||
.receivedOn(rs.getObject("received_on", OffsetDateTime.class)).validatedOn(rs.getObject("validated_on", OffsetDateTime.class))
|
||||
.decisionedOn(rs.getObject("decisioned_on", OffsetDateTime.class)).settledOn(rs.getObject("settled_on", OffsetDateTime.class))
|
||||
.createdOn(rs.getObject("created_on", OffsetDateTime.class))
|
||||
.presentingBankCode(rs.getString("presenting_bank_code")).presentingBankName(rs.getString("presenting_bank_name"))
|
||||
.returnTatDeadline(rs.getObject("return_tat_deadline", LocalDate.class)).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ItemMapper implements RowMapper<InwardClearingItemData> {
|
||||
|
||||
String schema() {
|
||||
return " i.id as id, i.batch_id as batch_id, i.cheque_leaf_id as cheque_leaf_id, i.account_type as account_type, "
|
||||
+ "i.account_id as account_id, i.cheque_number as cheque_number, i.micr_code as micr_code, i.ifsc as ifsc, "
|
||||
+ "i.payee_name as payee_name, i.amount as amount, i.amount_in_words as amount_in_words, "
|
||||
+ "i.instrument_date as instrument_date, i.presented_date as presented_date, "
|
||||
+ "i.front_image_ref as front_image_ref, i.back_image_ref as back_image_ref, i.iqa_status as iqa_status, "
|
||||
+ "i.iqa_detail as iqa_detail, "
|
||||
+ "i.account_check_passed as account_check_passed, i.balance_check_passed as balance_check_passed, "
|
||||
+ "i.signature_ok as signature_ok, i.duplicate_check_passed as duplicate_check_passed, "
|
||||
+ "i.stop_payment_check_passed as stop_payment_check_passed, i.amount_words_figures_match as amount_words_figures_match, "
|
||||
+ "i.stale_or_postdated as stale_or_postdated, i.positive_pay_check as positive_pay_check, "
|
||||
+ "i.validation_detail as validation_detail, i.decision as decision, "
|
||||
+ "i.return_reason_code as return_reason_code, i.decision_reason as decision_reason, i.decisioned_by as decisioned_by, "
|
||||
+ "i.decisioned_on as decisioned_on, i.item_status as item_status, i.hold_placed as hold_placed, "
|
||||
+ "i.hold_amount as hold_amount, i.settlement_batch_id as settlement_batch_id, "
|
||||
+ "i.original_serial_number as original_serial_number, i.presenting_branch_code as presenting_branch_code, "
|
||||
+ "i.return_deadline as return_deadline, i.cts_reference_number as cts_reference_number, "
|
||||
+ "i.instrument_stop_payment_flag as instrument_stop_payment_flag, i.deposit_date as deposit_date "
|
||||
+ "from m_inward_clearing_item i";
|
||||
}
|
||||
|
||||
@Override
|
||||
public InwardClearingItemData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return InwardClearingItemData.builder().id(rs.getLong("id")).batchId(rs.getLong("batch_id"))
|
||||
.chequeLeafId((Long) rs.getObject("cheque_leaf_id")).accountType(rs.getString("account_type"))
|
||||
.accountId((Long) rs.getObject("account_id")).chequeNumber(rs.getString("cheque_number"))
|
||||
.micrCode(rs.getString("micr_code")).ifsc(rs.getString("ifsc")).payeeName(rs.getString("payee_name"))
|
||||
.amount(rs.getBigDecimal("amount")).amountInWords(rs.getString("amount_in_words"))
|
||||
.instrumentDate(rs.getObject("instrument_date", LocalDate.class)).presentedDate(rs.getObject("presented_date", LocalDate.class))
|
||||
.frontImageRef(rs.getString("front_image_ref")).backImageRef(rs.getString("back_image_ref"))
|
||||
.iqaStatus(rs.getString("iqa_status")).iqaDetail(rs.getString("iqa_detail"))
|
||||
.accountCheckPassed((Boolean) rs.getObject("account_check_passed"))
|
||||
.balanceCheckPassed((Boolean) rs.getObject("balance_check_passed")).signatureOk((Boolean) rs.getObject("signature_ok"))
|
||||
.duplicateCheckPassed((Boolean) rs.getObject("duplicate_check_passed"))
|
||||
.stopPaymentCheckPassed((Boolean) rs.getObject("stop_payment_check_passed"))
|
||||
.amountWordsFiguresMatch((Boolean) rs.getObject("amount_words_figures_match"))
|
||||
.staleOrPostdated((Boolean) rs.getObject("stale_or_postdated"))
|
||||
.positivePayCheck((Boolean) rs.getObject("positive_pay_check")).validationDetail(rs.getString("validation_detail"))
|
||||
.decision(rs.getString("decision")).returnReasonCode(rs.getString("return_reason_code"))
|
||||
.decisionReason(rs.getString("decision_reason")).decisionedBy((Long) rs.getObject("decisioned_by"))
|
||||
.decisionedOn(rs.getObject("decisioned_on", OffsetDateTime.class)).itemStatus(rs.getString("item_status"))
|
||||
.holdPlaced((Boolean) rs.getObject("hold_placed")).holdAmount(rs.getBigDecimal("hold_amount"))
|
||||
.settlementBatchId((Long) rs.getObject("settlement_batch_id"))
|
||||
.originalSerialNumber(rs.getString("original_serial_number"))
|
||||
.presentingBranchCode(rs.getString("presenting_branch_code"))
|
||||
.returnDeadline(rs.getObject("return_deadline", LocalDate.class))
|
||||
.ctsReferenceNumber(rs.getString("cts_reference_number"))
|
||||
.instrumentStopPaymentFlag((Boolean) rs.getObject("instrument_stop_payment_flag"))
|
||||
.depositDate(rs.getObject("deposit_date", LocalDate.class)).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OutwardMapper implements RowMapper<OutwardClearingPresentmentData> {
|
||||
|
||||
String schema() {
|
||||
return " p.id as id, p.depositing_account_type as depositing_account_type, p.depositing_account_id as depositing_account_id, "
|
||||
+ "p.drawer_bank_micr as drawer_bank_micr, p.drawer_ifsc as drawer_ifsc, p.drawer_bank_name as drawer_bank_name, "
|
||||
+ "p.cheque_number as cheque_number, p.amount as amount, p.payee_name as payee_name, "
|
||||
+ "p.deposited_date as deposited_date, p.instrument_date as instrument_date, "
|
||||
+ "p.stop_payment_flag as stop_payment_flag, p.front_image_ref as front_image_ref, "
|
||||
+ "p.back_image_ref as back_image_ref, p.iqa_status as iqa_status, p.iqa_detail as iqa_detail, "
|
||||
+ "p.drawer_account_number as drawer_account_number, p.grid_zone_code as grid_zone_code, "
|
||||
+ "p.grid_zone_name as grid_zone_name, p.status as status, "
|
||||
+ "p.batch_reference as batch_reference, p.cts_file_json as cts_file_json, "
|
||||
+ "p.npci_response_json as npci_response_json, "
|
||||
+ "p.sent_on as sent_on, p.dishonour_reason_code as dishonour_reason_code, "
|
||||
+ "p.office_id as office_id, p.settled_on as settled_on, p.created_by as created_by, p.created_on as created_on "
|
||||
+ "from m_outward_clearing_presentment p";
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutwardClearingPresentmentData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return OutwardClearingPresentmentData.builder().id(rs.getLong("id"))
|
||||
.depositingAccountType(rs.getString("depositing_account_type"))
|
||||
.depositingAccountId((Long) rs.getObject("depositing_account_id")).drawerBankMicr(rs.getString("drawer_bank_micr"))
|
||||
.drawerIfsc(rs.getString("drawer_ifsc")).drawerBankName(rs.getString("drawer_bank_name"))
|
||||
.chequeNumber(rs.getString("cheque_number")).amount(rs.getBigDecimal("amount")).payeeName(rs.getString("payee_name"))
|
||||
.depositedDate(rs.getObject("deposited_date", LocalDate.class))
|
||||
.instrumentDate(rs.getObject("instrument_date", LocalDate.class))
|
||||
.stopPaymentFlag((Boolean) rs.getObject("stop_payment_flag")).frontImageRef(rs.getString("front_image_ref"))
|
||||
.backImageRef(rs.getString("back_image_ref")).iqaStatus(rs.getString("iqa_status"))
|
||||
.iqaDetail(rs.getString("iqa_detail")).drawerAccountNumber(rs.getString("drawer_account_number"))
|
||||
.gridZoneCode(rs.getString("grid_zone_code")).gridZoneName(rs.getString("grid_zone_name"))
|
||||
.status(rs.getString("status"))
|
||||
.batchReference(rs.getString("batch_reference")).ctsFileJson(rs.getString("cts_file_json"))
|
||||
.npciResponseJson(rs.getString("npci_response_json"))
|
||||
.sentOn(rs.getObject("sent_on", OffsetDateTime.class))
|
||||
.dishonourReasonCode(rs.getString("dishonour_reason_code")).officeId((Long) rs.getObject("office_id"))
|
||||
.settledOn(rs.getObject("settled_on", OffsetDateTime.class)).createdBy((Long) rs.getObject("created_by"))
|
||||
.createdOn(rs.getObject("created_on", OffsetDateTime.class)).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OutwardBatchSummaryMapper implements RowMapper<OutwardClearingBatchSummaryData> {
|
||||
|
||||
@Override
|
||||
public OutwardClearingBatchSummaryData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return OutwardClearingBatchSummaryData.builder().batchReference(rs.getString("batch_reference"))
|
||||
.sessionStartedOn(rs.getObject("session_started_on", OffsetDateTime.class))
|
||||
.sessionEndedOn(rs.getObject("session_ended_on", OffsetDateTime.class)).itemCount(rs.getInt("item_count"))
|
||||
.totalAmount(rs.getBigDecimal("total_amount")).sentCount(rs.getInt("sent_count"))
|
||||
.settledCount(rs.getInt("settled_count")).dishonouredCount(rs.getInt("dishonoured_count")).build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PositivePayMapper implements RowMapper<PositivePayRegistrationData> {
|
||||
|
||||
String schema() {
|
||||
return " pp.id as id, pp.account_type as account_type, pp.account_id as account_id, pp.cheque_number as cheque_number, "
|
||||
+ "pp.instrument_date as instrument_date, pp.amount as amount, pp.payee_name as payee_name, pp.channel as channel, "
|
||||
+ "pp.status as status, pp.registered_by as registered_by, pp.registered_on as registered_on, "
|
||||
+ "pp.cancelled_reason as cancelled_reason from m_positive_pay_registration pp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PositivePayRegistrationData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return PositivePayRegistrationData.builder().id(rs.getLong("id")).accountType(rs.getString("account_type"))
|
||||
.accountId((Long) rs.getObject("account_id")).chequeNumber(rs.getString("cheque_number"))
|
||||
.instrumentDate(rs.getObject("instrument_date", LocalDate.class)).amount(rs.getBigDecimal("amount"))
|
||||
.payeeName(rs.getString("payee_name")).channel(rs.getString("channel")).status(rs.getString("status"))
|
||||
.registeredBy((Long) rs.getObject("registered_by")).registeredOn(rs.getObject("registered_on", OffsetDateTime.class))
|
||||
.cancelledReason(rs.getString("cancelled_reason")).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface CtsClearingWritePlatformService {
|
||||
|
||||
CommandProcessingResult receiveBatch(JsonCommand command);
|
||||
|
||||
CommandProcessingResult validateBatch(Long batchId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult passItem(Long itemId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult returnItem(Long itemId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult createOutward(JsonCommand command);
|
||||
|
||||
CommandProcessingResult sendOutward(Long presentmentId, JsonCommand command);
|
||||
|
||||
/** Bundles every still-CAPTURED outward presentment into ONE CTS batch file and sends it to NPCI. */
|
||||
CommandProcessingResult sendPendingOutwardBatch(JsonCommand command);
|
||||
|
||||
CommandProcessingResult settleOutward(Long presentmentId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult dishonourOutward(Long presentmentId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult createPositivePay(JsonCommand command);
|
||||
|
||||
CommandProcessingResult cancelPositivePay(Long registrationId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Records where a real uploaded front/back image for an inward item lives (a plain field update, not a
|
||||
* command — no financial effect, so it bypasses the audited command pipeline like the shared election
|
||||
* module's own image-upload endpoint does).
|
||||
*/
|
||||
void recordInwardItemImage(Long itemId, String side, String mimeTypeAndPath);
|
||||
|
||||
/** Same as {@link #recordInwardItemImage} for an outward presentment. */
|
||||
void recordOutwardPresentmentImage(Long presentmentId, String side, String mimeTypeAndPath);
|
||||
}
|
||||
+848
@@ -0,0 +1,848 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ctsclearing.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
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 com.google.gson.JsonObject;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingReturnReasonCode;
|
||||
import org.apache.fineract.portfolio.clearing.domain.ClearingReturnReasonCodeRepository;
|
||||
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
|
||||
import org.apache.fineract.portfolio.clearing.npci.client.NpciClearingClient;
|
||||
import org.apache.fineract.portfolio.clearing.service.ClearingGlAccountResolverService;
|
||||
import org.apache.fineract.portfolio.clearing.service.ClearingJournalService;
|
||||
import org.apache.fineract.portfolio.clearing.service.ClearingNotificationService;
|
||||
import org.apache.fineract.portfolio.ctsclearing.api.CtsClearingApiConstants;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatch;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatchRepository;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingBatchRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingItem;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingItemRepository;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.InwardClearingItemRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.OutwardClearingPresentment;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.OutwardClearingPresentmentRepository;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.OutwardClearingPresentmentRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.OutwardClearingPresentmentStatus;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.PositivePayRegistration;
|
||||
import org.apache.fineract.portfolio.ctsclearing.domain.PositivePayRegistrationRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.ctsclearing.serialization.CtsClearingCommandFromApiJsonDeserializer;
|
||||
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.SavingsAccount;
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Write-side of the CTS inward/outward clearing module. Mirrors the JsonCommand + CommandProcessingResultBuilder
|
||||
* conventions used by {@code ChequeBookWritePlatformServiceImpl}, and reuses the shared clearing services for GL
|
||||
* resolution, balanced journal posting and customer notifications. The customer-account debit on a passed item
|
||||
* follows {@code ChequeBookIssuanceChargeService}'s direct-ledger pattern (a transaction row + balance-field
|
||||
* update, no standard withdrawal API).
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CtsClearingWritePlatformServiceImpl implements CtsClearingWritePlatformService {
|
||||
|
||||
private static final String SAVINGS = "SAVINGS";
|
||||
private static final String CTS_INWARD_SUSPENSE = "CTS_INWARD_SUSPENSE";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CtsClearingCommandFromApiJsonDeserializer validator;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
private final InwardClearingBatchRepository batchRepository;
|
||||
private final InwardClearingBatchRepositoryWrapper batchRepositoryWrapper;
|
||||
private final InwardClearingItemRepository itemRepository;
|
||||
private final InwardClearingItemRepositoryWrapper itemRepositoryWrapper;
|
||||
private final OutwardClearingPresentmentRepository outwardRepository;
|
||||
private final OutwardClearingPresentmentRepositoryWrapper outwardRepositoryWrapper;
|
||||
private final PositivePayRegistrationRepositoryWrapper positivePayRepositoryWrapper;
|
||||
private final InwardClearingValidationService validationService;
|
||||
|
||||
private final ClearingGlAccountResolverService glResolver;
|
||||
private final ClearingJournalService clearingJournalService;
|
||||
private final ClearingNotificationService notificationService;
|
||||
private final ClearingReturnReasonCodeRepository returnReasonCodeRepository;
|
||||
private final ContentStoreService contentStoreService;
|
||||
private final NpciClearingClient npciClearingClient;
|
||||
|
||||
private final ChequeLeafRepository chequeLeafRepository;
|
||||
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
|
||||
private final CurrentAccountRepositoryWrapper currentAccountRepositoryWrapper;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
|
||||
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
|
||||
private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper;
|
||||
private final SavingsHelper savingsHelper;
|
||||
|
||||
// ============================= Inward batch =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult receiveBatch(final JsonCommand command) {
|
||||
this.validator.validateForReceiveBatch(command.json());
|
||||
final JsonElement element = command.parsedJson();
|
||||
|
||||
final String batchReference = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.BATCH_REFERENCE, element);
|
||||
if (this.batchRepository.existsByBatchReferenceIgnoreCase(batchReference)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.batch.duplicate.reference",
|
||||
"An inward clearing batch with reference " + batchReference + " already exists.", batchReference);
|
||||
}
|
||||
final LocalDate sessionDate = this.fromApiJsonHelper.extractLocalDateNamed(CtsClearingApiConstants.SESSION_DATE, element);
|
||||
final String presentmentType = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTMENT_TYPE, element);
|
||||
final String sourceFile = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.SOURCE_FILE, element);
|
||||
final String presentingBankCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BANK_CODE,
|
||||
element);
|
||||
final String presentingBankName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BANK_NAME,
|
||||
element);
|
||||
final Long officeId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.OFFICE_ID, element);
|
||||
final Long receivedBy = this.context.authenticatedUser().getId();
|
||||
|
||||
final InwardClearingBatch batch = InwardClearingBatch.receive(batchReference, sessionDate, presentmentType, sourceFile,
|
||||
presentingBankCode, presentingBankName, officeId, receivedBy);
|
||||
final InwardClearingBatch savedBatch = this.batchRepositoryWrapper.saveAndFlush(batch);
|
||||
|
||||
final Locale locale = localeOrDefault(element);
|
||||
final String dateFormat = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DATE_FORMAT, element);
|
||||
final JsonArray items = this.fromApiJsonHelper.extractJsonArrayNamed(CtsClearingApiConstants.ITEMS, element);
|
||||
|
||||
// RBI CTS return TAT: T+1 from the clearing session date, shared by every item received in this batch
|
||||
final LocalDate returnDeadline = sessionDate == null ? null : sessionDate.plusDays(1);
|
||||
|
||||
final List<InwardClearingItem> toSave = new ArrayList<>();
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
final JsonElement itemEl = items.get(i);
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, itemEl);
|
||||
final String micrCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.MICR_CODE, itemEl);
|
||||
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.IFSC, itemEl);
|
||||
final String payeeName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PAYEE_NAME, itemEl);
|
||||
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, itemEl, locale);
|
||||
final String amountInWords = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.AMOUNT_IN_WORDS, itemEl);
|
||||
final LocalDate instrumentDate = extractItemDate(CtsClearingApiConstants.INSTRUMENT_DATE, itemEl, dateFormat, locale);
|
||||
final LocalDate presentedDate = extractItemDate(CtsClearingApiConstants.PRESENTED_DATE, itemEl, dateFormat, locale);
|
||||
final String accountType = upperOrNull(this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ACCOUNT_TYPE, itemEl));
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.ACCOUNT_ID, itemEl);
|
||||
final Long chequeLeafId = resolveLeafId(chequeNumber);
|
||||
|
||||
final InwardClearingItem item = InwardClearingItem.receive(savedBatch, chequeLeafId, accountType, accountId, chequeNumber,
|
||||
micrCode, ifsc, payeeName, amount, amountInWords, instrumentDate, presentedDate);
|
||||
// scanned cheque images + IQA outcome received with the item (spec section 5)
|
||||
item.captureImages(this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.FRONT_IMAGE_REF, itemEl),
|
||||
this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.BACK_IMAGE_REF, itemEl),
|
||||
this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.IQA_STATUS, itemEl),
|
||||
this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.IQA_DETAIL, itemEl));
|
||||
item.recordInwardMeta(this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ORIGINAL_SERIAL_NUMBER, itemEl),
|
||||
this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PRESENTING_BRANCH_CODE, itemEl), returnDeadline);
|
||||
item.recordSourceDetails(this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CTS_REFERENCE_NUMBER, itemEl),
|
||||
this.fromApiJsonHelper.extractBooleanNamed(CtsClearingApiConstants.STOP_PAYMENT_FLAG, itemEl),
|
||||
extractItemDate(CtsClearingApiConstants.DEPOSIT_DATE, itemEl, dateFormat, locale));
|
||||
toSave.add(item);
|
||||
totalAmount = totalAmount.add(amount == null ? BigDecimal.ZERO : amount);
|
||||
}
|
||||
this.itemRepository.saveAll(toSave);
|
||||
savedBatch.setTotals(toSave.size(), totalAmount);
|
||||
this.batchRepositoryWrapper.saveAndFlush(savedBatch);
|
||||
log.info("Received inward clearing batch {} ({}) with {} items totalling {}", savedBatch.getId(), batchReference, toSave.size(),
|
||||
totalAmount);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(savedBatch.getId()).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult validateBatch(final Long batchId, final JsonCommand command) {
|
||||
final Long id = this.validationService.validate(batchId);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).build();
|
||||
}
|
||||
|
||||
// ============================= Inward item decisions =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult passItem(final Long itemId, final JsonCommand command) {
|
||||
this.validator.validateForPassItem(command.json());
|
||||
final InwardClearingItem item = this.itemRepositoryWrapper.findOneWithNotFoundDetection(itemId);
|
||||
assertDecidable(item);
|
||||
final Long userId = this.context.authenticatedUser().getId();
|
||||
final String note = command.stringValueOfParameterNamedAllowingNull(CtsClearingApiConstants.NOTE);
|
||||
final BigDecimal amount = item.getAmount() == null ? BigDecimal.ZERO : item.getAmount();
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
|
||||
// debit the drawn-on customer account for the cheque amount, consuming any hold placed at validation
|
||||
final AccountDebit debit = debitCustomerAccount(item, amount, today);
|
||||
|
||||
// move the money DR customer-control-GL / CR CTS inward suspense (best effort — skipped if GL unmapped)
|
||||
final Long controlGlId = this.glResolver.resolveControlGlAccountId(item.getAccountType(), item.getAccountId());
|
||||
final Long suspenseGlId = this.glResolver.resolveClearingGlAccountId(CTS_INWARD_SUSPENSE);
|
||||
if (controlGlId == null || suspenseGlId == null) {
|
||||
log.warn("Inward clearing item {} passed but GL posting skipped - missing control GL ({}) or suspense GL ({}).", itemId,
|
||||
controlGlId, suspenseGlId);
|
||||
} else {
|
||||
this.clearingJournalService.postJournalEntry(debit.officeId, debit.currencyCode, today, controlGlId, suspenseGlId, amount,
|
||||
"CTS inward clearing pass - item #" + itemId, item.getAccountId());
|
||||
}
|
||||
|
||||
markLeaf(item, true, "CTS cleared", null, userId);
|
||||
item.pass(note, userId);
|
||||
item.clearHold();
|
||||
this.itemRepositoryWrapper.saveAndFlush(item);
|
||||
maybeMarkBatchDecisioned(item.getBatch());
|
||||
log.info("Passed inward clearing item {} for {} account {} amount {}", itemId, item.getAccountType(), item.getAccountId(), amount);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(itemId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult returnItem(final Long itemId, final JsonCommand command) {
|
||||
this.validator.validateForReturnItem(command.json());
|
||||
final InwardClearingItem item = this.itemRepositoryWrapper.findOneWithNotFoundDetection(itemId);
|
||||
assertDecidable(item);
|
||||
final Long userId = this.context.authenticatedUser().getId();
|
||||
|
||||
final String returnReasonCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.RETURN_REASON_CODE,
|
||||
command.parsedJson());
|
||||
final ClearingReturnReasonCode reasonCode = this.returnReasonCodeRepository.findByCodeIgnoreCase(returnReasonCode)
|
||||
.orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.clearing.return.reason.code.invalid",
|
||||
"Clearing return reason code " + returnReasonCode + " is not configured.", returnReasonCode));
|
||||
final String reason = command.stringValueOfParameterNamedAllowingNull(CtsClearingApiConstants.REASON);
|
||||
final String effectiveReason = reason != null && !reason.isBlank() ? reason : reasonCode.getDescription();
|
||||
|
||||
// a return moves no money — just release any hold reserved at validation
|
||||
releaseHoldOnly(item);
|
||||
|
||||
markLeaf(item, false, null, effectiveReason, userId);
|
||||
item.returnItem(returnReasonCode, reason, userId);
|
||||
item.clearHold();
|
||||
this.itemRepositoryWrapper.saveAndFlush(item);
|
||||
|
||||
final String recipient = String.valueOf(item.getAccountId());
|
||||
final String message = "Cheque " + item.getChequeNumber() + " for " + item.getAmount() + " returned unpaid. Reason: "
|
||||
+ effectiveReason;
|
||||
this.notificationService.notifyCustomer("SMS", recipient, "CTS_RETURN", "CTS", "INWARD_CLEARING_ITEM", itemId, message);
|
||||
|
||||
maybeMarkBatchDecisioned(item.getBatch());
|
||||
log.info("Returned inward clearing item {} (reason {})", itemId, returnReasonCode);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(itemId).build();
|
||||
}
|
||||
|
||||
// ============================= Outward presentments =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createOutward(final JsonCommand command) {
|
||||
this.validator.validateForCreateOutward(command.json());
|
||||
final JsonElement element = command.parsedJson();
|
||||
final Locale locale = localeOrDefault(element);
|
||||
|
||||
final String accountType = upperOrNull(
|
||||
this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DEPOSITING_ACCOUNT_TYPE, element));
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.DEPOSITING_ACCOUNT_ID, element);
|
||||
final String drawerBankMicr = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_BANK_MICR, element);
|
||||
final String drawerIfsc = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_IFSC, element);
|
||||
final String drawerBankName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_BANK_NAME, element);
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, element);
|
||||
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, element, locale);
|
||||
final String payeeName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PAYEE_NAME, element);
|
||||
final LocalDate depositedDate = this.fromApiJsonHelper.extractLocalDateNamed(CtsClearingApiConstants.DEPOSITED_DATE, element);
|
||||
// Instrument date is not user-entered — a cheque is always captured the day it's presented over the counter.
|
||||
final LocalDate instrumentDate = DateUtils.getBusinessLocalDate();
|
||||
final Long createdBy = this.context.authenticatedUser().getId();
|
||||
|
||||
// captured on the UI form — not hardcoded — so the CTS outward file reflects what was actually keyed in
|
||||
final Boolean stopPaymentFlag = this.fromApiJsonHelper.extractBooleanNamed(CtsClearingApiConstants.STOP_PAYMENT_FLAG, element);
|
||||
final String frontImageRef = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.FRONT_IMAGE_REF, element);
|
||||
final String backImageRef = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.BACK_IMAGE_REF, element);
|
||||
final String iqaStatus = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.IQA_STATUS, element);
|
||||
final String iqaDetail = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.IQA_DETAIL, element);
|
||||
final String drawerAccountNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DRAWER_ACCOUNT_NUMBER,
|
||||
element);
|
||||
final String gridZoneCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.GRID_ZONE_CODE, element);
|
||||
final String gridZoneName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.GRID_ZONE_NAME, element);
|
||||
final Long officeId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.OFFICE_ID, element);
|
||||
|
||||
final OutwardClearingPresentment presentment = OutwardClearingPresentment.capture(accountType, accountId, drawerBankMicr,
|
||||
drawerIfsc, drawerBankName, chequeNumber, amount, payeeName, depositedDate, instrumentDate, officeId, createdBy,
|
||||
stopPaymentFlag, frontImageRef, backImageRef, iqaStatus, iqaDetail, drawerAccountNumber, gridZoneCode, gridZoneName);
|
||||
final OutwardClearingPresentment saved = this.outwardRepositoryWrapper.saveAndFlush(presentment);
|
||||
log.info("Captured outward clearing presentment {} (cheque {} amount {})", saved.getId(), chequeNumber, amount);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an outward cheque to the clearing house: bundles it into the real CTS-2010 file (batchHeader +
|
||||
* instrument + batchFooter, images embedded), submits it to the real, separate NPCI clearing-house
|
||||
* service (encrypted with NPCI's public key, signed with this bank's own private key — see
|
||||
* {@link NpciClearingClient}), and marks the presentment SENT_TO_CLEARING_HOUSE only once NPCI has
|
||||
* actually accepted it. If NPCI is unreachable or rejects the submission, this throws and the whole
|
||||
* transaction rolls back — nothing is ever marked "sent" without a real transmission.
|
||||
*/
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult sendOutward(final Long presentmentId, final JsonCommand command) {
|
||||
this.validator.validateNoBody(command.json());
|
||||
final OutwardClearingPresentment presentment = this.outwardRepositoryWrapper.findOneWithNotFoundDetection(presentmentId);
|
||||
assertImagesUploaded(presentment);
|
||||
sendOutwardBatchInternal(java.util.List.of(presentment), "CTS-OUT-" + DateUtils.getBusinessLocalDate() + "-" + presentmentId);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(presentmentId).build();
|
||||
}
|
||||
|
||||
/** A cheque cannot go to NPCI without a real front+back image — the CTS file has nothing to embed otherwise. */
|
||||
private void assertImagesUploaded(final OutwardClearingPresentment p) {
|
||||
if (!hasImage(p.getFrontImageStore()) || !hasImage(p.getBackImageStore())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.outward.clearing.presentment.images.missing",
|
||||
"Outward clearing presentment " + p.getId() + " cannot be sent to NPCI without both front and back cheque images.",
|
||||
p.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasImage(final String store) {
|
||||
return store != null && !store.isBlank() && store.contains("|");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends every still-CAPTURED outward presentment to NPCI as ONE combined CTS-2010 batch file (one
|
||||
* batchHeader shared by all instruments), instead of one file per cheque. This is what a real bank's
|
||||
* clearing-session batch job does — bundle everything presented since the last session into a single
|
||||
* outward file. Intended to be called either from the manual "Send Pending Batch" action or from the
|
||||
* (currently disabled) scheduled clearing-session job. A no-op (no exception) when nothing is pending, so
|
||||
* a scheduled run with nothing to send is not treated as a failure.
|
||||
*/
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult sendPendingOutwardBatch(final JsonCommand command) {
|
||||
final List<OutwardClearingPresentment> allPending = this.outwardRepository
|
||||
.findByStatusOrderByIdAsc(OutwardClearingPresentmentStatus.CAPTURED.name());
|
||||
if (allPending.isEmpty()) {
|
||||
log.info("Send-pending-batch run found no CAPTURED outward presentments - nothing to send.");
|
||||
return new CommandProcessingResultBuilder().withCommandId(command == null ? null : command.commandId()).build();
|
||||
}
|
||||
|
||||
// a cheque without both images stays CAPTURED and is skipped rather than blocking the whole batch —
|
||||
// important for the unattended scheduled job, where one incomplete cheque shouldn't hold up the rest
|
||||
final List<OutwardClearingPresentment> pending = new ArrayList<>();
|
||||
final List<Long> skipped = new ArrayList<>();
|
||||
for (final OutwardClearingPresentment p : allPending) {
|
||||
if (hasImage(p.getFrontImageStore()) && hasImage(p.getBackImageStore())) {
|
||||
pending.add(p);
|
||||
} else {
|
||||
skipped.add(p.getId());
|
||||
}
|
||||
}
|
||||
if (!skipped.isEmpty()) {
|
||||
log.warn("Send-pending-batch skipped {} outward presentment(s) missing front/back image: {}", skipped.size(), skipped);
|
||||
}
|
||||
if (pending.isEmpty()) {
|
||||
log.info("Send-pending-batch run found only image-incomplete outward presentments - nothing sent.");
|
||||
return new CommandProcessingResultBuilder().withCommandId(command == null ? null : command.commandId()).build();
|
||||
}
|
||||
|
||||
final long minId = pending.stream().mapToLong(OutwardClearingPresentment::getId).min().orElseThrow();
|
||||
final String batchReference = "CTS-OUT-BATCH-" + DateUtils.getBusinessLocalDate() + "-" + minId;
|
||||
sendOutwardBatchInternal(pending, batchReference);
|
||||
log.info("Sent pending outward batch {} to NPCI ({} cheques, {} skipped for missing images)", batchReference, pending.size(),
|
||||
skipped.size());
|
||||
return new CommandProcessingResultBuilder().withCommandId(command == null ? null : command.commandId())
|
||||
.withEntityId((long) pending.size()).build();
|
||||
}
|
||||
|
||||
private void sendOutwardBatchInternal(final List<OutwardClearingPresentment> items, final String batchReference) {
|
||||
final String ctsFile = buildCtsFile(batchReference, items);
|
||||
final String npciResponseJson = this.npciClearingClient.submitOutwardBatch(ctsFile);
|
||||
for (final OutwardClearingPresentment p : items) {
|
||||
p.send(batchReference, ctsFile);
|
||||
p.recordNpciResponse(npciResponseJson);
|
||||
}
|
||||
this.outwardRepository.saveAll(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full CTS-2010 outward clearing file for one or more cheques: a batch header (session, grid,
|
||||
* totals), one instrument record per cheque (MICR, transaction code, drawee bank + IFSC, amount in figures
|
||||
* & words, image references, IQA), and a batch footer with a record count and CRC32 checksum over
|
||||
* every instrument. Values captured on each cheque are used as-is; the rest are derived.
|
||||
*/
|
||||
private String buildCtsFile(final String batchReference, final List<OutwardClearingPresentment> items) {
|
||||
final java.time.LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
|
||||
final JsonArray instruments = new JsonArray();
|
||||
java.math.BigDecimal totalAmount = java.math.BigDecimal.ZERO;
|
||||
int serial = 1;
|
||||
for (final OutwardClearingPresentment p : items) {
|
||||
final JsonObject instrument = buildInstrument(p, serial, today);
|
||||
instruments.add(instrument);
|
||||
totalAmount = totalAmount.add(p.getAmount() == null ? java.math.BigDecimal.ZERO : p.getAmount());
|
||||
serial++;
|
||||
}
|
||||
|
||||
// grid zone: shared across the batch — taken from the first cheque that specified one, else default
|
||||
final OutwardClearingPresentment zoneSource = items.stream()
|
||||
.filter(p -> p.getGridZoneCode() != null && !p.getGridZoneCode().isBlank()).findFirst().orElse(null);
|
||||
|
||||
// ----- header -----
|
||||
final JsonObject header = new JsonObject();
|
||||
header.addProperty("clearingHouseCode", "NPCI-CTS");
|
||||
header.addProperty("gridZoneCode", zoneSource != null ? zoneSource.getGridZoneCode() : "NG");
|
||||
header.addProperty("gridZoneName", zoneSource != null ? zoneSource.getGridZoneName() : "National Grid");
|
||||
header.addProperty("sessionId", "SESS-" + today.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE) + "-01");
|
||||
header.addProperty("sessionType", "OUTWARD");
|
||||
header.addProperty("batchDate", today.toString());
|
||||
header.addProperty("batchReference", batchReference);
|
||||
header.addProperty("fileSequenceNumber", batchReference);
|
||||
header.addProperty("fileGenerationTimestamp", DateUtils.getOffsetDateTimeOfTenant().toString());
|
||||
header.addProperty("totalInstrumentCount", items.size());
|
||||
header.addProperty("totalAmount", totalAmount);
|
||||
header.addProperty("currency", "INR");
|
||||
|
||||
// ----- footer (record count + control total + CRC32 checksum over every instrument) -----
|
||||
final java.util.zip.CRC32 crc = new java.util.zip.CRC32();
|
||||
crc.update(instruments.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
final JsonObject footer = new JsonObject();
|
||||
footer.addProperty("recordCount", items.size());
|
||||
footer.addProperty("controlTotalAmount", totalAmount);
|
||||
footer.addProperty("endOfFileMarker", "EOF-CTS-BATCH");
|
||||
footer.addProperty("checksum", "CRC32:" + Long.toHexString(crc.getValue()));
|
||||
|
||||
final JsonObject file = new JsonObject();
|
||||
file.add("batchHeader", header);
|
||||
file.add("instruments", instruments);
|
||||
file.add("batchFooter", footer);
|
||||
return file.toString();
|
||||
}
|
||||
|
||||
/** Builds one instrument (cheque) record within a CTS outward file. */
|
||||
private JsonObject buildInstrument(final OutwardClearingPresentment p, final int serial, final java.time.LocalDate today) {
|
||||
final java.math.BigDecimal amount = p.getAmount() == null ? java.math.BigDecimal.ZERO : p.getAmount();
|
||||
final boolean current = "CURRENT".equalsIgnoreCase(p.getDepositingAccountType());
|
||||
final String instrumentType = current ? "11" : "10";
|
||||
final String instrumentTypeDesc = current ? "Current account cheque" : "Savings account cheque";
|
||||
final String ctsRef = "CTS" + today.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE) + String.format("%06d", p.getId());
|
||||
|
||||
final JsonObject instrument = new JsonObject();
|
||||
instrument.addProperty("serialNumber", String.format("%03d", serial));
|
||||
instrument.addProperty("ctsReferenceNumber", ctsRef);
|
||||
instrument.addProperty("micrCode", p.getDrawerBankMicr());
|
||||
instrument.addProperty("instrumentType", instrumentType);
|
||||
instrument.addProperty("instrumentTypeDescription", instrumentTypeDesc);
|
||||
instrument.addProperty("transactionCode", instrumentType);
|
||||
instrument.addProperty("instrumentNumber", p.getChequeNumber());
|
||||
instrument.addProperty("instrumentDate", p.getInstrumentDate() == null ? null : p.getInstrumentDate().toString());
|
||||
instrument.addProperty("draweeBankName", p.getDrawerBankName());
|
||||
instrument.addProperty("draweeBranchIfsc", p.getDrawerIfsc());
|
||||
instrument.addProperty("drawerAccountNumber", maskAccountNumber(p.getDrawerAccountNumber()));
|
||||
instrument.addProperty("payeeName", p.getPayeeName());
|
||||
instrument.addProperty("amount", amount);
|
||||
instrument.addProperty("amountInWords", amountInWords(amount) + " Only");
|
||||
|
||||
final String frontRef = p.getFrontImageRef() != null && !p.getFrontImageRef().isBlank() ? p.getFrontImageRef()
|
||||
: "IMG-F-" + p.getId();
|
||||
final String backRef = p.getBackImageRef() != null && !p.getBackImageRef().isBlank() ? p.getBackImageRef() : "IMG-B-" + p.getId();
|
||||
final JsonObject images = new JsonObject();
|
||||
images.add("frontImage", buildImageBlock(frontRef, p.getFrontImageStore()));
|
||||
images.add("backImage", buildImageBlock(backRef, p.getBackImageStore()));
|
||||
instrument.add("images", images);
|
||||
|
||||
// captured on the UI form at deposit time (or the manual "Fill Dummy Data" helper) — not hardcoded
|
||||
final String iqaStatus = p.getIqaStatus() == null || p.getIqaStatus().isBlank() ? "NA" : p.getIqaStatus();
|
||||
instrument.addProperty("iqaStatus", iqaStatus);
|
||||
final String checkResult = "FAIL".equalsIgnoreCase(iqaStatus) ? "FAIL" : "PASS";
|
||||
final JsonObject iqa = new JsonObject();
|
||||
iqa.addProperty("skew", checkResult);
|
||||
iqa.addProperty("legibility", checkResult);
|
||||
iqa.addProperty("completeness", checkResult);
|
||||
iqa.addProperty("noise", checkResult);
|
||||
instrument.add("iqaChecks", iqa);
|
||||
if (p.getIqaDetail() != null && !p.getIqaDetail().isBlank()) {
|
||||
instrument.addProperty("iqaDetail", p.getIqaDetail());
|
||||
}
|
||||
|
||||
instrument.addProperty("stopPaymentFlag", p.isStopPaymentFlag());
|
||||
final String depositBranchCode = p.getOfficeId() == null ? "BR000" : "BR" + String.format("%03d", p.getOfficeId());
|
||||
instrument.addProperty("depositBranchCode", depositBranchCode);
|
||||
instrument.addProperty("depositAccountNumber", String.valueOf(p.getDepositingAccountId()));
|
||||
instrument.addProperty("depositDate", p.getDepositedDate() == null ? null : p.getDepositedDate().toString());
|
||||
return instrument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds one image block (front or back) for the CTS wire file. When a real file was uploaded (via
|
||||
* {@link ContentStoreService}, {@code store} is "mimeType|storagePath"), embeds the actual base64 bytes
|
||||
* plus size/checksum — this is what genuinely travels to NPCI. When nothing was uploaded, returns a bare
|
||||
* reference-only placeholder instead of fabricating image bytes that don't exist.
|
||||
*/
|
||||
private JsonObject buildImageBlock(final String imageRef, final String store) {
|
||||
final JsonObject image = new JsonObject();
|
||||
image.addProperty("imageRef", imageRef);
|
||||
if (store == null || store.isBlank() || !store.contains("|")) {
|
||||
image.addProperty("format", "NA");
|
||||
image.addProperty("encoding", "none");
|
||||
image.addProperty("note", "No image uploaded for this cheque");
|
||||
return image;
|
||||
}
|
||||
final String[] parts = store.split("\\|", 2);
|
||||
final String mimeType = parts[0];
|
||||
final String storagePath = parts[1];
|
||||
try {
|
||||
final byte[] bytes = this.contentStoreService.download(storagePath).readAllBytes();
|
||||
image.addProperty("format", mimeType);
|
||||
image.addProperty("sizeBytes", bytes.length);
|
||||
image.addProperty("checksum", "MD5:" + md5Hex(bytes));
|
||||
image.addProperty("encoding", "base64");
|
||||
image.addProperty("data", java.util.Base64.getEncoder().encodeToString(bytes));
|
||||
} catch (final Exception e) {
|
||||
image.addProperty("format", mimeType);
|
||||
image.addProperty("encoding", "none");
|
||||
image.addProperty("note", "Stored image could not be read: " + e.getMessage());
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private String md5Hex(final byte[] bytes) {
|
||||
try {
|
||||
final java.security.MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
|
||||
final byte[] hash = digest.digest(bytes);
|
||||
final StringBuilder hex = new StringBuilder();
|
||||
for (final byte b : hash) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
} catch (final java.security.NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("MD5 is not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** SHA-256 hex digest of the given content, used as a visible integrity fingerprint in the CTS header. */
|
||||
private String sha256Hex(final String content) {
|
||||
try {
|
||||
final java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
final byte[] hash = digest.digest(content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
final StringBuilder hex = new StringBuilder();
|
||||
for (final byte b : hash) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
} catch (final java.security.NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 is not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Masks all but the last 4 characters of an account number (matches the CTS file convention of not exposing full numbers). */
|
||||
private String maskAccountNumber(final String accountNumber) {
|
||||
if (accountNumber == null || accountNumber.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
final String trimmed = accountNumber.trim();
|
||||
if (trimmed.length() <= 4) {
|
||||
return trimmed;
|
||||
}
|
||||
final String last4 = trimmed.substring(trimmed.length() - 4);
|
||||
return "X".repeat(trimmed.length() - 4) + last4;
|
||||
}
|
||||
|
||||
/** Minimal Indian-system number-to-words for whole rupees (paise ignored) used in the CTS file. */
|
||||
private String amountInWords(final java.math.BigDecimal amount) {
|
||||
long n = amount.setScale(0, java.math.RoundingMode.DOWN).longValueExact();
|
||||
if (n == 0) {
|
||||
return "Zero Rupees";
|
||||
}
|
||||
final String[] ones = { "", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve",
|
||||
"Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" };
|
||||
final String[] tens = { "", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" };
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final long crore = n / 10000000;
|
||||
n %= 10000000;
|
||||
final long lakh = n / 100000;
|
||||
n %= 100000;
|
||||
final long thousand = n / 1000;
|
||||
n %= 1000;
|
||||
final long hundred = n / 100;
|
||||
final long rest = n % 100;
|
||||
if (crore > 0) {
|
||||
sb.append(twoDigits(crore, ones, tens)).append(" Crore ");
|
||||
}
|
||||
if (lakh > 0) {
|
||||
sb.append(twoDigits(lakh, ones, tens)).append(" Lakh ");
|
||||
}
|
||||
if (thousand > 0) {
|
||||
sb.append(twoDigits(thousand, ones, tens)).append(" Thousand ");
|
||||
}
|
||||
if (hundred > 0) {
|
||||
sb.append(ones[(int) hundred]).append(" Hundred ");
|
||||
}
|
||||
if (rest > 0) {
|
||||
sb.append(twoDigits(rest, ones, tens)).append(" ");
|
||||
}
|
||||
return sb.append("Rupees").toString().replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private String twoDigits(final long value, final String[] ones, final String[] tens) {
|
||||
if (value < 20) {
|
||||
return ones[(int) value];
|
||||
}
|
||||
return (tens[(int) (value / 10)] + " " + ones[(int) (value % 10)]).trim();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult settleOutward(final Long presentmentId, final JsonCommand command) {
|
||||
this.validator.validateNoBody(command.json());
|
||||
final OutwardClearingPresentment presentment = this.outwardRepositoryWrapper.findOneWithNotFoundDetection(presentmentId);
|
||||
presentment.settle();
|
||||
this.outwardRepositoryWrapper.saveAndFlush(presentment);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(presentmentId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult dishonourOutward(final Long presentmentId, final JsonCommand command) {
|
||||
this.validator.validateForDishonourOutward(command.json());
|
||||
final OutwardClearingPresentment presentment = this.outwardRepositoryWrapper.findOneWithNotFoundDetection(presentmentId);
|
||||
final String reasonCode = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.DISHONOUR_REASON_CODE,
|
||||
command.parsedJson());
|
||||
this.returnReasonCodeRepository.findByCodeIgnoreCase(reasonCode)
|
||||
.orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.clearing.return.reason.code.invalid",
|
||||
"Clearing return reason code " + reasonCode + " is not configured.", reasonCode));
|
||||
presentment.dishonour(reasonCode);
|
||||
this.outwardRepositoryWrapper.saveAndFlush(presentment);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(presentmentId).build();
|
||||
}
|
||||
|
||||
// ============================= Positive Pay (RBI PPS) =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createPositivePay(final JsonCommand command) {
|
||||
this.validator.validateForCreatePositivePay(command.json());
|
||||
final JsonElement element = command.parsedJson();
|
||||
final Locale locale = localeOrDefault(element);
|
||||
final String accountType = upperOrNull(this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.ACCOUNT_TYPE, element));
|
||||
final Long accountId = this.fromApiJsonHelper.extractLongNamed(CtsClearingApiConstants.ACCOUNT_ID, element);
|
||||
final String chequeNumber = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHEQUE_NUMBER, element);
|
||||
final LocalDate instrumentDate = this.fromApiJsonHelper.extractLocalDateNamed(CtsClearingApiConstants.INSTRUMENT_DATE, element);
|
||||
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(CtsClearingApiConstants.AMOUNT, element, locale);
|
||||
final String payeeName = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.PAYEE_NAME, element);
|
||||
final String channel = this.fromApiJsonHelper.extractStringNamed(CtsClearingApiConstants.CHANNEL, element);
|
||||
final Long userId = this.context.authenticatedUser().getId();
|
||||
|
||||
final PositivePayRegistration registration = PositivePayRegistration.register(accountType, accountId, chequeNumber, instrumentDate,
|
||||
amount, payeeName, channel, userId);
|
||||
final PositivePayRegistration saved = this.positivePayRepositoryWrapper.saveAndFlush(registration);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult cancelPositivePay(final Long registrationId, final JsonCommand command) {
|
||||
this.validator.validateForCancelPositivePay(command.json());
|
||||
final PositivePayRegistration registration = this.positivePayRepositoryWrapper.findOneWithNotFoundDetection(registrationId);
|
||||
final String reason = command.stringValueOfParameterNamedAllowingNull(CtsClearingApiConstants.REASON);
|
||||
registration.cancel(reason, this.context.authenticatedUser().getId());
|
||||
this.positivePayRepositoryWrapper.saveAndFlush(registration);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(registrationId).build();
|
||||
}
|
||||
|
||||
// ============================= Cheque images (plain, no command pipeline) =============================
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void recordInwardItemImage(final Long itemId, final String side, final String mimeTypeAndPath) {
|
||||
final InwardClearingItem item = this.itemRepositoryWrapper.findOneWithNotFoundDetection(itemId);
|
||||
item.recordImageStore(side, mimeTypeAndPath);
|
||||
this.itemRepositoryWrapper.saveAndFlush(item);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void recordOutwardPresentmentImage(final Long presentmentId, final String side, final String mimeTypeAndPath) {
|
||||
final OutwardClearingPresentment presentment = this.outwardRepositoryWrapper.findOneWithNotFoundDetection(presentmentId);
|
||||
presentment.recordImageStore(side, mimeTypeAndPath);
|
||||
this.outwardRepositoryWrapper.saveAndFlush(presentment);
|
||||
}
|
||||
|
||||
// ============================= Helpers =============================
|
||||
|
||||
private void assertDecidable(final InwardClearingItem item) {
|
||||
if (!item.isValidated()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.item.not.validated",
|
||||
"Inward clearing item " + item.getId() + " has not been validated.", item.getId());
|
||||
}
|
||||
if (!item.isPending()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.inward.clearing.item.already.decided",
|
||||
"Inward clearing item " + item.getId() + " has already been decided.", item.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debits the drawn-on account for the cheque amount using the direct-ledger pattern (a transaction row plus a
|
||||
* balance-field update), first releasing any hold that validation placed so the reserved funds become the
|
||||
* debit rather than being counted twice.
|
||||
*/
|
||||
private AccountDebit debitCustomerAccount(final InwardClearingItem item, final BigDecimal amount, final LocalDate today) {
|
||||
if (isSavings(item)) {
|
||||
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(item.getAccountId());
|
||||
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
|
||||
if (item.isHoldPlaced() && item.getHoldAmount() != null) {
|
||||
account.releaseOnHoldAmount(item.getHoldAmount());
|
||||
}
|
||||
final Office office = account.office();
|
||||
final Money amountMoney = Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = SavingsAccountTransaction.withdrawal(account, office, null, today, amountMoney, null);
|
||||
account.addTransaction(txn);
|
||||
this.savingsAccountTransactionRepository.saveAndFlush(txn);
|
||||
account.recalculateBalancesAfterFacilityCharges(today);
|
||||
this.savingsAccountRepositoryWrapper.saveAndFlush(account);
|
||||
return new AccountDebit(office.getId(), account.getCurrency().getCode());
|
||||
}
|
||||
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(item.getAccountId());
|
||||
if (item.isHoldPlaced() && item.getHoldAmount() != null) {
|
||||
account.setOnHoldFundsDerived(safe(account.getOnHoldFundsDerived()).subtract(item.getHoldAmount()).max(BigDecimal.ZERO));
|
||||
}
|
||||
final Office office = account.getOffice();
|
||||
final BigDecimal newBalance = safe(account.getAccountBalanceDerived()).subtract(amount);
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, today, null);
|
||||
txn.setRunningBalanceDerived(newBalance);
|
||||
this.currentAccountTransactionRepository.saveAndFlush(txn);
|
||||
account.setAccountBalanceDerived(newBalance);
|
||||
this.currentAccountRepository.saveAndFlush(account);
|
||||
return new AccountDebit(office == null ? null : office.getId(), account.getCurrencyCode());
|
||||
}
|
||||
|
||||
/** Releases (without debiting) any hold placed at validation — used when an item is returned. */
|
||||
private void releaseHoldOnly(final InwardClearingItem item) {
|
||||
if (!item.isHoldPlaced() || item.getHoldAmount() == null || item.getHoldAmount().signum() <= 0) {
|
||||
return;
|
||||
}
|
||||
if (isSavings(item)) {
|
||||
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(item.getAccountId());
|
||||
account.releaseOnHoldAmount(item.getHoldAmount());
|
||||
this.savingsAccountRepositoryWrapper.saveAndFlush(account);
|
||||
} else {
|
||||
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(item.getAccountId());
|
||||
account.setOnHoldFundsDerived(safe(account.getOnHoldFundsDerived()).subtract(item.getHoldAmount()).max(BigDecimal.ZERO));
|
||||
this.currentAccountRepository.saveAndFlush(account);
|
||||
}
|
||||
}
|
||||
|
||||
/** Drives the linked cheque leaf's USED (pass) / RETURNED (return) status, but only when it is still ACTIVE. */
|
||||
private void markLeaf(final InwardClearingItem item, final boolean passed, final String usedReason, final String returnReason,
|
||||
final Long userId) {
|
||||
if (item.getChequeLeafId() == null) {
|
||||
return;
|
||||
}
|
||||
final ChequeLeaf leaf = this.chequeLeafRepository.findById(item.getChequeLeafId()).orElse(null);
|
||||
if (leaf == null || !ChequeLeafStatus.ACTIVE.name().equals(leaf.getStatus())) {
|
||||
return;
|
||||
}
|
||||
if (passed) {
|
||||
leaf.markUsed(usedReason, userId);
|
||||
} else {
|
||||
leaf.returned(returnReason, userId);
|
||||
}
|
||||
this.chequeLeafRepository.saveAndFlush(leaf);
|
||||
}
|
||||
|
||||
/** Once no item of the batch is left PENDING, advance the batch from VALIDATED to DECISIONED. */
|
||||
private void maybeMarkBatchDecisioned(final InwardClearingBatch batch) {
|
||||
if (!batch.isValidated()) {
|
||||
return;
|
||||
}
|
||||
final long pending = this.itemRepository.countByBatchIdAndDecision(batch.getId(), "PENDING");
|
||||
if (pending == 0) {
|
||||
batch.markDecisioned();
|
||||
this.batchRepositoryWrapper.saveAndFlush(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private Long resolveLeafId(final String chequeNumber) {
|
||||
if (chequeNumber == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final Long serial = Long.valueOf(chequeNumber.trim());
|
||||
final ChequeLeaf leaf = this.chequeLeafRepository.findByLeafSerialNo(serial).orElse(null);
|
||||
return leaf == null ? null : leaf.getId();
|
||||
} catch (final RuntimeException e) {
|
||||
// non-numeric cheque number, or more than one leaf shares this serial across series — leave unlinked
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate extractItemDate(final String parameter, final JsonElement itemEl, final String dateFormat, final Locale locale) {
|
||||
if (dateFormat == null || locale == null) {
|
||||
return null;
|
||||
}
|
||||
return this.fromApiJsonHelper.extractLocalDateNamed(parameter, itemEl, dateFormat, locale);
|
||||
}
|
||||
|
||||
private boolean isSavings(final InwardClearingItem item) {
|
||||
return SAVINGS.equalsIgnoreCase(item.getAccountType());
|
||||
}
|
||||
|
||||
/** The command's locale, defaulting to English so amount parsing never NPEs when the client omits it. */
|
||||
private Locale localeOrDefault(final JsonElement element) {
|
||||
final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject());
|
||||
return locale == null ? Locale.ENGLISH : locale;
|
||||
}
|
||||
|
||||
private String upperOrNull(final String value) {
|
||||
return value == null ? null : value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private BigDecimal safe(final BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
private static final class AccountDebit {
|
||||
|
||||
private final Long officeId;
|
||||
private final String currencyCode;
|
||||
|
||||
private AccountDebit(final Long officeId, final String currencyCode) {
|
||||
this.officeId = officeId;
|
||||
this.currencyCode = currencyCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user