feat(chequebook): add RETURNED leaf status for dishonoured/bounced cheques
Adds a maker-checker-gated "return" command on cheque books that flags presented-and-dishonoured leaves as terminal RETURNED (distinct from STOP_PAYMENT/drawer-initiated and LOST/custody-failure), following the same reason/actor/timestamp pattern already used for lost/cancelled/used leaves on m_cheque_leaf. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
04d65d857f
commit
3bb7fc8050
+8
@@ -207,6 +207,14 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder returnChequeBook(final Long chequeBookId) {
|
||||
this.actionName = "RETURN";
|
||||
this.entityName = "CHEQUEBOOK";
|
||||
this.entityId = chequeBookId;
|
||||
this.href = "/chequebooks/" + chequeBookId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder approveChequeBookPendingRequest(final Long pendingRequestId) {
|
||||
this.actionName = "APPROVE";
|
||||
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
|
||||
|
||||
+4
-2
@@ -99,10 +99,10 @@ public class ChequeBookApiResource {
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Run a command on a cheque book", description = "Supported commands: issue, reject, withdraw, "
|
||||
+ "stoppayment, revokestoppayment, reportlost, cancelleaf, markused.")
|
||||
+ "stoppayment, revokestoppayment, reportlost, cancelleaf, markused, return.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||
@QueryParam("command") @Parameter(description = "issue | reject | withdraw | stoppayment | revokestoppayment | "
|
||||
+ "reportlost | cancelleaf | markused") final String commandParam,
|
||||
+ "reportlost | cancelleaf | markused | return") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
final CommandWrapper request;
|
||||
@@ -122,6 +122,8 @@ public class ChequeBookApiResource {
|
||||
request = builder.cancelLeafChequeBook(id).build();
|
||||
} else if ("markused".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.markUsedChequeBook(id).build();
|
||||
} else if ("return".equalsIgnoreCase(commandParam)) {
|
||||
request = builder.returnChequeBook(id).build();
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
|
||||
+3
@@ -52,6 +52,9 @@ public class ChequeLeafData {
|
||||
private final String usedReason;
|
||||
private final Long usedBy;
|
||||
private final OffsetDateTime usedOn;
|
||||
private final String returnReason;
|
||||
private final Long returnedBy;
|
||||
private final OffsetDateTime returnedOn;
|
||||
private final String accountType;
|
||||
private final Long accountId;
|
||||
private final String accountNo;
|
||||
|
||||
+24
-4
@@ -40,10 +40,11 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
*
|
||||
* <p>
|
||||
* Legal transitions: (created)->ACTIVE (issued) or (created, no book)->LOST (unissued theft report);
|
||||
* ACTIVE<->STOP_PAYMENT (the one round-trip); ACTIVE->CANCELLED / LOST / WITHDRAWN / USED (all terminal).
|
||||
* A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating, since the
|
||||
* customer has typically already been issued a replacement book. USED is manually recorded by a branch,
|
||||
* never automatic — see {@link #markUsed(String, Long)}.
|
||||
* ACTIVE<->STOP_PAYMENT (the one round-trip); ACTIVE->CANCELLED / LOST / WITHDRAWN / USED / RETURNED (all
|
||||
* terminal). A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating,
|
||||
* since the customer has typically already been issued a replacement book; a RETURNED (dishonoured) leaf
|
||||
* likewise stays flagged rather than reverting to ACTIVE. USED is manually recorded by a branch, never
|
||||
* automatic — see {@link #markUsed(String, Long)}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_cheque_leaf")
|
||||
@@ -105,6 +106,16 @@ public class ChequeLeaf extends AbstractPersistableCustom<Long> {
|
||||
@Column(name = "used_on")
|
||||
private OffsetDateTime usedOn;
|
||||
|
||||
/** Set when a presented leaf is dishonoured/bounced (insufficient funds, signature mismatch, etc.). */
|
||||
@Column(name = "return_reason", length = 500)
|
||||
private String returnReason;
|
||||
|
||||
@Column(name = "returned_by")
|
||||
private Long returnedBy;
|
||||
|
||||
@Column(name = "returned_on")
|
||||
private OffsetDateTime returnedOn;
|
||||
|
||||
@Column(name = "created_on")
|
||||
private OffsetDateTime createdOn;
|
||||
|
||||
@@ -191,4 +202,13 @@ public class ChequeLeaf extends AbstractPersistableCustom<Long> {
|
||||
this.usedBy = markedBy;
|
||||
this.usedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
/** The leaf was presented for payment and dishonoured/bounced. Terminal — no revoke, matching LOST/CANCELLED. */
|
||||
public void returned(final String reason, final Long actorId) {
|
||||
assertActive();
|
||||
this.status = ChequeLeafStatus.RETURNED.name();
|
||||
this.returnReason = reason;
|
||||
this.returnedBy = actorId;
|
||||
this.returnedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -23,9 +23,11 @@ package org.apache.fineract.portfolio.chequebook.domain;
|
||||
* stolen); ACTIVE<->STOP_PAYMENT (the one round-trip, via stop-payment/revoke); ACTIVE->CANCELLED
|
||||
* (spoiled leaf voided); ACTIVE->LOST (reported lost/stolen post-issuance); ACTIVE->WITHDRAWN (account
|
||||
* closed); ACTIVE->USED (manually recorded once the branch sees the leaf clear on the bank/clearing
|
||||
* statement — this CBS has no clearing/presentment integration, so USED can never be set automatically).
|
||||
* LOST, CANCELLED, WITHDRAWN and USED are all terminal — a recovered "lost" cheque conventionally stays
|
||||
* hotlisted since the customer has typically already been issued a replacement book.
|
||||
* statement — this CBS has no clearing/presentment integration, so USED can never be set automatically);
|
||||
* ACTIVE->RETURNED (presented and dishonoured/bounced — insufficient funds, signature mismatch, etc.).
|
||||
* LOST, CANCELLED, WITHDRAWN, USED and RETURNED are all terminal — a recovered "lost" cheque conventionally
|
||||
* stays hotlisted since the customer has typically already been issued a replacement book, and a returned
|
||||
* leaf stays flagged rather than silently reverting to ACTIVE.
|
||||
*/
|
||||
public enum ChequeLeafStatus {
|
||||
|
||||
@@ -34,5 +36,6 @@ public enum ChequeLeafStatus {
|
||||
CANCELLED, //
|
||||
LOST, //
|
||||
WITHDRAWN, //
|
||||
USED; //
|
||||
USED, //
|
||||
RETURNED; //
|
||||
}
|
||||
|
||||
+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.chequebook.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.chequebook.service.ChequeBookWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "CHEQUEBOOK", action = "RETURN")
|
||||
@RequiredArgsConstructor
|
||||
public class ReturnChequeBookCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final ChequeBookWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.returnChequeBook(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+4
@@ -296,6 +296,7 @@ public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatform
|
||||
.lostReason(leaf.getLostReason()).lostReportedBy(leaf.getLostReportedBy()).lostReportedOn(leaf.getLostReportedOn())
|
||||
.cancelledReason(leaf.getCancelledReason()).cancelledBy(leaf.getCancelledBy()).cancelledOn(leaf.getCancelledOn())
|
||||
.usedReason(leaf.getUsedReason()).usedBy(leaf.getUsedBy()).usedOn(leaf.getUsedOn())
|
||||
.returnReason(leaf.getReturnReason()).returnedBy(leaf.getReturnedBy()).returnedOn(leaf.getReturnedOn())
|
||||
.accountType(leaf.getAccountType()).accountId(leaf.getAccountId()).accountNo(display.accountNo)
|
||||
.clientName(display.clientName).officeName(leaf.getOfficeName()).build();
|
||||
}
|
||||
@@ -448,6 +449,7 @@ public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatform
|
||||
+ "f.lost_reported_on as lost_reported_on, f.cancelled_reason as cancelled_reason, "
|
||||
+ "f.cancelled_by as cancelled_by, f.cancelled_on as cancelled_on, "
|
||||
+ "f.used_reason as used_reason, f.used_by as used_by, f.used_on as used_on, "
|
||||
+ "f.return_reason as return_reason, f.returned_by as returned_by, f.returned_on as returned_on, "
|
||||
+ "b.account_type as account_type, b.account_id as account_id, bo.name as book_office_name, "
|
||||
+ "lo.name as lot_office_name "
|
||||
+ "from m_cheque_leaf f left join m_cheque_book b on b.id = f.cheque_book_id left join m_office bo on bo.id = b.office_id "
|
||||
@@ -469,6 +471,8 @@ public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatform
|
||||
.cancelledReason(rs.getString("cancelled_reason")).cancelledBy((Long) rs.getObject("cancelled_by"))
|
||||
.cancelledOn(rs.getObject("cancelled_on", OffsetDateTime.class)).usedReason(rs.getString("used_reason"))
|
||||
.usedBy((Long) rs.getObject("used_by")).usedOn(rs.getObject("used_on", OffsetDateTime.class))
|
||||
.returnReason(rs.getString("return_reason")).returnedBy((Long) rs.getObject("returned_by"))
|
||||
.returnedOn(rs.getObject("returned_on", OffsetDateTime.class))
|
||||
.accountType(rs.getString("account_type")).accountId((Long) rs.getObject("account_id")).accountNo(null)
|
||||
.clientName(null).officeName(officeName).build();
|
||||
}
|
||||
|
||||
+3
@@ -61,6 +61,9 @@ public interface ChequeBookWritePlatformService {
|
||||
|
||||
CommandProcessingResult cancelLeaf(Long chequeBookId, JsonCommand command);
|
||||
|
||||
/** Flags leaves in range as RETURNED — presented and dishonoured/bounced. Terminal, like reportlost. */
|
||||
CommandProcessingResult returnChequeBook(Long chequeBookId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Manually records that leaves cleared, once a branch sees them on the bank/clearing statement — never
|
||||
* automatic, since this CBS has no clearing/presentment integration to detect it on its own.
|
||||
|
||||
+15
@@ -541,6 +541,21 @@ public class ChequeBookWritePlatformServiceImpl implements ChequeBookWritePlatfo
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult returnChequeBook(final Long chequeBookId, final JsonCommand command) {
|
||||
this.validator.validateForBookLeafAction(command.json(), true);
|
||||
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
|
||||
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
|
||||
final Long actorId = this.context.authenticatedUser().getId();
|
||||
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
|
||||
for (final ChequeLeaf leaf : leaves) {
|
||||
leaf.returned(reason, actorId);
|
||||
}
|
||||
this.chequeLeafRepository.saveAll(leaves);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult markLeafUsed(final Long chequeBookId, final JsonCommand command) {
|
||||
|
||||
@@ -408,4 +408,5 @@
|
||||
<include file="parts/0385_chequebook_pending_requests.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0386_enable_global_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0387_chequeleaf_serial_unique_per_lot.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0388_chequeleaf_returned_status.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
A RETURNED status for a cheque leaf — the leaf was presented for payment and dishonoured/bounced
|
||||
(insufficient funds, signature mismatch, etc.), as distinct from STOP_PAYMENT (drawer-initiated) or
|
||||
LOST (custody failure). Follows the same free-text reason pattern already used for lost_reason/
|
||||
cancelled_reason/used_reason on this table, rather than introducing a one-off code-value list.
|
||||
Terminal like LOST/CANCELLED/USED — no revoke, matching this table's existing conventions.
|
||||
-->
|
||||
|
||||
<changeSet id="0388-cheque-leaf-returned-columns" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_cheque_leaf" columnName="return_reason"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_cheque_leaf">
|
||||
<column name="return_reason" type="VARCHAR(500)"/>
|
||||
<column name="returned_by" type="BIGINT"/>
|
||||
<column name="returned_on" type="TIMESTAMP WITH TIME ZONE"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0388-permission" author="CBS" runOnChange="false">
|
||||
<sql>
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'RETURN_CHEQUEBOOK', 'CHEQUEBOOK', 'RETURN', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'RETURN_CHEQUEBOOK');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user