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;
|
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) {
|
public CommandWrapperBuilder approveChequeBookPendingRequest(final Long pendingRequestId) {
|
||||||
this.actionName = "APPROVE";
|
this.actionName = "APPROVE";
|
||||||
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
|
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
|
||||||
|
|||||||
+4
-2
@@ -99,10 +99,10 @@ public class ChequeBookApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Run a command on a cheque book", description = "Supported commands: issue, reject, withdraw, "
|
@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,
|
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
|
||||||
@QueryParam("command") @Parameter(description = "issue | reject | withdraw | stoppayment | revokestoppayment | "
|
@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) {
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||||
final CommandWrapper request;
|
final CommandWrapper request;
|
||||||
@@ -122,6 +122,8 @@ public class ChequeBookApiResource {
|
|||||||
request = builder.cancelLeafChequeBook(id).build();
|
request = builder.cancelLeafChequeBook(id).build();
|
||||||
} else if ("markused".equalsIgnoreCase(commandParam)) {
|
} else if ("markused".equalsIgnoreCase(commandParam)) {
|
||||||
request = builder.markUsedChequeBook(id).build();
|
request = builder.markUsedChequeBook(id).build();
|
||||||
|
} else if ("return".equalsIgnoreCase(commandParam)) {
|
||||||
|
request = builder.returnChequeBook(id).build();
|
||||||
} else {
|
} else {
|
||||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -52,6 +52,9 @@ public class ChequeLeafData {
|
|||||||
private final String usedReason;
|
private final String usedReason;
|
||||||
private final Long usedBy;
|
private final Long usedBy;
|
||||||
private final OffsetDateTime usedOn;
|
private final OffsetDateTime usedOn;
|
||||||
|
private final String returnReason;
|
||||||
|
private final Long returnedBy;
|
||||||
|
private final OffsetDateTime returnedOn;
|
||||||
private final String accountType;
|
private final String accountType;
|
||||||
private final Long accountId;
|
private final Long accountId;
|
||||||
private final String accountNo;
|
private final String accountNo;
|
||||||
|
|||||||
+24
-4
@@ -40,10 +40,11 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Legal transitions: (created)->ACTIVE (issued) or (created, no book)->LOST (unissued theft report);
|
* 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).
|
* ACTIVE<->STOP_PAYMENT (the one round-trip); ACTIVE->CANCELLED / LOST / WITHDRAWN / USED / RETURNED (all
|
||||||
* A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating, since the
|
* terminal). A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating,
|
||||||
* customer has typically already been issued a replacement book. USED is manually recorded by a branch,
|
* since the customer has typically already been issued a replacement book; a RETURNED (dishonoured) leaf
|
||||||
* never automatic — see {@link #markUsed(String, Long)}.
|
* likewise stays flagged rather than reverting to ACTIVE. USED is manually recorded by a branch, never
|
||||||
|
* automatic — see {@link #markUsed(String, Long)}.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_cheque_leaf")
|
@Table(name = "m_cheque_leaf")
|
||||||
@@ -105,6 +106,16 @@ public class ChequeLeaf extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "used_on")
|
@Column(name = "used_on")
|
||||||
private OffsetDateTime usedOn;
|
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")
|
@Column(name = "created_on")
|
||||||
private OffsetDateTime createdOn;
|
private OffsetDateTime createdOn;
|
||||||
|
|
||||||
@@ -191,4 +202,13 @@ public class ChequeLeaf extends AbstractPersistableCustom<Long> {
|
|||||||
this.usedBy = markedBy;
|
this.usedBy = markedBy;
|
||||||
this.usedOn = DateUtils.getOffsetDateTimeOfTenant();
|
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
|
* 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
|
* (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
|
* 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).
|
* 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
|
* ACTIVE->RETURNED (presented and dishonoured/bounced — insufficient funds, signature mismatch, etc.).
|
||||||
* hotlisted since the customer has typically already been issued a replacement book.
|
* 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 {
|
public enum ChequeLeafStatus {
|
||||||
|
|
||||||
@@ -34,5 +36,6 @@ public enum ChequeLeafStatus {
|
|||||||
CANCELLED, //
|
CANCELLED, //
|
||||||
LOST, //
|
LOST, //
|
||||||
WITHDRAWN, //
|
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())
|
.lostReason(leaf.getLostReason()).lostReportedBy(leaf.getLostReportedBy()).lostReportedOn(leaf.getLostReportedOn())
|
||||||
.cancelledReason(leaf.getCancelledReason()).cancelledBy(leaf.getCancelledBy()).cancelledOn(leaf.getCancelledOn())
|
.cancelledReason(leaf.getCancelledReason()).cancelledBy(leaf.getCancelledBy()).cancelledOn(leaf.getCancelledOn())
|
||||||
.usedReason(leaf.getUsedReason()).usedBy(leaf.getUsedBy()).usedOn(leaf.getUsedOn())
|
.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)
|
.accountType(leaf.getAccountType()).accountId(leaf.getAccountId()).accountNo(display.accountNo)
|
||||||
.clientName(display.clientName).officeName(leaf.getOfficeName()).build();
|
.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.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.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.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, "
|
+ "b.account_type as account_type, b.account_id as account_id, bo.name as book_office_name, "
|
||||||
+ "lo.name as lot_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 "
|
+ "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"))
|
.cancelledReason(rs.getString("cancelled_reason")).cancelledBy((Long) rs.getObject("cancelled_by"))
|
||||||
.cancelledOn(rs.getObject("cancelled_on", OffsetDateTime.class)).usedReason(rs.getString("used_reason"))
|
.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))
|
.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)
|
.accountType(rs.getString("account_type")).accountId((Long) rs.getObject("account_id")).accountNo(null)
|
||||||
.clientName(null).officeName(officeName).build();
|
.clientName(null).officeName(officeName).build();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -61,6 +61,9 @@ public interface ChequeBookWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult cancelLeaf(Long chequeBookId, JsonCommand command);
|
CommandProcessingResult cancelLeaf(Long chequeBookId, JsonCommand command);
|
||||||
|
|
||||||
|
/** Flags leaves in range as RETURNED — presented and dishonoured/bounced. Terminal, like reportlost. */
|
||||||
|
CommandProcessingResult returnChequeBook(Long chequeBookId, JsonCommand command);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually records that leaves cleared, once a branch sees them on the bank/clearing statement — never
|
* 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.
|
* 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();
|
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
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult markLeafUsed(final Long chequeBookId, final JsonCommand command) {
|
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/0385_chequebook_pending_requests.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0386_enable_global_maker_checker.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/0387_chequeleaf_serial_unique_per_lot.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0388_chequeleaf_returned_status.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</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