Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2569e0f836 | ||
|
|
81b5850b62 | ||
|
|
88157ee9d6 | ||
|
|
760fff8d3e | ||
|
|
d7ff4e4a7b | ||
|
|
7068d65422 |
+2
@@ -33,4 +33,6 @@ public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecif
|
|||||||
Collection<Teller> findTellerByOfficeId(Long officeId);
|
Collection<Teller> findTellerByOfficeId(Long officeId);
|
||||||
|
|
||||||
long countByDeletedFalse();
|
long countByDeletedFalse();
|
||||||
|
|
||||||
|
long countByOfficeIdAndDeletedFalse(Long officeId);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -58,4 +58,9 @@ public class TellerRepositoryWrapper {
|
|||||||
public boolean existsAny() {
|
public boolean existsAny() {
|
||||||
return this.repository.countByDeletedFalse() > 0;
|
return this.repository.countByDeletedFalse() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the branch already has a (not deleted) teller — each branch keeps one vault of its own. */
|
||||||
|
public boolean existsInOffice(final Long officeId) {
|
||||||
|
return this.repository.countByOfficeIdAndDeletedFalse(officeId) > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -28,4 +28,9 @@ public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleExce
|
|||||||
public TellerAlreadyExistsException() {
|
public TellerAlreadyExistsException() {
|
||||||
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
|
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TellerAlreadyExistsException(final String officeName) {
|
||||||
|
super(ERROR_MESSAGE_CODE, "Cannot create teller: " + officeName + " already has a teller. Each branch has one teller.",
|
||||||
|
officeName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-10
@@ -137,16 +137,7 @@ public class CommandSourceService {
|
|||||||
String permission = commandSource.getPermissionCode();
|
String permission = commandSource.getPermissionCode();
|
||||||
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
||||||
if (isMakerChecker || result.isRollbackTransaction()) {
|
if (isMakerChecker || result.isRollbackTransaction()) {
|
||||||
// isApprovedByChecker is only ever true here for a genuine checker approval
|
if (isApprovedByChecker || isSuperUserBypassAllowed(commandSource, user)) {
|
||||||
// (PortfolioCommandSourceWritePlatformServiceImpl.approveEntry(), which has already run
|
|
||||||
// validateMakerCheckerTransaction() and confirmed the approver is not the maker) or a user
|
|
||||||
// editing their own account details (CommandWrapper.isChangeOfOwnUserDetails() - maker-checker
|
|
||||||
// genuinely does not apply there). On the ORIGINAL submission this is always false and `user`
|
|
||||||
// here is always the maker themselves, so there is deliberately no CHECKER_SUPER_USER (or any
|
|
||||||
// other) bypass at this point: holding CHECKER_SUPER_USER lets a user approve someone ELSE's
|
|
||||||
// request without needing the specific <TASK>_CHECKER permission, it must never let a maker
|
|
||||||
// auto-approve their own submission just by holding it.
|
|
||||||
if (isApprovedByChecker) {
|
|
||||||
commandSource.markAsChecked(user);
|
commandSource.markAsChecked(user);
|
||||||
} else {
|
} else {
|
||||||
if (commandSource.isSanitized()) {
|
if (commandSource.isSanitized()) {
|
||||||
@@ -160,6 +151,25 @@ public class CommandSourceService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSuperUserBypassAllowed(CommandSource commandSource, AppUser user) {
|
||||||
|
if (!user.isCheckerSuperUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
||||||
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+56
@@ -95,6 +95,62 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentParty() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/parties";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/investment/parties/" + partyId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/investment/parties/" + partyId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentScript() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTSCRIPT";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/scripts";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "INVESTMENTSCRIPT";
|
||||||
|
this.entityId = scriptId;
|
||||||
|
this.href = "/investment/scripts/" + scriptId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentPurchase() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/purchases";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createChequeStockLot() {
|
public CommandWrapperBuilder createChequeStockLot() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "CHEQUESTOCKLOT";
|
this.entityName = "CHEQUESTOCKLOT";
|
||||||
|
|||||||
+8
-20
@@ -48,12 +48,6 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final CommandProcessingService processAndLogCommandService;
|
private final CommandProcessingService processAndLogCommandService;
|
||||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||||
// No longer consulted by validateMakerCheckerTransaction() below: the maker-identity rule (a user
|
|
||||||
// can never approve their own submission) is now unconditional and isSameMakerCheckerEnabled() no
|
|
||||||
// longer overrides it. Left injected rather than removed - enable-same-maker-checker is still a
|
|
||||||
// live, independently-configurable global switch (System -> Configurations); deleting this field,
|
|
||||||
// and by extension deciding that switch should stop existing altogether, is a separate decision
|
|
||||||
// from closing this specific self-approval loophole.
|
|
||||||
private final ConfigurationDomainService configurationService;
|
private final ConfigurationDomainService configurationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -129,20 +123,14 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
AppUser appUser = this.context.authenticatedUser();
|
AppUser appUser = this.context.authenticatedUser();
|
||||||
String permissionCode = commandSource.getPermissionCode();
|
String permissionCode = commandSource.getPermissionCode();
|
||||||
appUser.validateHasCheckerPermissionTo(permissionCode);
|
appUser.validateHasCheckerPermissionTo(permissionCode);
|
||||||
// Maker-identity rule: the user who submitted a command may never be the one who approves
|
if (!configurationService.isSameMakerCheckerEnabled() && !appUser.isCheckerSuperUser()) {
|
||||||
// (or rejects) it - this is a fixed platform rule, not overridable by CHECKER_SUPER_USER (that
|
AppUser maker = commandSource.getMaker();
|
||||||
// permission still lets its holder action a DIFFERENT user's request without needing the
|
if (maker == null) {
|
||||||
// specific <TASK>_CHECKER permission - see validateHasCheckerPermissionTo() above - it just no
|
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
||||||
// longer lets them action their own) and not overridable by the enable-same-maker-checker
|
}
|
||||||
// global switch (isSameMakerCheckerEnabled() is deliberately no longer consulted here; that
|
if (Objects.equals(appUser.getId(), maker.getId())) {
|
||||||
// configuration is left in place and unused for this decision rather than removed, since
|
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
||||||
// deleting a still-configurable global setting is a separate decision from this one).
|
}
|
||||||
AppUser maker = commandSource.getMaker();
|
|
||||||
if (maker == null) {
|
|
||||||
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
|
||||||
}
|
|
||||||
if (Objects.equals(appUser.getId(), maker.getId())) {
|
|
||||||
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
|
||||||
}
|
}
|
||||||
return commandSource;
|
return commandSource;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -130,7 +130,12 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<TellerData> getTellers(Long officeId) {
|
public Collection<TellerData> getTellers(Long officeId) {
|
||||||
return retrieveAllTellers(false);
|
if (officeId == null) {
|
||||||
|
return retrieveAllTellers(false);
|
||||||
|
}
|
||||||
|
// The tellers of that one branch. The officeId used to be ignored, so the head office, whose
|
||||||
|
// hierarchy covers every branch, counted each branch's vault and cashiers as its own at day close.
|
||||||
|
return retrieveAllTellers(false).stream().filter(t -> officeId.equals(t.getOfficeId())).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+6
-4
@@ -70,10 +70,6 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer
|
|||||||
try {
|
try {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
if (this.tellerRepositoryWrapper.existsAny()) {
|
|
||||||
throw new TellerAlreadyExistsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Long officeId = command.longValueOfParameterNamed("officeId");
|
final Long officeId = command.longValueOfParameterNamed("officeId");
|
||||||
|
|
||||||
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
|
this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json());
|
||||||
@@ -81,6 +77,12 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer
|
|||||||
// final Office parent =
|
// final Office parent =
|
||||||
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
|
// validateUserPriviledgeOnOfficeAndRetrieve(currentUser, officeId);
|
||||||
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
final Office tellerOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
|
||||||
|
// One teller (vault) per branch, not one for the whole bank: each branch holds and settles
|
||||||
|
// its own cash, so a second branch needs a vault of its own.
|
||||||
|
if (this.tellerRepositoryWrapper.existsInOffice(officeId)) {
|
||||||
|
throw new TellerAlreadyExistsException(tellerOffice.getName());
|
||||||
|
}
|
||||||
final Teller teller = Teller.fromJson(tellerOffice, command);
|
final Teller teller = Teller.fromJson(tellerOffice, command);
|
||||||
|
|
||||||
// pre save to generate id for use in office hierarchy
|
// pre save to generate id for use in office hierarchy
|
||||||
|
|||||||
+29
-20
@@ -71,6 +71,20 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public class BranchDayRegisterApiResource {
|
public class BranchDayRegisterApiResource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The branch whose counter took a breakdown {@code std}: the teller office of its officer's cashier
|
||||||
|
* allocation if that allocation already existed when the cash was counted, otherwise the officer's
|
||||||
|
* own office. A person moved to another branch's teller keeps what they took at the old branch
|
||||||
|
* there.
|
||||||
|
*/
|
||||||
|
private static final String COUNTER_OFFICE_OF_STD = """
|
||||||
|
COALESCE((SELECT t.office_id FROM m_appuser au JOIN m_cashiers mc ON mc.staff_id = au.staff_id
|
||||||
|
JOIN m_tellers t ON t.id = mc.teller_id
|
||||||
|
WHERE au.id = std.officer_id""" + CountedCashLedger.SINCE_ALLOCATION + """
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT au.office_id FROM m_appuser au WHERE au.id = std.officer_id))
|
||||||
|
""";
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final FromJsonHelper fromJsonHelper;
|
private final FromJsonHelper fromJsonHelper;
|
||||||
@@ -195,8 +209,8 @@ public class BranchDayRegisterApiResource {
|
|||||||
// Scoped to this office through the officer who took the cash (appuser -> staff -> cashier ->
|
// Scoped to this office through the officer who took the cash (appuser -> staff -> cashier ->
|
||||||
// teller -> office). Unscoped, a branch's own "does the physical count agree with the books"
|
// teller -> office). Unscoped, a branch's own "does the physical count agree with the books"
|
||||||
// cross-check was being compared against every branch's counted cash at once, which makes it
|
// cross-check was being compared against every branch's counted cash at once, which makes it
|
||||||
// wrong at every office but the first. A row whose officer maps to no cashier is still
|
// wrong at every office but the first. A row whose officer has no cashier allocation counts
|
||||||
// counted, for the same reason as the withdrawal legs in bookBreakdown.
|
// at the officer's own office (see COUNTER_OFFICE_OF_STD), not at every office.
|
||||||
//
|
//
|
||||||
// Which rows are real counted cash is CountedCashLedger's rule, not this screen's -- see the
|
// Which rows are real counted cash is CountedCashLedger's rule, not this screen's -- see the
|
||||||
// note there on why a manual GL entry's decoded reference is kept and a stamped millisecond
|
// note there on why a manual GL entry's decoded reference is kept and a stamped millisecond
|
||||||
@@ -205,23 +219,15 @@ public class BranchDayRegisterApiResource {
|
|||||||
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
|
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
|
||||||
WHERE std.txn_date = ? AND std.txn_type = 'DEPOSIT'
|
WHERE std.txn_date = ? AND std.txn_type = 'DEPOSIT'
|
||||||
AND """ + CountedCashLedger.COUNTED_ROW + """
|
AND """ + CountedCashLedger.COUNTED_ROW + """
|
||||||
AND (std.officer_id IS NULL
|
AND (std.officer_id IS NULL OR (""" + COUNTER_OFFICE_OF_STD + """
|
||||||
OR NOT EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
|
) = ?)
|
||||||
WHERE au.id = std.officer_id)
|
|
||||||
OR EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
|
|
||||||
JOIN m_tellers t ON t.id = c.teller_id
|
|
||||||
WHERE au.id = std.officer_id AND t.office_id = ?))
|
|
||||||
""", BigDecimal.class, Date.valueOf(businessDate), officeId));
|
""", BigDecimal.class, Date.valueOf(businessDate), officeId));
|
||||||
final BigDecimal countedOut = zero(jdbcTemplate.queryForObject("""
|
final BigDecimal countedOut = zero(jdbcTemplate.queryForObject("""
|
||||||
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
|
SELECT COALESCE(SUM(std.total_amount), 0) FROM m_savings_txn_denomination std
|
||||||
WHERE std.txn_date = ? AND std.txn_type = 'WITHDRAWAL'
|
WHERE std.txn_date = ? AND std.txn_type = 'WITHDRAWAL'
|
||||||
AND """ + CountedCashLedger.COUNTED_ROW + """
|
AND """ + CountedCashLedger.COUNTED_ROW + """
|
||||||
AND (std.officer_id IS NULL
|
AND (std.officer_id IS NULL OR (""" + COUNTER_OFFICE_OF_STD + """
|
||||||
OR NOT EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
|
) = ?)
|
||||||
WHERE au.id = std.officer_id)
|
|
||||||
OR EXISTS (SELECT 1 FROM m_appuser au JOIN m_cashiers c ON c.staff_id = au.staff_id
|
|
||||||
JOIN m_tellers t ON t.id = c.teller_id
|
|
||||||
WHERE au.id = std.officer_id AND t.office_id = ?))
|
|
||||||
""", BigDecimal.class, Date.valueOf(businessDate), officeId));
|
""", BigDecimal.class, Date.valueOf(businessDate), officeId));
|
||||||
result.put("denominationIn", countedIn);
|
result.put("denominationIn", countedIn);
|
||||||
result.put("denominationOut", countedOut);
|
result.put("denominationOut", countedOut);
|
||||||
@@ -609,11 +615,9 @@ public class BranchDayRegisterApiResource {
|
|||||||
// never opened is not waited for.
|
// never opened is not waited for.
|
||||||
final boolean isHeadOffice = interestPostingReportService.isHeadOffice(officeId);
|
final boolean isHeadOffice = interestPostingReportService.isHeadOffice(officeId);
|
||||||
if (isHeadOffice) {
|
if (isHeadOffice) {
|
||||||
final List<String> stillOpen = jdbcTemplate.queryForList("""
|
// Any day still open counts, not only today's: a branch whose earlier day was never closed is
|
||||||
SELECT o.name FROM m_branch_day_register r JOIN m_office o ON o.id = r.office_id
|
// still working and has not posted its interest.
|
||||||
WHERE r.business_date = ? AND r.closed_at IS NULL AND r.office_id <> ?
|
final List<String> stillOpen = interestPostingReportService.branchesStillOpen(businessDate, officeId);
|
||||||
ORDER BY o.name
|
|
||||||
""", String.class, Date.valueOf(businessDate), officeId);
|
|
||||||
if (!stillOpen.isEmpty()) {
|
if (!stillOpen.isEmpty()) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.branchday.branches.still.open",
|
throw new PlatformDataIntegrityException("error.msg.branchday.branches.still.open",
|
||||||
"Cannot close the head office yet — these branches have not closed the day: " + String.join(", ", stillOpen)
|
"Cannot close the head office yet — these branches have not closed the day: " + String.join(", ", stillOpen)
|
||||||
@@ -1058,6 +1062,9 @@ public class BranchDayRegisterApiResource {
|
|||||||
final Map<String, Object> r = rows.get(0);
|
final Map<String, Object> r = rows.get(0);
|
||||||
final Map<String, Object> m = new LinkedHashMap<>();
|
final Map<String, Object> m = new LinkedHashMap<>();
|
||||||
m.put("id", r.get("id"));
|
m.put("id", r.get("id"));
|
||||||
|
// The close screen fetches this office's tellers with it; left out, it fetched every teller
|
||||||
|
// the user can see, and the head office counted each branch's vault as its own.
|
||||||
|
m.put("officeId", r.get("office_id"));
|
||||||
m.put("businessDate", str(r.get("business_date")));
|
m.put("businessDate", str(r.get("business_date")));
|
||||||
m.put("openedByName", r.get("opened_by_name"));
|
m.put("openedByName", r.get("opened_by_name"));
|
||||||
m.put("openedAt", str(r.get("opened_at")));
|
m.put("openedAt", str(r.get("opened_at")));
|
||||||
@@ -1111,11 +1118,13 @@ public class BranchDayRegisterApiResource {
|
|||||||
(rs, n) -> rs.getBigDecimal(1), officeId, date).stream().findFirst().orElse(null);
|
(rs, n) -> rs.getBigDecimal(1), officeId, date).stream().findFirst().orElse(null);
|
||||||
final BigDecimal opening = ownOpening != null ? ownOpening : lastClosingCash(officeId, businessDate);
|
final BigDecimal opening = ownOpening != null ? ownOpening : lastClosingCash(officeId, businessDate);
|
||||||
|
|
||||||
|
// By the counter that took the cash. A savings deposit carries no office_id, and the old
|
||||||
|
// "OR office_id IS NULL" counted every such deposit in the bank at every branch.
|
||||||
final BigDecimal savingsDeposits = zero(jdbcTemplate.queryForObject("""
|
final BigDecimal savingsDeposits = zero(jdbcTemplate.queryForObject("""
|
||||||
SELECT COALESCE(SUM(amount), 0) FROM m_cashier_deposit_request
|
SELECT COALESCE(SUM(amount), 0) FROM m_cashier_deposit_request
|
||||||
WHERE status = 'APPROVED'
|
WHERE status = 'APPROVED'
|
||||||
AND COALESCE(txn_date, CAST(submitted_at AS DATE)) = ?
|
AND COALESCE(txn_date, CAST(submitted_at AS DATE)) = ?
|
||||||
AND (office_id = ? OR office_id IS NULL)
|
AND COALESCE(counter_office_id, office_id) = ?
|
||||||
""", BigDecimal.class, date, officeId));
|
""", BigDecimal.class, date, officeId));
|
||||||
|
|
||||||
// Cash paid over the counter to buy a demand draft is real cash arriving at the branch too --
|
// Cash paid over the counter to buy a demand draft is real cash arriving at the branch too --
|
||||||
|
|||||||
+17
-3
@@ -166,8 +166,8 @@ public class CashierDepositRequestApiResource {
|
|||||||
client_id, client_name, amount,
|
client_id, client_name, amount,
|
||||||
payment_type_id, payment_type_name, description, cheque_no, bank_name, txn_date,
|
payment_type_id, payment_type_name, description, cheque_no, bank_name, txn_date,
|
||||||
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at,
|
status, cashier_id, cashier_name, cashier_username, denomination_data, activity_log, submitted_at,
|
||||||
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id)
|
dd_face_value, dd_charges, dd_gst_percentage, dd_gst_amount, entry_type_cv_id, counter_office_id)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
Long.class,
|
Long.class,
|
||||||
@@ -179,7 +179,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
"PENDING",
|
"PENDING",
|
||||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||||
denominationData, activityLog, Timestamp.from(Instant.now()),
|
denominationData, activityLog, Timestamp.from(Instant.now()),
|
||||||
ddFaceValue, ddCharges, ddGstPercentage, ddGstAmount, entryTypeCvId
|
ddFaceValue, ddCharges, ddGstPercentage, ddGstAmount, entryTypeCvId, counterOfficeId(currentUser)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Globally unique voucher number from the central m_voucher register
|
// Globally unique voucher number from the central m_voucher register
|
||||||
@@ -691,6 +691,20 @@ public class CashierDepositRequestApiResource {
|
|||||||
.orElse(Map.of());
|
.orElse(Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The branch whose counter took the cash: the office of the teller the user is allocated to as a
|
||||||
|
* cashier, otherwise the user's own office. office_id cannot say this -- a savings deposit carries
|
||||||
|
* none, and a GL deposit's is the GL's branch -- and the day close totals each branch's cash by it.
|
||||||
|
*/
|
||||||
|
private Long counterOfficeId(final AppUser user) {
|
||||||
|
final List<Long> tellerOffice = user.getStaff() == null ? List.of()
|
||||||
|
: jdbcTemplate.queryForList("""
|
||||||
|
SELECT t.office_id FROM m_cashiers c JOIN m_tellers t ON t.id = c.teller_id
|
||||||
|
WHERE c.staff_id = ? LIMIT 1
|
||||||
|
""", Long.class, user.getStaff().getId());
|
||||||
|
return tellerOffice.isEmpty() ? user.getOffice().getId() : tellerOffice.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
private Map<?, ?> loadRecord(final Long id) {
|
private Map<?, ?> loadRecord(final Long id) {
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
SELECT id, voucher_no, account_no, account_type, savings_account_id,
|
SELECT id, voucher_no, account_no, account_type, savings_account_id,
|
||||||
|
|||||||
+2
-2
@@ -93,7 +93,7 @@ public class CashReconciliationApiResource {
|
|||||||
+ "AND " + CountedCashLedger.COUNTED_ROW
|
+ "AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ "AND std.officer_id IN ("
|
+ "AND std.officer_id IN ("
|
||||||
+ " SELECT au.id FROM m_appuser au "
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ") "
|
||||||
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
// Out: settlements to vault
|
// Out: settlements to vault
|
||||||
@@ -123,7 +123,7 @@ public class CashReconciliationApiResource {
|
|||||||
+ "AND " + CountedCashLedger.COUNTED_ROW
|
+ "AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ "AND std.officer_id IN ("
|
+ "AND std.officer_id IN ("
|
||||||
+ " SELECT au.id FROM m_appuser au "
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ") "
|
||||||
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
// In: cash exchange — customer gave denomination TO cashier (RECEIVED direction)
|
// In: cash exchange — customer gave denomination TO cashier (RECEIVED direction)
|
||||||
|
|||||||
+9
-9
@@ -671,25 +671,25 @@ public class CashReportApiResource {
|
|||||||
// day itself: taken from the vault, in over the counter, out over the counter, settled
|
// day itself: taken from the vault, in over the counter, out over the counter, settled
|
||||||
// back. What is left is what should physically be in the drawer.
|
// back. What is left is what should physically be in the drawer.
|
||||||
result.put("todayVaultIn", dayLegTotal(
|
result.put("todayVaultIn", dayLegTotal(
|
||||||
"SELECT COALESCE(SUM(ctd.count * ctd.denomination_value), 0) FROM m_cash_transfer_detail ctd"
|
"SELECT COALESCE(SUM(CAST(ctd.count AS BIGINT) * ctd.denomination_value), 0) FROM m_cash_transfer_detail ctd"
|
||||||
+ " JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id"
|
+ " JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id"
|
||||||
+ " WHERE ct.to_cashier_id = ? AND ct.transfer_date = CAST(? AS DATE)"
|
+ " WHERE ct.to_cashier_id = ? AND ct.transfer_date = CAST(? AS DATE)"
|
||||||
+ " AND COALESCE(ct.is_reversed, false) = false",
|
+ " AND COALESCE(ct.is_reversed, false) = false",
|
||||||
scopedCashierId, txnDate));
|
scopedCashierId, txnDate));
|
||||||
result.put("todaySettledOut", dayLegTotal(
|
result.put("todaySettledOut", dayLegTotal(
|
||||||
"SELECT COALESCE(SUM(csd.count * csd.denomination_value), 0) FROM m_cash_settlement_detail csd"
|
"SELECT COALESCE(SUM(CAST(csd.count AS BIGINT) * csd.denomination_value), 0) FROM m_cash_settlement_detail csd"
|
||||||
+ " JOIN m_cash_settlement cs ON cs.id = csd.settlement_id"
|
+ " JOIN m_cash_settlement cs ON cs.id = csd.settlement_id"
|
||||||
+ " WHERE cs.from_cashier_id = ? AND cs.settlement_date = CAST(? AS DATE)"
|
+ " WHERE cs.from_cashier_id = ? AND cs.settlement_date = CAST(? AS DATE)"
|
||||||
+ " AND COALESCE(cs.is_reversed, false) = false",
|
+ " AND COALESCE(cs.is_reversed, false) = false",
|
||||||
scopedCashierId, txnDate));
|
scopedCashierId, txnDate));
|
||||||
result.put("todayInterCashierIn", dayLegTotal(
|
result.put("todayInterCashierIn", dayLegTotal(
|
||||||
"SELECT COALESCE(SUM(icd.count * icd.denomination_value), 0) FROM m_cash_inter_cashier_detail icd"
|
"SELECT COALESCE(SUM(CAST(icd.count AS BIGINT) * icd.denomination_value), 0) FROM m_cash_inter_cashier_detail icd"
|
||||||
+ " JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
+ " JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
||||||
+ " WHERE ic.to_cashier_id = ? AND ic.transfer_date = CAST(? AS DATE)"
|
+ " WHERE ic.to_cashier_id = ? AND ic.transfer_date = CAST(? AS DATE)"
|
||||||
+ " AND COALESCE(ic.is_reversed, false) = false",
|
+ " AND COALESCE(ic.is_reversed, false) = false",
|
||||||
scopedCashierId, txnDate));
|
scopedCashierId, txnDate));
|
||||||
result.put("todayInterCashierOut", dayLegTotal(
|
result.put("todayInterCashierOut", dayLegTotal(
|
||||||
"SELECT COALESCE(SUM(icd.count * icd.denomination_value), 0) FROM m_cash_inter_cashier_detail icd"
|
"SELECT COALESCE(SUM(CAST(icd.count AS BIGINT) * icd.denomination_value), 0) FROM m_cash_inter_cashier_detail icd"
|
||||||
+ " JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
+ " JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
||||||
+ " WHERE ic.from_cashier_id = ? AND ic.transfer_date = CAST(? AS DATE)"
|
+ " WHERE ic.from_cashier_id = ? AND ic.transfer_date = CAST(? AS DATE)"
|
||||||
+ " AND COALESCE(ic.is_reversed, false) = false",
|
+ " AND COALESCE(ic.is_reversed, false) = false",
|
||||||
@@ -721,7 +721,7 @@ public class CashReportApiResource {
|
|||||||
final Long scopedCashierId) {
|
final Long scopedCashierId) {
|
||||||
final String cashierFilter = scopedCashierId != null ? "AND std.officer_id IN ("
|
final String cashierFilter = scopedCashierId != null ? "AND std.officer_id IN ("
|
||||||
+ " SELECT au.id FROM m_appuser au "
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " : "";
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ") " : "";
|
||||||
final String vaultCashierFilter = scopedCashierId != null ? "AND ct.to_cashier_id = ? " : "";
|
final String vaultCashierFilter = scopedCashierId != null ? "AND ct.to_cashier_id = ? " : "";
|
||||||
final String settleCashierFilter = scopedCashierId != null ? "AND cs.from_cashier_id = ? " : "";
|
final String settleCashierFilter = scopedCashierId != null ? "AND cs.from_cashier_id = ? " : "";
|
||||||
|
|
||||||
@@ -1171,7 +1171,7 @@ public class CashReportApiResource {
|
|||||||
+ "WHERE std.txn_type = 'DEPOSIT' AND std.txn_date <= CAST(? AS DATE) "
|
+ "WHERE std.txn_type = 'DEPOSIT' AND std.txn_date <= CAST(? AS DATE) "
|
||||||
+ "AND std.officer_id IN ("
|
+ "AND std.officer_id IN ("
|
||||||
+ " SELECT au.id FROM m_appuser au "
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + COUNTER_NOT_REVERSED
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ") " + COUNTER_NOT_REVERSED
|
||||||
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
// Settlements back to vault
|
// Settlements back to vault
|
||||||
@@ -1197,7 +1197,7 @@ public class CashReportApiResource {
|
|||||||
+ "WHERE std.txn_type = 'WITHDRAWAL' AND std.txn_date <= CAST(? AS DATE) "
|
+ "WHERE std.txn_type = 'WITHDRAWAL' AND std.txn_date <= CAST(? AS DATE) "
|
||||||
+ "AND std.officer_id IN ("
|
+ "AND std.officer_id IN ("
|
||||||
+ " SELECT au.id FROM m_appuser au "
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + COUNTER_NOT_REVERSED
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ") " + COUNTER_NOT_REVERSED
|
||||||
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
// Exchange IN: customer gave denomination to cashier (RECEIVED direction)
|
// Exchange IN: customer gave denomination to cashier (RECEIVED direction)
|
||||||
@@ -1315,13 +1315,13 @@ public class CashReportApiResource {
|
|||||||
if (cashierId != null) {
|
if (cashierId != null) {
|
||||||
officerFilter = "AND std.officer_id IN ("
|
officerFilter = "AND std.officer_id IN ("
|
||||||
+ "SELECT au.id FROM m_appuser au "
|
+ "SELECT au.id FROM m_appuser au "
|
||||||
+ "JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)";
|
+ "JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")";
|
||||||
baseParams = new Object[] { txnDate, cashierId };
|
baseParams = new Object[] { txnDate, cashierId };
|
||||||
} else if (tellerId != null) {
|
} else if (tellerId != null) {
|
||||||
officerFilter = "AND std.officer_id IN ("
|
officerFilter = "AND std.officer_id IN ("
|
||||||
+ "SELECT au.id FROM m_appuser au "
|
+ "SELECT au.id FROM m_appuser au "
|
||||||
+ "JOIN m_cashiers mc ON mc.staff_id = au.staff_id "
|
+ "JOIN m_cashiers mc ON mc.staff_id = au.staff_id "
|
||||||
+ "JOIN m_tellers mt ON mt.id = mc.teller_id WHERE mt.id = ?)";
|
+ "JOIN m_tellers mt ON mt.id = mc.teller_id WHERE mt.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")";
|
||||||
baseParams = new Object[] { txnDate, tellerId };
|
baseParams = new Object[] { txnDate, tellerId };
|
||||||
} else {
|
} else {
|
||||||
officerFilter = "AND std.officer_id IN ("
|
officerFilter = "AND std.officer_id IN ("
|
||||||
|
|||||||
+11
-1
@@ -130,9 +130,19 @@ public final class CountedCashLedger {
|
|||||||
+ " AND (rct.is_reversed = true OR " + breakdownDiffersFrom("rct.amount") + "))"
|
+ " AND (rct.is_reversed = true OR " + breakdownDiffersFrom("rct.amount") + "))"
|
||||||
+ " AND" + REAL_TXN_ID + ") ";
|
+ " AND" + REAL_TXN_ID + ") ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Over the aliases {@code std} and {@code mc} (the cashier a breakdown's officer maps to): only what
|
||||||
|
* the login counted once this allocation existed. A breakdown is tied to a cashier through its
|
||||||
|
* officer's login, so without this a person moved from one branch's teller to another's carried
|
||||||
|
* every rupee they had ever taken -- at the old branch too -- into the new drawer. An allocation
|
||||||
|
* with no recorded creation time keeps its whole history.
|
||||||
|
*/
|
||||||
|
public static final String SINCE_ALLOCATION = " AND (mc.created_at IS NULL OR std.created_at IS NULL"
|
||||||
|
+ " OR std.created_at >= mc.created_at)";
|
||||||
|
|
||||||
/** Sub-rupee tolerance: a paisa-exact amount is handed over as the nearest whole rupee. */
|
/** Sub-rupee tolerance: a paisa-exact amount is handed over as the nearest whole rupee. */
|
||||||
private static String breakdownDiffersFrom(final String amountColumn) {
|
private static String breakdownDiffersFrom(final String amountColumn) {
|
||||||
return "ABS(" + amountColumn + " - (SELECT SUM(sd.count * sd.denomination_value)"
|
return "ABS(" + amountColumn + " - (SELECT SUM(CAST(sd.count AS BIGINT) * sd.denomination_value)"
|
||||||
+ " FROM m_savings_txn_denomination sd WHERE sd.txn_id = std.txn_id"
|
+ " FROM m_savings_txn_denomination sd WHERE sd.txn_id = std.txn_id"
|
||||||
+ " AND sd.account_no IS NOT DISTINCT FROM std.account_no)) >= 1";
|
+ " AND sd.account_no IS NOT DISTINCT FROM std.account_no)) >= 1";
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -288,7 +288,7 @@ public class DenominationBreakdownWriter {
|
|||||||
+ " WHERE std.txn_type = 'DEPOSIT'"
|
+ " WHERE std.txn_type = 'DEPOSIT'"
|
||||||
+ " AND " + CountedCashLedger.COUNTED_ROW
|
+ " AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")"
|
||||||
+ " GROUP BY std.denomination_id"
|
+ " GROUP BY std.denomination_id"
|
||||||
+ " UNION ALL"
|
+ " UNION ALL"
|
||||||
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
|
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
|
||||||
@@ -312,7 +312,7 @@ public class DenominationBreakdownWriter {
|
|||||||
+ " WHERE std.txn_type = 'WITHDRAWAL'"
|
+ " WHERE std.txn_type = 'WITHDRAWAL'"
|
||||||
+ " AND " + CountedCashLedger.COUNTED_ROW
|
+ " AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")"
|
||||||
+ " GROUP BY std.denomination_id"
|
+ " GROUP BY std.denomination_id"
|
||||||
+ " UNION ALL"
|
+ " UNION ALL"
|
||||||
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
|
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
|
||||||
@@ -409,7 +409,7 @@ public class DenominationBreakdownWriter {
|
|||||||
+ " WHERE std.txn_type = 'DEPOSIT'"
|
+ " WHERE std.txn_type = 'DEPOSIT'"
|
||||||
+ " AND " + CountedCashLedger.COUNTED_ROW
|
+ " AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")"
|
||||||
+ " GROUP BY std.denomination_id"
|
+ " GROUP BY std.denomination_id"
|
||||||
+ " UNION ALL"
|
+ " UNION ALL"
|
||||||
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
|
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
|
||||||
@@ -433,7 +433,7 @@ public class DenominationBreakdownWriter {
|
|||||||
+ " WHERE std.txn_type = 'WITHDRAWAL'"
|
+ " WHERE std.txn_type = 'WITHDRAWAL'"
|
||||||
+ " AND " + CountedCashLedger.COUNTED_ROW
|
+ " AND " + CountedCashLedger.COUNTED_ROW
|
||||||
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?" + CountedCashLedger.SINCE_ALLOCATION + ")"
|
||||||
+ " GROUP BY std.denomination_id"
|
+ " GROUP BY std.denomination_id"
|
||||||
+ " UNION ALL"
|
+ " UNION ALL"
|
||||||
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
|
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
|
||||||
|
|||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.api;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
|
||||||
|
* CBS Investment screens.
|
||||||
|
*/
|
||||||
|
public final class InvestmentApiConstants {
|
||||||
|
|
||||||
|
private InvestmentApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
|
||||||
|
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
|
||||||
|
public static final String RESOURCE_PARTY = "investmentparty";
|
||||||
|
public static final String RESOURCE_SCRIPT = "investmentscript";
|
||||||
|
|
||||||
|
/** Instrument ("Bond Type") an investment party / script belongs to. */
|
||||||
|
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
|
||||||
|
|
||||||
|
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
|
||||||
|
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
|
||||||
|
|
||||||
|
/** SLR classification on the Script Master ("GOI" = Central Government). */
|
||||||
|
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
|
||||||
|
|
||||||
|
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
|
||||||
|
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
|
||||||
|
"M", "FD", "F", "SHARE", "S");
|
||||||
|
|
||||||
|
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||||
|
|
||||||
|
// Common parameters
|
||||||
|
public static final String BOND_TYPE = "bondType";
|
||||||
|
public static final String GL_ACCOUNT_ID = "glAccountId";
|
||||||
|
public static final String ADDRESS1 = "address1";
|
||||||
|
public static final String ADDRESS2 = "address2";
|
||||||
|
public static final String ADDRESS3 = "address3";
|
||||||
|
public static final String CITY = "city";
|
||||||
|
public static final String PINCODE = "pincode";
|
||||||
|
public static final String PHONE = "phone";
|
||||||
|
public static final String FAX = "fax";
|
||||||
|
public static final String ACTIVE = "active";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
// Party parameters
|
||||||
|
public static final String PARTY_NAME = "partyName";
|
||||||
|
public static final String BANK_TYPE = "bankType";
|
||||||
|
public static final String RBI_CODE = "rbiCode";
|
||||||
|
public static final String EXPOSURE_LIMIT = "limit";
|
||||||
|
public static final String LEI_NO = "leiNo";
|
||||||
|
public static final String CP_BANK_NAME = "cpBankName";
|
||||||
|
public static final String CP_BRANCH_NAME = "cpBranchName";
|
||||||
|
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
||||||
|
public static final String CP_IFSC = "cpIfsc";
|
||||||
|
|
||||||
|
// Script parameters
|
||||||
|
public static final String SLR_TYPE = "slrType";
|
||||||
|
public static final String SCRIPT_DESC = "scriptDesc";
|
||||||
|
public static final String ISIN = "isin";
|
||||||
|
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
|
||||||
|
public static final String INT_RATE = "intRate";
|
||||||
|
public static final String ISSUE_DATE = "issueDate";
|
||||||
|
public static final String MATURITY_DATE = "maturityDate";
|
||||||
|
public static final String COMPANY_NAME = "companyName";
|
||||||
|
public static final String CORP_CODE = "corpCode";
|
||||||
|
|
||||||
|
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
||||||
|
BANK_TYPE, GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
||||||
|
LOCALE);
|
||||||
|
|
||||||
|
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
||||||
|
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
||||||
|
ACTIVE, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/parties")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Bank / Party Master", description = "Counterparties an investment is bought from or placed with.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentPartyApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment parties")
|
||||||
|
public List<InvestmentPartyData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||||
|
return this.readPlatformService.retrieveAllParties();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve an investment party")
|
||||||
|
public InvestmentPartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||||
|
return this.readPlatformService.retrieveParty(partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Create an investment party")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentParty().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Update an investment party")
|
||||||
|
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentParty(partyId).withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Drop an investment party")
|
||||||
|
public CommandProcessingResult delete(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentParty(partyId).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Constants for the Investment Purchase Entry (a deal that creates a holding). */
|
||||||
|
public final class InvestmentPurchaseApiConstants {
|
||||||
|
|
||||||
|
private InvestmentPurchaseApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTPURCHASE";
|
||||||
|
public static final String RESOURCE = "investmentpurchase";
|
||||||
|
|
||||||
|
public static final Set<String> SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2");
|
||||||
|
public static final Set<String> CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)");
|
||||||
|
|
||||||
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
|
||||||
|
public static final String BOND_TYPE = "bondType";
|
||||||
|
public static final String SCRIPT_ID = "scriptId";
|
||||||
|
public static final String PARTY_ID = "partyId";
|
||||||
|
public static final String DEAL_DATE = "dealDate";
|
||||||
|
public static final String DEAL_TIME = "dealTime";
|
||||||
|
public static final String SETTLEMENT_TYPE = "settlementType";
|
||||||
|
public static final String FACE_VALUE = "faceValue";
|
||||||
|
public static final String PURCHASE_RATE = "purchaseRate";
|
||||||
|
public static final String CONSIDER_AMT = "considerAmt";
|
||||||
|
public static final String NAV = "nav";
|
||||||
|
public static final String STAMP_DUTY = "stampDuty";
|
||||||
|
public static final String BROKERAGE = "brokerage";
|
||||||
|
public static final String CATEGORY = "category";
|
||||||
|
public static final String LAST_INT_DATE = "lastIntDate";
|
||||||
|
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
|
||||||
|
public static final String SUB_TYPE = "subType";
|
||||||
|
/** Optional legacy fields (trade no, authorities, lien, delivery mode, …) kept as a JSON object. */
|
||||||
|
public static final String OTHER_DETAILS = "otherDetails";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||||
|
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE, OTHER_DETAILS,
|
||||||
|
LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
|
import jakarta.ws.rs.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.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/purchases")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Purchase Entry", description = "Investment deals (Govt Security, Bond, Mutual Fund, …) — each becomes a holding.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentPurchaseApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment purchases (holdings)")
|
||||||
|
public List<InvestmentPurchaseData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
return this.readPlatformService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{purchaseId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve an investment purchase")
|
||||||
|
public InvestmentPurchaseData retrieveOne(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
return this.readPlatformService.retrieveOne(purchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{purchaseId}/interest-schedule")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Interest schedule of a holding", description = "Half-yearly coupons and 31 Mar / 30 Sep closing provisions "
|
||||||
|
+ "from purchase to maturity. Calculated only — nothing is posted. asOnDate (dd-MM-yyyy) defaults to the business date.")
|
||||||
|
public InvestmentInterestScheduleData retrieveInterestSchedule(
|
||||||
|
@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
final LocalDate asOn = StringUtils.isBlank(asOnDate) ? null
|
||||||
|
: LocalDate.parse(asOnDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
||||||
|
return this.readPlatformService.retrieveInterestSchedule(purchaseId, asOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Save a purchase entry")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentPurchase().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{purchaseId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Drop a purchase entry")
|
||||||
|
public CommandProcessingResult delete(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentPurchase(purchaseId).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.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.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/scripts")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Script Master", description = "Securities / instruments the bank invests in (Govt Security, Bond, Mutual Fund, …).")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentScriptApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment scripts")
|
||||||
|
public List<InvestmentScriptData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||||
|
return this.readPlatformService.retrieveAllScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{scriptId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve an investment script")
|
||||||
|
public InvestmentScriptData retrieveOne(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||||
|
return this.readPlatformService.retrieveScript(scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Create an investment script")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentScript().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{scriptId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Update an investment script")
|
||||||
|
public CommandProcessingResult update(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentScript(scriptId).withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interest schedule of one holding: every half-yearly coupon and every 31 March / 30 September closing provision from
|
||||||
|
* purchase to maturity. Calculated, not yet posted.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentInterestScheduleData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal unitValue;
|
||||||
|
private final BigDecimal balanceUnits;
|
||||||
|
private final BigDecimal purchaseRate;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String lastIntDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
/** Interest paid to the seller at purchase — already in Interest Receivable. */
|
||||||
|
private final BigDecimal purchasedAccruedInterest;
|
||||||
|
private final BigDecimal couponPerHalfYear;
|
||||||
|
private final String asOnDate;
|
||||||
|
private final String interestGlCode;
|
||||||
|
private final String interestGlName;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** COUPON (cash received) or PROVISION (closing accrual). */
|
||||||
|
private final String type;
|
||||||
|
private final String intDate;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final int days;
|
||||||
|
/** Coupon received, or interest earned since the last coupon up to the closing. */
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
/** Interest Receivable balance before this line (purchased accrued interest / earlier provision). */
|
||||||
|
private final BigDecimal alreadyBooked;
|
||||||
|
/** Provision to book now, or the part of the coupon that is new income. */
|
||||||
|
private final BigDecimal amount;
|
||||||
|
/** True when the date is on or before the as-on date. */
|
||||||
|
private final boolean due;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentPartyData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String bondType;
|
||||||
|
private final String partyName;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String address3;
|
||||||
|
private final String city;
|
||||||
|
private final String pincode;
|
||||||
|
private final String phone;
|
||||||
|
private final String fax;
|
||||||
|
private final String bankType;
|
||||||
|
private final Long glAccountId;
|
||||||
|
private final String glCode;
|
||||||
|
private final String glName;
|
||||||
|
private final String rbiCode;
|
||||||
|
private final BigDecimal limit;
|
||||||
|
private final String leiNo;
|
||||||
|
private final String cpBankName;
|
||||||
|
private final String cpBranchName;
|
||||||
|
private final String cpAccountNo;
|
||||||
|
private final String cpIfsc;
|
||||||
|
private final boolean active;
|
||||||
|
|
||||||
|
public static InvestmentPartyData from(final InvestmentParty p) {
|
||||||
|
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
|
||||||
|
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
|
||||||
|
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
|
||||||
|
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
|
||||||
|
.rbiCode(p.getRbiCode()).limit(p.getExposureLimit()).leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
||||||
|
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentPurchaseData {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String bondType;
|
||||||
|
private final Long scriptId;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String isin;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String partyName;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String dealTime;
|
||||||
|
private final String settlementType;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal purchaseRate;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal balanceUnits;
|
||||||
|
private final BigDecimal brokerage;
|
||||||
|
private final BigDecimal stampDuty;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final BigDecimal premiumDiscount;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String issueDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final String lastIntDate;
|
||||||
|
private final Integer accruedDays;
|
||||||
|
private final BigDecimal accruedInterest;
|
||||||
|
private final BigDecimal settlementAmount;
|
||||||
|
private final BigDecimal ytm;
|
||||||
|
private final String category;
|
||||||
|
private final String subType;
|
||||||
|
private final Long investmentGlAccountId;
|
||||||
|
private final String investmentGlCode;
|
||||||
|
private final String investmentGlName;
|
||||||
|
private final Long crGlAccountId;
|
||||||
|
private final String crGlCode;
|
||||||
|
private final String crGlName;
|
||||||
|
private final String otherDetails;
|
||||||
|
private final String status;
|
||||||
|
|
||||||
|
public static InvestmentPurchaseData from(final InvestmentPurchase p) {
|
||||||
|
return InvestmentPurchaseData.builder().id(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo()).bondType(p.getBondType())
|
||||||
|
.scriptId(p.getScript().getId()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
|
||||||
|
.isin(p.getScript().getIsin()).partyId(p.getParty().getId()).partyCode(p.getParty().getPartyCode())
|
||||||
|
.partyName(p.getParty().getPartyName()).dealDate(format(p.getDealDate())).dealTime(p.getDealTime())
|
||||||
|
.settlementType(p.getSettlementType()).settlementDate(format(p.getSettlementDate())).faceValue(p.getFaceValue())
|
||||||
|
.purchaseRate(p.getPurchaseRate()).units(p.getUnits()).balanceUnits(p.getBalanceUnits()).brokerage(p.getBrokerage())
|
||||||
|
.stampDuty(p.getStampDuty()).bookValue(p.getBookValue()).premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate())
|
||||||
|
.issueDate(format(p.getIssueDate())).maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate()))
|
||||||
|
.accruedDays(p.getAccruedDays()).accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount())
|
||||||
|
.ytm(p.getYtm()).category(p.getCategory()).subType(p.getSubType())
|
||||||
|
.investmentGlAccountId(p.getInvestmentGlAccount().getId()).investmentGlCode(p.getInvestmentGlAccount().getGlCode())
|
||||||
|
.investmentGlName(p.getInvestmentGlAccount().getName()).crGlAccountId(p.getCrGlAccount().getId())
|
||||||
|
.crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName()).otherDetails(p.getOtherDetails())
|
||||||
|
.status(p.getStatus()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentScriptData {
|
||||||
|
|
||||||
|
/** Dates go out as DD-MM-YYYY, the format the Investment screens use throughout. */
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String bondType;
|
||||||
|
private final String slrType;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final Long glAccountId;
|
||||||
|
private final String glCode;
|
||||||
|
private final String glName;
|
||||||
|
private final String isin;
|
||||||
|
private final BigDecimal faceValuePerUnit;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String issueDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final String companyName;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String address3;
|
||||||
|
private final String city;
|
||||||
|
private final String pincode;
|
||||||
|
private final String phone;
|
||||||
|
private final String fax;
|
||||||
|
private final String corpCode;
|
||||||
|
private final boolean active;
|
||||||
|
|
||||||
|
public static InvestmentScriptData from(final InvestmentScript s) {
|
||||||
|
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
|
||||||
|
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
|
||||||
|
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
|
||||||
|
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
|
||||||
|
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
|
||||||
|
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
|
||||||
|
.fax(s.getFax()).corpCode(s.getCorpCode()).active(s.isActive()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank / Party Master — the counterparty an investment is bought from or placed with (bank, RBI, issuer, AMC, broker),
|
||||||
|
* following the bank's existing CBS Bank / Party Master.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_party")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "party_code", nullable = false, length = 20, unique = true)
|
||||||
|
private String partyCode;
|
||||||
|
|
||||||
|
@Column(name = "bond_type", nullable = false, length = 20)
|
||||||
|
private String bondType;
|
||||||
|
|
||||||
|
@Column(name = "party_name", nullable = false, length = 150)
|
||||||
|
private String partyName;
|
||||||
|
|
||||||
|
@Column(name = "address1", length = 150)
|
||||||
|
private String address1;
|
||||||
|
|
||||||
|
@Column(name = "address2", length = 150)
|
||||||
|
private String address2;
|
||||||
|
|
||||||
|
@Column(name = "address3", length = 150)
|
||||||
|
private String address3;
|
||||||
|
|
||||||
|
@Column(name = "city", length = 100)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Column(name = "pincode", length = 10)
|
||||||
|
private String pincode;
|
||||||
|
|
||||||
|
@Column(name = "phone", length = 20)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "fax", length = 20)
|
||||||
|
private String fax;
|
||||||
|
|
||||||
|
@Column(name = "bank_type", length = 60)
|
||||||
|
private String bankType;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||||
|
private GLAccount glAccount;
|
||||||
|
|
||||||
|
@Column(name = "rbi_code", length = 30)
|
||||||
|
private String rbiCode;
|
||||||
|
|
||||||
|
@Column(name = "exposure_limit", scale = 6, precision = 19)
|
||||||
|
private BigDecimal exposureLimit;
|
||||||
|
|
||||||
|
@Column(name = "lei_no", length = 30)
|
||||||
|
private String leiNo;
|
||||||
|
|
||||||
|
@Column(name = "cp_bank_name", length = 150)
|
||||||
|
private String cpBankName;
|
||||||
|
|
||||||
|
@Column(name = "cp_branch_name", length = 150)
|
||||||
|
private String cpBranchName;
|
||||||
|
|
||||||
|
@Column(name = "cp_account_no", length = 40)
|
||||||
|
private String cpAccountNo;
|
||||||
|
|
||||||
|
@Column(name = "cp_ifsc", length = 11)
|
||||||
|
private String cpIfsc;
|
||||||
|
|
||||||
|
/** An inactive party is kept for history but not offered for new deals. */
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Investment Purchase Entry — one deal (as in the bank's existing CBS "Govt.Security/Bond Entry"). Once saved it is the
|
||||||
|
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held.
|
||||||
|
* Every derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_purchase")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
/** Financial-year deal number, e.g. 202627000001 for the first deal of FY 2026-27. */
|
||||||
|
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
|
||||||
|
private String dealNo;
|
||||||
|
|
||||||
|
/** Certificate number — system-generated running number. */
|
||||||
|
@Column(name = "cert_no", nullable = false, length = 20, unique = true)
|
||||||
|
private String certNo;
|
||||||
|
|
||||||
|
@Column(name = "bond_type", nullable = false, length = 20)
|
||||||
|
private String bondType;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "script_id", nullable = false)
|
||||||
|
private InvestmentScript script;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "party_id", nullable = false)
|
||||||
|
private InvestmentParty party;
|
||||||
|
|
||||||
|
@Column(name = "deal_date", nullable = false)
|
||||||
|
private LocalDate dealDate;
|
||||||
|
|
||||||
|
@Column(name = "deal_time", length = 8)
|
||||||
|
private String dealTime;
|
||||||
|
|
||||||
|
@Column(name = "settlement_type", nullable = false, length = 5)
|
||||||
|
private String settlementType;
|
||||||
|
|
||||||
|
@Column(name = "settlement_date", nullable = false)
|
||||||
|
private LocalDate settlementDate;
|
||||||
|
|
||||||
|
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal faceValue;
|
||||||
|
|
||||||
|
/** Price per ₹100 face value (Govt Security / Bond); NAV for a mutual fund. */
|
||||||
|
@Column(name = "purchase_rate", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal purchaseRate;
|
||||||
|
|
||||||
|
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal units;
|
||||||
|
|
||||||
|
@Column(name = "balance_units", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal balanceUnits;
|
||||||
|
|
||||||
|
@Column(name = "brokerage", scale = 6, precision = 19)
|
||||||
|
private BigDecimal brokerage;
|
||||||
|
|
||||||
|
@Column(name = "stamp_duty", scale = 6, precision = 19)
|
||||||
|
private BigDecimal stampDuty;
|
||||||
|
|
||||||
|
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal bookValue;
|
||||||
|
|
||||||
|
@Column(name = "premium_discount", scale = 6, precision = 19)
|
||||||
|
private BigDecimal premiumDiscount;
|
||||||
|
|
||||||
|
@Column(name = "int_rate", scale = 4, precision = 9)
|
||||||
|
private BigDecimal intRate;
|
||||||
|
|
||||||
|
@Column(name = "issue_date")
|
||||||
|
private LocalDate issueDate;
|
||||||
|
|
||||||
|
@Column(name = "maturity_date")
|
||||||
|
private LocalDate maturityDate;
|
||||||
|
|
||||||
|
@Column(name = "last_int_date")
|
||||||
|
private LocalDate lastIntDate;
|
||||||
|
|
||||||
|
@Column(name = "accrued_days")
|
||||||
|
private Integer accruedDays;
|
||||||
|
|
||||||
|
/** Interest paid to the seller from the last coupon date up to the deal date (30/360). */
|
||||||
|
@Column(name = "accrued_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal accruedInterest;
|
||||||
|
|
||||||
|
@Column(name = "settlement_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal settlementAmount;
|
||||||
|
|
||||||
|
@Column(name = "ytm", scale = 4, precision = 9)
|
||||||
|
private BigDecimal ytm;
|
||||||
|
|
||||||
|
@Column(name = "category", nullable = false, length = 40)
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Column(name = "sub_type", length = 40)
|
||||||
|
private String subType;
|
||||||
|
|
||||||
|
/** Investment GL the holding is booked to (from the script). */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "investment_gl_account_id", nullable = false)
|
||||||
|
private GLAccount investmentGlAccount;
|
||||||
|
|
||||||
|
/** Bank GL the purchase is paid from ("CR. To A/c"). */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "cr_gl_account_id", nullable = false)
|
||||||
|
private GLAccount crGlAccount;
|
||||||
|
|
||||||
|
@Column(name = "other_details", length = 4000)
|
||||||
|
private String otherDetails;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {}
|
||||||
+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.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script Master — what is being bought (a Govt Security, bond, mutual fund scheme, share, …), following the bank's
|
||||||
|
* existing CBS Script Master. A purchase picks these terms up instead of the dealer re-typing them.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_script")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentScript extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
/** System-generated: bond-type letter + running number, e.g. G00001. */
|
||||||
|
@Column(name = "script_code", nullable = false, length = 20, unique = true)
|
||||||
|
private String scriptCode;
|
||||||
|
|
||||||
|
@Column(name = "bond_type", nullable = false, length = 20)
|
||||||
|
private String bondType;
|
||||||
|
|
||||||
|
/** GOI / STATE_GOVT / TREASURY / NON_SLR. */
|
||||||
|
@Column(name = "slr_type", length = 20)
|
||||||
|
private String slrType;
|
||||||
|
|
||||||
|
@Column(name = "script_desc", nullable = false, length = 150)
|
||||||
|
private String scriptDesc;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||||
|
private GLAccount glAccount;
|
||||||
|
|
||||||
|
@Column(name = "isin", length = 20)
|
||||||
|
private String isin;
|
||||||
|
|
||||||
|
@Column(name = "face_value_per_unit", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal faceValuePerUnit;
|
||||||
|
|
||||||
|
@Column(name = "int_rate", scale = 4, precision = 9)
|
||||||
|
private BigDecimal intRate;
|
||||||
|
|
||||||
|
@Column(name = "issue_date")
|
||||||
|
private LocalDate issueDate;
|
||||||
|
|
||||||
|
@Column(name = "maturity_date")
|
||||||
|
private LocalDate maturityDate;
|
||||||
|
|
||||||
|
@Column(name = "company_name", length = 150)
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
@Column(name = "address1", length = 150)
|
||||||
|
private String address1;
|
||||||
|
|
||||||
|
@Column(name = "address2", length = 150)
|
||||||
|
private String address2;
|
||||||
|
|
||||||
|
@Column(name = "address3", length = 150)
|
||||||
|
private String address3;
|
||||||
|
|
||||||
|
@Column(name = "city", length = 100)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Column(name = "pincode", length = 10)
|
||||||
|
private String pincode;
|
||||||
|
|
||||||
|
@Column(name = "phone", length = 20)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "fax", length = 20)
|
||||||
|
private String fax;
|
||||||
|
|
||||||
|
@Column(name = "corp_code", length = 30)
|
||||||
|
private String corpCode;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {}
|
||||||
+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.investment.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class InvestmentPartyNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public InvestmentPartyNotFoundException(final Long id) {
|
||||||
|
super("error.msg.investmentparty.id.invalid", "Investment party 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.investment.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class InvestmentPurchaseNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public InvestmentPurchaseNotFoundException(final Long id) {
|
||||||
|
super("error.msg.investmentpurchase.id.invalid", "Investment purchase 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.investment.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class InvestmentScriptNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public InvestmentScriptNotFoundException(final Long id) {
|
||||||
|
super("error.msg.investmentscript.id.invalid", "Investment script 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.investment.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.investment.service.InvestmentMasterWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTPARTY", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CreateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createParty(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.investment.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.investment.service.InvestmentPurchaseWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTPURCHASE", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CreateInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createPurchase(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.investment.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.investment.service.InvestmentMasterWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSCRIPT", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CreateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createScript(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.investment.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.investment.service.InvestmentMasterWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTPARTY", action = "DELETE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeleteInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.deleteParty(command.entityId());
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.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.investment.service.InvestmentPurchaseWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTPURCHASE", action = "DELETE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeleteInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.deletePurchase(command.entityId());
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.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.investment.service.InvestmentMasterWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTPARTY", action = "UPDATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UpdateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.updateParty(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.investment.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.investment.service.InvestmentMasterWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSCRIPT", action = "UPDATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UpdateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.updateScript(command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+254
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.serialization;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
||||||
|
|
||||||
|
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.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Validates Bank / Party Master and Script Master requests before anything is written. */
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
||||||
|
|
||||||
|
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
|
||||||
|
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
// -------------------- Bank / Party Master --------------------
|
||||||
|
|
||||||
|
public void validateForCreateParty(final String json) {
|
||||||
|
validateParty(json, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForUpdateParty(final String json) {
|
||||||
|
validateParty(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateParty(final String json, final boolean create) {
|
||||||
|
final JsonElement element = parse(json, PARTY_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
|
||||||
|
|
||||||
|
validateBondType(element, v, create);
|
||||||
|
validateRequiredString(element, v, PARTY_NAME, 150, create);
|
||||||
|
validateGlAccountId(element, v, create);
|
||||||
|
validateAddress(element, v);
|
||||||
|
validateOptionalString(element, v, BANK_TYPE, 60);
|
||||||
|
validateOptionalString(element, v, RBI_CODE, 30);
|
||||||
|
validateOptionalString(element, v, LEI_NO, 30);
|
||||||
|
validateOptionalString(element, v, CP_BANK_NAME, 150);
|
||||||
|
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
|
||||||
|
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
|
||||||
|
|
||||||
|
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
|
||||||
|
if (StringUtils.isNotBlank(ifsc)) {
|
||||||
|
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
|
||||||
|
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
|
||||||
|
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
||||||
|
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
|
}
|
||||||
|
throwIfErrors(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Script Master --------------------
|
||||||
|
|
||||||
|
public void validateForCreateScript(final String json) {
|
||||||
|
validateScript(json, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForUpdateScript(final String json, final String existingBondType) {
|
||||||
|
validateScript(json, false, existingBondType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateScript(final String json, final boolean create) {
|
||||||
|
validateScript(json, create, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateScript(final String json, final boolean create, final String existingBondType) {
|
||||||
|
final JsonElement element = parse(json, SCRIPT_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
|
||||||
|
|
||||||
|
validateBondType(element, v, create);
|
||||||
|
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
|
||||||
|
validateGlAccountId(element, v, create);
|
||||||
|
|
||||||
|
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
|
||||||
|
if (StringUtils.isNotBlank(slrType)) {
|
||||||
|
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
|
||||||
|
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
|
||||||
|
|
||||||
|
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
|
||||||
|
if (create && debt) {
|
||||||
|
v.reset().parameter(ISIN).value(isin).notBlank();
|
||||||
|
}
|
||||||
|
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
|
||||||
|
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
|
||||||
|
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
|
||||||
|
if (create && debt) {
|
||||||
|
rate.notNull();
|
||||||
|
}
|
||||||
|
rate.ignoreIfNull().zeroOrPositiveAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
|
||||||
|
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
|
||||||
|
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate issueDate = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
|
||||||
|
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
|
||||||
|
if (create && debt) {
|
||||||
|
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element)) {
|
||||||
|
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
|
||||||
|
v.reset().parameter(MATURITY_DATE).value(maturityDate).ignoreIfNull().validateDateAfter(issueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateOptionalString(element, v, COMPANY_NAME, 150);
|
||||||
|
validateOptionalString(element, v, CORP_CODE, 30);
|
||||||
|
validateAddress(element, v);
|
||||||
|
throwIfErrors(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- shared --------------------
|
||||||
|
|
||||||
|
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||||
|
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||||
|
if (required) {
|
||||||
|
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(bondType)) {
|
||||||
|
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||||
|
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
|
||||||
|
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
|
||||||
|
final boolean required) {
|
||||||
|
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||||
|
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
|
||||||
|
final int maxLength) {
|
||||||
|
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||||
|
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
|
||||||
|
validateOptionalString(element, v, ADDRESS1, 150);
|
||||||
|
validateOptionalString(element, v, ADDRESS2, 150);
|
||||||
|
validateOptionalString(element, v, ADDRESS3, 150);
|
||||||
|
validateOptionalString(element, v, CITY, 100);
|
||||||
|
validateOptionalString(element, v, PHONE, 20);
|
||||||
|
validateOptionalString(element, v, FAX, 20);
|
||||||
|
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
|
||||||
|
if (StringUtils.isNotBlank(pincode)) {
|
||||||
|
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.serialization;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORIES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.RESOURCE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.investment.api.InvestmentApiConstants;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Validates a Purchase Entry before anything is computed or written. */
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
||||||
|
|
||||||
|
private static final String TIME_PATTERN = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
public void validateForCreate(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
|
||||||
|
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||||
|
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||||
|
if (StringUtils.isNotBlank(bondType)) {
|
||||||
|
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(InvestmentApiConstants.BOND_TYPES.toArray());
|
||||||
|
}
|
||||||
|
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||||
|
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
|
||||||
|
v.reset().parameter(CR_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(CR_GL_ACCOUNT_ID, element)).notNull()
|
||||||
|
.longGreaterThanZero();
|
||||||
|
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
|
||||||
|
|
||||||
|
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
|
||||||
|
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
|
||||||
|
if (StringUtils.isNotBlank(settlementType)) {
|
||||||
|
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).isOneOfTheseValues(SETTLEMENT_TYPES.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String category = this.fromApiJsonHelper.extractStringNamed(CATEGORY, element);
|
||||||
|
v.reset().parameter(CATEGORY).value(category).notBlank();
|
||||||
|
if (StringUtils.isNotBlank(category)) {
|
||||||
|
v.reset().parameter(CATEGORY).value(category).isOneOfTheseValues(CATEGORIES.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("MUTUAL_FUND".equals(bondType)) {
|
||||||
|
v.reset().parameter(CONSIDER_AMT).value(amount(CONSIDER_AMT, element)).notNull().positiveAmount();
|
||||||
|
v.reset().parameter(NAV).value(amount(NAV, element)).notNull().positiveAmount();
|
||||||
|
} else {
|
||||||
|
v.reset().parameter(FACE_VALUE).value(amount(FACE_VALUE, element)).notNull().positiveAmount();
|
||||||
|
v.reset().parameter(PURCHASE_RATE).value(amount(PURCHASE_RATE, element)).notNull().positiveAmount();
|
||||||
|
v.reset().parameter(LAST_INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(LAST_INT_DATE, element)).notNull();
|
||||||
|
}
|
||||||
|
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
|
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
|
|
||||||
|
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
|
||||||
|
if (StringUtils.isNotBlank(dealTime)) {
|
||||||
|
v.reset().parameter(DEAL_TIME).value(dealTime).matchesRegularExpression(TIME_PATTERN, "Deal Time must be HH:mm:ss (24 hr)");
|
||||||
|
}
|
||||||
|
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(40);
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
|
||||||
|
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
|
||||||
|
if (!other.isJsonNull() && !other.isJsonObject()) {
|
||||||
|
v.reset().parameter(OTHER_DETAILS).failWithCode("must.be.an.object");
|
||||||
|
} else if (other.isJsonObject() && other.toString().length() > 4000) {
|
||||||
|
v.reset().parameter(OTHER_DETAILS).failWithCode("too.long");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal amount(final String param, final JsonElement element) {
|
||||||
|
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||||
|
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.Month;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interest arithmetic for half-yearly coupon securities, as the bank's existing CBS does it (Jitendra Sir):
|
||||||
|
* <ul>
|
||||||
|
* <li>days are counted 30/360 — every month is 30 days, a half-year is 180, a year 360;</li>
|
||||||
|
* <li>the coupon is fixed: face value × rate ÷ 2, paid on the maturity day/month and six months either side;</li>
|
||||||
|
* <li>at each closing (31 March, 30 September) the interest earned since the last coupon is booked as a provision (Dr
|
||||||
|
* Interest Receivable / Cr Interest Income), even though the cash arrives at the next coupon;</li>
|
||||||
|
* <li>when the coupon arrives, the provision already booked is cleared and only the rest is new income.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class InvestmentInterestCalculator {
|
||||||
|
|
||||||
|
private static final BigDecimal THIRTY_SIX_THOUSAND = BigDecimal.valueOf(36000);
|
||||||
|
private static final BigDecimal TWO_HUNDRED = BigDecimal.valueOf(200);
|
||||||
|
|
||||||
|
public static final String TYPE_PROVISION = "PROVISION";
|
||||||
|
public static final String TYPE_COUPON = "COUPON";
|
||||||
|
|
||||||
|
private InvestmentInterestCalculator() {}
|
||||||
|
|
||||||
|
/** One line of the interest schedule. */
|
||||||
|
public record ScheduleRow(String type, LocalDate date, LocalDate fromDate, LocalDate toDate, int days, BigDecimal grossInterest,
|
||||||
|
BigDecimal alreadyBooked, BigDecimal amount) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Days between two dates on a 30/360 basis. Counting runs from the day after {@code from} up to and including
|
||||||
|
* {@code to}: 10-08 → 30-09 is 50 days, 10-08 → 08-10 is 58 days, 30-06 → 30-09 is 90 days.
|
||||||
|
*/
|
||||||
|
public static int days360(final LocalDate from, final LocalDate to) {
|
||||||
|
if (from == null || to == null || !to.isAfter(from)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final int d1 = Math.min(from.getDayOfMonth(), 30);
|
||||||
|
final int d2 = Math.min(to.getDayOfMonth(), 30);
|
||||||
|
return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), rounded to the rupee. */
|
||||||
|
public static BigDecimal interestForDays(final BigDecimal faceValue, final BigDecimal ratePercent, final int days) {
|
||||||
|
return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 0, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fixed half-yearly coupon: face value × rate ÷ 2. */
|
||||||
|
public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) {
|
||||||
|
return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 0, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */
|
||||||
|
private static LocalDate couponBack(final LocalDate maturity, final int k) {
|
||||||
|
return maturity.minusMonths(6L * k);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The most recent coupon date on or before {@code on}. */
|
||||||
|
public static LocalDate lastCouponOnOrBefore(final LocalDate maturity, final LocalDate on) {
|
||||||
|
int k = 0;
|
||||||
|
LocalDate d = maturity;
|
||||||
|
while (d.isAfter(on)) {
|
||||||
|
k++;
|
||||||
|
d = couponBack(maturity, k);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coupon dates strictly after {@code from}, up to and including maturity, in date order. */
|
||||||
|
public static List<LocalDate> couponDatesAfter(final LocalDate maturity, final LocalDate from) {
|
||||||
|
final List<LocalDate> dates = new ArrayList<>();
|
||||||
|
int k = 0;
|
||||||
|
LocalDate d = maturity;
|
||||||
|
while (d.isAfter(from)) {
|
||||||
|
dates.add(0, d);
|
||||||
|
k++;
|
||||||
|
d = couponBack(maturity, k);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closing dates (31 March, 30 September) from {@code from} up to {@code to}, inclusive. */
|
||||||
|
public static List<LocalDate> closingDates(final LocalDate from, final LocalDate to) {
|
||||||
|
final List<LocalDate> dates = new ArrayList<>();
|
||||||
|
for (int year = from.getYear(); year <= to.getYear(); year++) {
|
||||||
|
for (final LocalDate closing : List.of(LocalDate.of(year, Month.MARCH, 31), LocalDate.of(year, Month.SEPTEMBER, 30))) {
|
||||||
|
if (!closing.isBefore(from) && !closing.isAfter(to)) {
|
||||||
|
dates.add(closing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full interest schedule of a holding, from purchase to maturity.
|
||||||
|
*
|
||||||
|
* @param faceValue
|
||||||
|
* face value currently held
|
||||||
|
* @param ratePercent
|
||||||
|
* coupon rate % p.a.
|
||||||
|
* @param maturity
|
||||||
|
* maturity date (coupons fall on its day/month and six months either side)
|
||||||
|
* @param lastIntDate
|
||||||
|
* coupon date immediately before the purchase
|
||||||
|
* @param dealDate
|
||||||
|
* purchase deal date — closings before it belong to the seller
|
||||||
|
* @param purchasedAccrued
|
||||||
|
* interest paid to the seller at purchase; it already sits in Interest Receivable
|
||||||
|
*/
|
||||||
|
public static List<ScheduleRow> schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity,
|
||||||
|
final LocalDate lastIntDate, final LocalDate dealDate, final BigDecimal purchasedAccrued) {
|
||||||
|
final List<ScheduleRow> rows = new ArrayList<>();
|
||||||
|
if (faceValue == null || ratePercent == null || ratePercent.signum() <= 0 || maturity == null || lastIntDate == null
|
||||||
|
|| dealDate == null || !maturity.isAfter(lastIntDate)) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
final List<LocalDate> coupons = couponDatesAfter(maturity, lastIntDate);
|
||||||
|
final TreeSet<LocalDate> events = new TreeSet<>(coupons);
|
||||||
|
for (final LocalDate closing : closingDates(dealDate, maturity)) {
|
||||||
|
if (closing.isAfter(lastIntDate) && !events.contains(closing)) {
|
||||||
|
events.add(closing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal coupon = halfYearCoupon(faceValue, ratePercent);
|
||||||
|
BigDecimal receivable = purchasedAccrued == null ? BigDecimal.ZERO : purchasedAccrued;
|
||||||
|
LocalDate previousCoupon = lastIntDate;
|
||||||
|
for (final LocalDate date : events) {
|
||||||
|
final int days = days360(previousCoupon, date);
|
||||||
|
if (coupons.contains(date)) {
|
||||||
|
rows.add(new ScheduleRow(TYPE_COUPON, date, previousCoupon.plusDays(1), date, days, coupon, receivable,
|
||||||
|
coupon.subtract(receivable)));
|
||||||
|
receivable = BigDecimal.ZERO;
|
||||||
|
previousCoupon = date;
|
||||||
|
} else {
|
||||||
|
final BigDecimal earned = interestForDays(faceValue, ratePercent, days);
|
||||||
|
rows.add(new ScheduleRow(TYPE_PROVISION, date, previousCoupon.plusDays(1), date, days, earned, receivable,
|
||||||
|
earned.subtract(receivable)));
|
||||||
|
receivable = earned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||||
|
|
||||||
|
public interface InvestmentMasterReadPlatformService {
|
||||||
|
|
||||||
|
List<InvestmentPartyData> retrieveAllParties();
|
||||||
|
|
||||||
|
InvestmentPartyData retrieveParty(Long partyId);
|
||||||
|
|
||||||
|
List<InvestmentScriptData> retrieveAllScripts();
|
||||||
|
|
||||||
|
InvestmentScriptData retrieveScript(Long scriptId);
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/** Masters are small (tens of rows), so they are listed whole, newest first, the way the screens show them. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class InvestmentMasterReadPlatformServiceImpl implements InvestmentMasterReadPlatformService {
|
||||||
|
|
||||||
|
private static final Sort NEWEST_FIRST = Sort.by(Sort.Direction.DESC, "id");
|
||||||
|
|
||||||
|
private final InvestmentPartyRepository partyRepository;
|
||||||
|
private final InvestmentScriptRepository scriptRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<InvestmentPartyData> retrieveAllParties() {
|
||||||
|
return this.partyRepository.findAll(NEWEST_FIRST).stream().map(InvestmentPartyData::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvestmentPartyData retrieveParty(final Long partyId) {
|
||||||
|
return this.partyRepository.findById(partyId).map(InvestmentPartyData::from)
|
||||||
|
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<InvestmentScriptData> retrieveAllScripts() {
|
||||||
|
return this.scriptRepository.findAll(NEWEST_FIRST).stream().map(InvestmentScriptData::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvestmentScriptData retrieveScript(final Long scriptId) {
|
||||||
|
return this.scriptRepository.findById(scriptId).map(InvestmentScriptData::from)
|
||||||
|
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
|
public interface InvestmentMasterWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult createParty(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult updateParty(Long partyId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult deleteParty(Long partyId);
|
||||||
|
|
||||||
|
CommandProcessingResult createScript(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult updateScript(Long scriptId, JsonCommand command);
|
||||||
|
}
|
||||||
+298
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_CODE_PREFIX;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
|
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.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.serialization.InvestmentMasterCommandFromApiJsonDeserializer;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMasterWritePlatformService {
|
||||||
|
|
||||||
|
private final InvestmentMasterCommandFromApiJsonDeserializer validator;
|
||||||
|
private final InvestmentPartyRepository partyRepository;
|
||||||
|
private final InvestmentScriptRepository scriptRepository;
|
||||||
|
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
// -------------------- Bank / Party Master --------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult createParty(final JsonCommand command) {
|
||||||
|
this.validator.validateForCreateParty(command.json());
|
||||||
|
final InvestmentParty party = new InvestmentParty();
|
||||||
|
party.setPartyCode(nextPartyCode());
|
||||||
|
applyParty(party, command);
|
||||||
|
if (command.parameterExists(ACTIVE)) {
|
||||||
|
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||||
|
}
|
||||||
|
final InvestmentParty saved = saveParty(party);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||||
|
.withResourceIdAsString(saved.getPartyCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
|
||||||
|
this.validator.validateForUpdateParty(command.json());
|
||||||
|
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||||
|
applyParty(party, command);
|
||||||
|
if (command.parameterExists(ACTIVE)) {
|
||||||
|
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||||
|
}
|
||||||
|
saveParty(party);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId)
|
||||||
|
.withResourceIdAsString(party.getPartyCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult deleteParty(final Long partyId) {
|
||||||
|
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||||
|
this.partyRepository.delete(party);
|
||||||
|
this.partyRepository.flush();
|
||||||
|
return new CommandProcessingResultBuilder().withEntityId(partyId).withResourceIdAsString(party.getPartyCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
|
||||||
|
private void applyParty(final InvestmentParty p, final JsonCommand command) {
|
||||||
|
setIfPresent(command, BOND_TYPE, p::setBondType);
|
||||||
|
setIfPresent(command, PARTY_NAME, p::setPartyName);
|
||||||
|
setIfPresent(command, ADDRESS1, p::setAddress1);
|
||||||
|
setIfPresent(command, ADDRESS2, p::setAddress2);
|
||||||
|
setIfPresent(command, ADDRESS3, p::setAddress3);
|
||||||
|
setIfPresent(command, CITY, p::setCity);
|
||||||
|
setIfPresent(command, PINCODE, p::setPincode);
|
||||||
|
setIfPresent(command, PHONE, p::setPhone);
|
||||||
|
setIfPresent(command, FAX, p::setFax);
|
||||||
|
setIfPresent(command, BANK_TYPE, p::setBankType);
|
||||||
|
setIfPresent(command, RBI_CODE, p::setRbiCode);
|
||||||
|
setIfPresent(command, LEI_NO, p::setLeiNo);
|
||||||
|
setIfPresent(command, CP_BANK_NAME, p::setCpBankName);
|
||||||
|
setIfPresent(command, CP_BRANCH_NAME, p::setCpBranchName);
|
||||||
|
setIfPresent(command, CP_ACCOUNT_NO, p::setCpAccountNo);
|
||||||
|
if (command.parameterExists(CP_IFSC)) {
|
||||||
|
final String ifsc = command.stringValueOfParameterNamedAllowingNull(CP_IFSC);
|
||||||
|
p.setCpIfsc(StringUtils.isBlank(ifsc) ? null : ifsc.trim().toUpperCase());
|
||||||
|
}
|
||||||
|
if (command.parameterExists(EXPOSURE_LIMIT)) {
|
||||||
|
p.setExposureLimit(command.bigDecimalValueOfParameterNamed(EXPOSURE_LIMIT));
|
||||||
|
}
|
||||||
|
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
||||||
|
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentParty saveParty(final InvestmentParty party) {
|
||||||
|
try {
|
||||||
|
return this.partyRepository.saveAndFlush(party);
|
||||||
|
} catch (final DataIntegrityViolationException e) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.investmentparty.duplicate", "A party with this code already exists",
|
||||||
|
"partyCode", party.getPartyCode(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Next code in the PTY-001, PTY-002 … series. */
|
||||||
|
private String nextPartyCode() {
|
||||||
|
final Integer max = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
|
||||||
|
Integer.class);
|
||||||
|
return PARTY_CODE_PREFIX + String.format("%03d", (max == null ? 0 : max) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Script Master --------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult createScript(final JsonCommand command) {
|
||||||
|
this.validator.validateForCreateScript(command.json());
|
||||||
|
final InvestmentScript script = new InvestmentScript();
|
||||||
|
script.setBondType(command.stringValueOfParameterNamed(BOND_TYPE));
|
||||||
|
script.setScriptCode(nextScriptCode(script.getBondType()));
|
||||||
|
script.setFaceValuePerUnit(defaultFaceValue(script.getBondType()));
|
||||||
|
applyScript(script, command);
|
||||||
|
if (command.parameterExists(ACTIVE)) {
|
||||||
|
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||||
|
}
|
||||||
|
final InvestmentScript saved = saveScript(script);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||||
|
.withResourceIdAsString(saved.getScriptCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult updateScript(final Long scriptId, final JsonCommand command) {
|
||||||
|
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
||||||
|
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||||
|
this.validator.validateForUpdateScript(command.json(), script.getBondType());
|
||||||
|
if (command.parameterExists(BOND_TYPE) && !script.getBondType().equals(command.stringValueOfParameterNamed(BOND_TYPE))) {
|
||||||
|
// The script code carries the bond-type letter (G00001), so the type cannot change once created.
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.bondtype.immutable",
|
||||||
|
"Bond Type of an existing script cannot be changed", script.getScriptCode());
|
||||||
|
}
|
||||||
|
applyScript(script, command);
|
||||||
|
if (command.parameterExists(ACTIVE)) {
|
||||||
|
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||||
|
}
|
||||||
|
saveScript(script);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scriptId)
|
||||||
|
.withResourceIdAsString(script.getScriptCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyScript(final InvestmentScript s, final JsonCommand command) {
|
||||||
|
setIfPresent(command, SLR_TYPE, s::setSlrType);
|
||||||
|
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
|
||||||
|
setIfPresent(command, ISIN, s::setIsin);
|
||||||
|
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
|
||||||
|
setIfPresent(command, ADDRESS1, s::setAddress1);
|
||||||
|
setIfPresent(command, ADDRESS2, s::setAddress2);
|
||||||
|
setIfPresent(command, ADDRESS3, s::setAddress3);
|
||||||
|
setIfPresent(command, CITY, s::setCity);
|
||||||
|
setIfPresent(command, PINCODE, s::setPincode);
|
||||||
|
setIfPresent(command, PHONE, s::setPhone);
|
||||||
|
setIfPresent(command, FAX, s::setFax);
|
||||||
|
setIfPresent(command, CORP_CODE, s::setCorpCode);
|
||||||
|
if (command.parameterExists(FACE_VALUE_PER_UNIT) && command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT) != null) {
|
||||||
|
s.setFaceValuePerUnit(command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT));
|
||||||
|
}
|
||||||
|
if (command.parameterExists(INT_RATE)) {
|
||||||
|
s.setIntRate(command.bigDecimalValueOfParameterNamed(INT_RATE));
|
||||||
|
}
|
||||||
|
if (command.parameterExists(ISSUE_DATE)) {
|
||||||
|
s.setIssueDate(command.localDateValueOfParameterNamed(ISSUE_DATE));
|
||||||
|
}
|
||||||
|
if (command.parameterExists(MATURITY_DATE)) {
|
||||||
|
s.setMaturityDate(command.localDateValueOfParameterNamed(MATURITY_DATE));
|
||||||
|
}
|
||||||
|
if (s.getIssueDate() != null && s.getMaturityDate() != null && s.getMaturityDate().isBefore(s.getIssueDate())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.maturity.before.issue",
|
||||||
|
"Maturity date cannot be before the issue date");
|
||||||
|
}
|
||||||
|
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
||||||
|
s.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentScript saveScript(final InvestmentScript script) {
|
||||||
|
try {
|
||||||
|
return this.scriptRepository.saveAndFlush(script);
|
||||||
|
} catch (final DataIntegrityViolationException e) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.investmentscript.duplicate", "A script with this code already exists",
|
||||||
|
"scriptCode", script.getScriptCode(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Next code for the bond type — its letter plus a five-digit running number, e.g. G00001. */
|
||||||
|
private String nextScriptCode(final String bondType) {
|
||||||
|
final String prefix = SCRIPT_CODE_PREFIX.get(bondType);
|
||||||
|
final Integer max = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(CAST(SUBSTRING(script_code FROM 2) AS INTEGER)) FROM m_investment_script WHERE script_code ~ ?",
|
||||||
|
Integer.class, "^" + prefix + "[0-9]{5}$");
|
||||||
|
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Face value of one unit: ₹100 for securities and bonds, as the existing CBS defaults it. */
|
||||||
|
private BigDecimal defaultFaceValue(final String bondType) {
|
||||||
|
return switch (bondType) {
|
||||||
|
case "MUTUAL_FUND" -> BigDecimal.valueOf(1000);
|
||||||
|
case "SHARE", "FD" -> BigDecimal.ONE;
|
||||||
|
default -> BigDecimal.valueOf(100);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- shared --------------------
|
||||||
|
|
||||||
|
/** An investment is an asset, so the mapped GL must be an enabled ASSET detail account. */
|
||||||
|
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||||
|
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||||
|
if (gl.isHeaderAccount()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.header",
|
||||||
|
"GL " + gl.getGlCode() + " is a header account; choose a detail account", glAccountId);
|
||||||
|
}
|
||||||
|
if (gl.isDisabled()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.disabled", "GL " + gl.getGlCode() + " is disabled",
|
||||||
|
glAccountId);
|
||||||
|
}
|
||||||
|
if (!GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.not.asset",
|
||||||
|
"GL " + gl.getGlCode() + " is not an Asset account", glAccountId);
|
||||||
|
}
|
||||||
|
return gl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setIfPresent(final JsonCommand command, final String param, final Consumer<String> setter) {
|
||||||
|
if (command.parameterExists(param)) {
|
||||||
|
final String value = command.stringValueOfParameterNamedAllowingNull(param);
|
||||||
|
setter.accept(StringUtils.isBlank(value) ? null : value.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class InvestmentPurchaseReadPlatformService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
/** Income head for coupon and provision interest (GL 400401 under 400400 INCOME FROM INVESTMENTS). */
|
||||||
|
private static final String INTEREST_GL_CODE = "400401";
|
||||||
|
private static final String INTEREST_GL_NAME = "INTEREST ON GOVT SECURITIES";
|
||||||
|
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
|
||||||
|
/** All deals, newest first — the Holdings register. */
|
||||||
|
public List<InvestmentPurchaseData> retrieveAll() {
|
||||||
|
return this.purchaseRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentPurchaseData::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvestmentPurchaseData retrieveOne(final Long purchaseId) {
|
||||||
|
return InvestmentPurchaseData.from(find(purchaseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coupon and closing-provision lines of a holding from purchase to maturity, worked out on the face value still
|
||||||
|
* held. Lines on or before {@code asOnDate} (default: business date) are marked due.
|
||||||
|
*/
|
||||||
|
public InvestmentInterestScheduleData retrieveInterestSchedule(final Long purchaseId, final LocalDate asOnDate) {
|
||||||
|
final InvestmentPurchase p = find(purchaseId);
|
||||||
|
final LocalDate asOn = asOnDate == null ? DateUtils.getBusinessLocalDate() : asOnDate;
|
||||||
|
final BigDecimal unitValue = p.getScript().getFaceValuePerUnit();
|
||||||
|
final BigDecimal faceHeld = p.getBalanceUnits().multiply(unitValue);
|
||||||
|
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||||
|
|
||||||
|
final List<InvestmentInterestScheduleData.Row> rows = InvestmentInterestCalculator
|
||||||
|
.schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate(), p.getAccruedInterest()).stream()
|
||||||
|
.map(r -> InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date()))
|
||||||
|
.fromDate(format(r.fromDate())).toDate(format(r.toDate())).days(r.days()).grossInterest(r.grossInterest())
|
||||||
|
.alreadyBooked(r.alreadyBooked()).amount(r.amount()).due(!r.date().isAfter(asOn)).build())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return InvestmentInterestScheduleData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||||
|
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).faceValue(faceHeld)
|
||||||
|
.unitValue(unitValue).balanceUnits(p.getBalanceUnits()).purchaseRate(p.getPurchaseRate()).bookValue(p.getBookValue())
|
||||||
|
.intRate(p.getIntRate()).dealDate(format(p.getDealDate())).lastIntDate(format(p.getLastIntDate()))
|
||||||
|
.maturityDate(format(p.getMaturityDate())).purchasedAccruedInterest(p.getAccruedInterest())
|
||||||
|
.couponPerHalfYear(rate.signum() > 0 ? InvestmentInterestCalculator.halfYearCoupon(faceHeld, rate) : BigDecimal.ZERO)
|
||||||
|
.asOnDate(format(asOn)).interestGlCode(INTEREST_GL_CODE).interestGlName(INTEREST_GL_NAME).rows(rows).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentPurchase find(final Long purchaseId) {
|
||||||
|
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.investment.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
|
public interface InvestmentPurchaseWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult createPurchase(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult deletePurchase(Long purchaseId);
|
||||||
|
}
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
|
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.portfolio.investment.domain.InvestmentParty;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.serialization.InvestmentPurchaseCommandFromApiJsonDeserializer;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a Purchase Entry as a holding. Every derived figure is computed here from the script master and the deal
|
||||||
|
* inputs, so the stored deal does not depend on what the screen calculated. No GL entry is posted yet.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPurchaseWritePlatformService {
|
||||||
|
|
||||||
|
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||||
|
|
||||||
|
private final InvestmentPurchaseCommandFromApiJsonDeserializer validator;
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentScriptRepository scriptRepository;
|
||||||
|
private final InvestmentPartyRepository partyRepository;
|
||||||
|
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult createPurchase(final JsonCommand command) {
|
||||||
|
this.validator.validateForCreate(command.json());
|
||||||
|
|
||||||
|
final String bondType = command.stringValueOfParameterNamed(BOND_TYPE);
|
||||||
|
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
||||||
|
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
||||||
|
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||||
|
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
|
||||||
|
final InvestmentParty party = this.partyRepository.findById(partyId)
|
||||||
|
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||||
|
if (!script.isActive()) {
|
||||||
|
throw rule("script.inactive", "Script " + script.getScriptCode() + " is inactive");
|
||||||
|
}
|
||||||
|
if (!script.getBondType().equals(bondType)) {
|
||||||
|
throw rule("script.bondtype.mismatch",
|
||||||
|
"Script " + script.getScriptCode() + " is a " + script.getBondType() + ", not " + bondType);
|
||||||
|
}
|
||||||
|
if (!party.isActive()) {
|
||||||
|
throw rule("party.inactive", "Party " + party.getPartyCode() + " is inactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
|
||||||
|
if (script.getMaturityDate() != null && dealDate.isAfter(script.getMaturityDate())) {
|
||||||
|
throw rule("deal.after.maturity", "Deal date is after the security's maturity date " + script.getMaturityDate());
|
||||||
|
}
|
||||||
|
final String settlementType = command.stringValueOfParameterNamed(SETTLEMENT_TYPE);
|
||||||
|
|
||||||
|
final InvestmentPurchase p = new InvestmentPurchase();
|
||||||
|
p.setDealNo(nextDealNo(dealDate));
|
||||||
|
p.setCertNo(nextCertNo());
|
||||||
|
p.setBondType(bondType);
|
||||||
|
p.setScript(script);
|
||||||
|
p.setParty(party);
|
||||||
|
p.setDealDate(dealDate);
|
||||||
|
p.setDealTime(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(DEAL_TIME)));
|
||||||
|
p.setSettlementType(settlementType);
|
||||||
|
p.setSettlementDate(dealDate.plusDays(Long.parseLong(settlementType.substring(2))));
|
||||||
|
p.setCategory(command.stringValueOfParameterNamed(CATEGORY));
|
||||||
|
p.setSubType(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(SUB_TYPE)));
|
||||||
|
p.setIntRate(script.getIntRate());
|
||||||
|
p.setIssueDate(script.getIssueDate());
|
||||||
|
p.setMaturityDate(script.getMaturityDate());
|
||||||
|
p.setInvestmentGlAccount(script.getGlAccount());
|
||||||
|
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
|
||||||
|
p.setOtherDetails(otherDetails(command));
|
||||||
|
p.setStatus(STATUS_ACTIVE);
|
||||||
|
|
||||||
|
final BigDecimal brokerage = zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE));
|
||||||
|
p.setBrokerage(brokerage);
|
||||||
|
if ("MUTUAL_FUND".equals(bondType)) {
|
||||||
|
computeMutualFund(p, command, brokerage);
|
||||||
|
} else {
|
||||||
|
computeSecurity(p, command, script, brokerage);
|
||||||
|
}
|
||||||
|
|
||||||
|
final InvestmentPurchase saved = this.purchaseRepository.saveAndFlush(p);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||||
|
.withResourceIdAsString(saved.getDealNo()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult deletePurchase(final Long purchaseId) {
|
||||||
|
final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId)
|
||||||
|
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
if (p.getBalanceUnits().compareTo(p.getUnits()) != 0) {
|
||||||
|
throw rule("purchase.partly.sold", "Deal " + p.getDealNo() + " has been partly sold and cannot be dropped");
|
||||||
|
}
|
||||||
|
this.purchaseRepository.delete(p);
|
||||||
|
this.purchaseRepository.flush();
|
||||||
|
return new CommandProcessingResultBuilder().withEntityId(purchaseId).withResourceIdAsString(p.getDealNo()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */
|
||||||
|
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script,
|
||||||
|
final BigDecimal brokerage) {
|
||||||
|
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
|
||||||
|
final BigDecimal rate = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
|
||||||
|
final LocalDate lastIntDate = command.localDateValueOfParameterNamed(LAST_INT_DATE);
|
||||||
|
if (lastIntDate.isAfter(p.getDealDate())) {
|
||||||
|
throw rule("lastint.after.deal", "Last interest date cannot be after the deal date");
|
||||||
|
}
|
||||||
|
final BigDecimal units = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP);
|
||||||
|
final BigDecimal cost = faceValue.multiply(rate).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||||
|
final int days = InvestmentInterestCalculator.days360(lastIntDate, p.getDealDate());
|
||||||
|
final BigDecimal intRate = zeroIfNull(script.getIntRate());
|
||||||
|
// Interest is counted up to the deal date (the event), not the settlement date — Jitendra Sir.
|
||||||
|
final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(faceValue, intRate, days);
|
||||||
|
|
||||||
|
p.setFaceValue(faceValue);
|
||||||
|
p.setPurchaseRate(rate);
|
||||||
|
p.setUnits(units);
|
||||||
|
p.setBalanceUnits(units);
|
||||||
|
p.setBookValue(cost.add(brokerage));
|
||||||
|
p.setPremiumDiscount(faceValue.multiply(rate.subtract(HUNDRED)).divide(HUNDRED, 2, RoundingMode.HALF_UP));
|
||||||
|
p.setLastIntDate(lastIntDate);
|
||||||
|
p.setAccruedDays(days);
|
||||||
|
p.setAccruedInterest(accrued);
|
||||||
|
p.setSettlementAmount(p.getBookValue().add(accrued));
|
||||||
|
p.setYtm(approxYtm(intRate, rate, script.getIssueDate(), script.getMaturityDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mutual fund: the amount invested buys units at the NAV; stamp duty and brokerage add to the cost. */
|
||||||
|
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command, final BigDecimal brokerage) {
|
||||||
|
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
|
||||||
|
final BigDecimal nav = command.bigDecimalValueOfParameterNamed(NAV);
|
||||||
|
final BigDecimal stampDuty = zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY));
|
||||||
|
final BigDecimal units = amount.divide(nav, 3, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
p.setFaceValue(amount);
|
||||||
|
p.setPurchaseRate(nav);
|
||||||
|
p.setUnits(units);
|
||||||
|
p.setBalanceUnits(units);
|
||||||
|
p.setStampDuty(stampDuty);
|
||||||
|
p.setBookValue(amount.add(stampDuty).add(brokerage));
|
||||||
|
p.setAccruedInterest(BigDecimal.ZERO);
|
||||||
|
p.setAccruedDays(0);
|
||||||
|
p.setSettlementAmount(p.getBookValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approximate yield: coupon adjusted for the discount/premium spread over the security's life, over the price. */
|
||||||
|
static BigDecimal approxYtm(final BigDecimal coupon, final BigDecimal price, final LocalDate issue, final LocalDate maturity) {
|
||||||
|
if (coupon.signum() == 0 || issue == null || maturity == null || !maturity.isAfter(issue)) {
|
||||||
|
return coupon;
|
||||||
|
}
|
||||||
|
final double years = ChronoUnit.DAYS.between(issue, maturity) / 365.25;
|
||||||
|
final double ytm = (coupon.doubleValue() + (100 - price.doubleValue()) / years) / price.doubleValue() * 100;
|
||||||
|
return BigDecimal.valueOf(ytm).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Financial-year deal number: FY start year + FY end (2 digits) + 6-digit running number, e.g. 202627000001. */
|
||||||
|
private String nextDealNo(final LocalDate dealDate) {
|
||||||
|
final String prefix = dealNoPrefix(dealDate);
|
||||||
|
final Integer max = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 7) AS INTEGER)) FROM m_investment_purchase WHERE deal_no LIKE ?", Integer.class,
|
||||||
|
prefix + "%");
|
||||||
|
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Financial year (April–March) of the deal date: 2026-04-01 … 2027-03-31 → "202627". */
|
||||||
|
static String dealNoPrefix(final LocalDate dealDate) {
|
||||||
|
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
|
||||||
|
return fyStart + String.format("%02d", (fyStart + 1) % 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextCertNo() {
|
||||||
|
final Integer max = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(CAST(cert_no AS INTEGER)) FROM m_investment_purchase WHERE cert_no ~ '^[0-9]+$'", Integer.class);
|
||||||
|
return String.valueOf((max == null ? 0 : max) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String otherDetails(final JsonCommand command) {
|
||||||
|
if (!command.parameterExists(OTHER_DETAILS)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final JsonElement other = command.parsedJson().getAsJsonObject().get(OTHER_DETAILS);
|
||||||
|
return other == null || other.isJsonNull() ? null : other.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The paying bank account must be an enabled ASSET detail GL. */
|
||||||
|
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||||
|
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||||
|
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||||
|
throw rule("cr.glaccount.invalid", "CR. To A/c " + gl.getGlCode() + " must be an enabled Asset detail account");
|
||||||
|
}
|
||||||
|
return gl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal zeroIfNull(final BigDecimal value) {
|
||||||
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
-31
@@ -222,9 +222,10 @@ public class SavingsInterestPostingReportService {
|
|||||||
return jdbcTemplate.queryForList("""
|
return jdbcTemplate.queryForList("""
|
||||||
SELECT r.id FROM m_savings_interest_posting_report r
|
SELECT r.id FROM m_savings_interest_posting_report r
|
||||||
WHERE r.status = 'APPROVED' AND r.posting_date <= ? AND r.office_id <> ?
|
WHERE r.status = 'APPROVED' AND r.posting_date <= ? AND r.office_id <> ?
|
||||||
AND NOT EXISTS (SELECT 1 FROM m_branch_day_register b WHERE b.office_id = r.office_id AND b.business_date = ?)
|
AND NOT EXISTS (SELECT 1 FROM m_branch_day_register b WHERE b.office_id = r.office_id
|
||||||
|
AND (b.business_date = ? OR (b.closed_at IS NULL AND b.business_date <= ?)))
|
||||||
ORDER BY r.posting_date, r.office_id, r.savings_product_id
|
ORDER BY r.posting_date, r.office_id, r.savings_product_id
|
||||||
""", Long.class, businessDate, headOfficeId, businessDate);
|
""", Long.class, businessDate, headOfficeId, businessDate, businessDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Every approved report still waiting, for the nightly safety net. */
|
/** Every approved report still waiting, for the nightly safety net. */
|
||||||
@@ -245,15 +246,15 @@ public class SavingsInterestPostingReportService {
|
|||||||
// ── reading ──────────────────────────────────────────────────────────────────────────────────
|
// ── reading ──────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reports for the screen. A branch user sees only their branch; a head office user sees every
|
* Reports for the screen. {@code scope} "own" (the default) is the user's own branch; "others", for a
|
||||||
* branch, so it can act for one that did not open. canAct says whether this user may regenerate
|
* head office user, is every other branch (or one, with officeId). canAct says whether this user
|
||||||
* or approve the report.
|
* may regenerate or approve the report, and blockedReason why not.
|
||||||
*/
|
*/
|
||||||
public List<Map<String, Object>> list(final LocalDate postingDate, final String status, final Long productId, final Long officeId,
|
public List<Map<String, Object>> list(final LocalDate postingDate, final String status, final Long productId, final Long officeId,
|
||||||
final AppUser user) {
|
final String scope, final AppUser user) {
|
||||||
final Long userOffice = officeOf(user);
|
final Long userOffice = officeOf(user);
|
||||||
final boolean headOffice = isHeadOffice(userOffice);
|
final boolean others = "others".equalsIgnoreCase(scope) && isHeadOffice(userOffice);
|
||||||
final Long branch = headOffice ? officeId : userOffice;
|
final Long branch = others ? officeId : userOffice;
|
||||||
|
|
||||||
final StringBuilder sql = new StringBuilder("""
|
final StringBuilder sql = new StringBuilder("""
|
||||||
SELECT r.id, r.savings_product_id AS "productId", p.name AS "productName", r.office_id AS "officeId",
|
SELECT r.id, r.savings_product_id AS "productId", p.name AS "productName", r.office_id AS "officeId",
|
||||||
@@ -287,6 +288,10 @@ public class SavingsInterestPostingReportService {
|
|||||||
sql.append(" AND r.office_id = ?");
|
sql.append(" AND r.office_id = ?");
|
||||||
params.add(branch);
|
params.add(branch);
|
||||||
}
|
}
|
||||||
|
if (others) {
|
||||||
|
sql.append(" AND r.office_id <> ?");
|
||||||
|
params.add(userOffice);
|
||||||
|
}
|
||||||
sql.append(" ORDER BY r.posting_date DESC, o.name, p.name, r.id DESC");
|
sql.append(" ORDER BY r.posting_date DESC, o.name, p.name, r.id DESC");
|
||||||
|
|
||||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
@@ -294,7 +299,9 @@ public class SavingsInterestPostingReportService {
|
|||||||
for (final Map<String, Object> row : jdbcTemplate.queryForList(sql.toString(), params.toArray())) {
|
for (final Map<String, Object> row : jdbcTemplate.queryForList(sql.toString(), params.toArray())) {
|
||||||
final Map<String, Object> out = new LinkedHashMap<>(row);
|
final Map<String, Object> out = new LinkedHashMap<>(row);
|
||||||
final Long reportOffice = row.get("officeId") == null ? null : ((Number) row.get("officeId")).longValue();
|
final Long reportOffice = row.get("officeId") == null ? null : ((Number) row.get("officeId")).longValue();
|
||||||
out.put("canAct", reportOffice != null && mayAct(userOffice, reportOffice, businessDate));
|
final String blocked = reportOffice == null ? "No branch on this report." : actBlockedReason(userOffice, reportOffice, businessDate);
|
||||||
|
out.put("canAct", blocked == null);
|
||||||
|
out.put("blockedReason", blocked);
|
||||||
rows.add(out);
|
rows.add(out);
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
@@ -339,6 +346,45 @@ public class SavingsInterestPostingReportService {
|
|||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What each savings account has earned so far in its current cycle — what it would be paid if
|
||||||
|
* interest were posted today. Read-only: nothing is saved, approved or posted. The ledger is first
|
||||||
|
* brought up to the business date (which moves no money) so the figure includes today.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A branch user sees their own branch; a head office user every branch, or one with officeId.
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> preview(final Long officeId, final AppUser user) {
|
||||||
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
|
transactionTemplate.executeWithoutResult(status -> accrualService.accrueForDate(businessDate));
|
||||||
|
|
||||||
|
final Long userOffice = officeOf(user);
|
||||||
|
final Long branch = isHeadOffice(userOffice) ? officeId : userOffice;
|
||||||
|
final StringBuilder sql = new StringBuilder("""
|
||||||
|
SELECT COALESCE(c.office_id, g.office_id) AS "officeId", o.name AS "officeName",
|
||||||
|
p.id AS "productId", p.name AS "productName", sa.id AS "savingsAccountId", sa.account_no AS "accountNo",
|
||||||
|
COALESCE(c.display_name, g.display_name) AS "clientName", d.closing_balance AS "balance",
|
||||||
|
d.interest_rate AS "rate", d.cycle_start_date AS "cycleStart", d.posting_date AS "postingDate",
|
||||||
|
d.balance_date AS "asOf", ROUND(d.cycle_cumulative_interest, 2) AS "interest"
|
||||||
|
FROM m_savings_daily_interest d
|
||||||
|
JOIN m_savings_account sa ON sa.id = d.savings_account_id
|
||||||
|
JOIN m_savings_product p ON p.id = d.savings_product_id
|
||||||
|
LEFT JOIN m_client c ON c.id = sa.client_id
|
||||||
|
LEFT JOIN m_group g ON g.id = sa.group_id
|
||||||
|
LEFT JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
|
||||||
|
WHERE p.deposit_type_enum = 100
|
||||||
|
AND sa.status_enum = ?
|
||||||
|
AND d.savings_transaction_id IS NULL
|
||||||
|
""");
|
||||||
|
final List<Object> params = new ArrayList<>(List.of(ACTIVE));
|
||||||
|
if (branch != null) {
|
||||||
|
sql.append(" AND COALESCE(c.office_id, g.office_id) = ?");
|
||||||
|
params.add(branch);
|
||||||
|
}
|
||||||
|
sql.append(" ORDER BY o.name, p.name, sa.account_no");
|
||||||
|
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Branch and product interest for the head office: per report, how many customers and how much
|
* Branch and product interest for the head office: per report, how many customers and how much
|
||||||
* interest, and where it stands. Superseded reports are left out — each line is the report that
|
* interest, and where it stands. Superseded reports are left out — each line is the report that
|
||||||
@@ -375,9 +421,13 @@ public class SavingsInterestPostingReportService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Branch and product pairs with interest due by the date that have no report yet — the noon job ran
|
* Branch and product pairs with interest due by the date that have no report yet — the noon job ran
|
||||||
* before they had anything due, or failed. Limited to what the user may act on.
|
* before they had anything due, or failed.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@code scope} "own" (the default) lists the user's own branch. "others", for a head office user,
|
||||||
|
* lists the other branches — each with canAct, and where the head office may not act for it, why.
|
||||||
*/
|
*/
|
||||||
public List<Map<String, Object>> dueWithoutReport(final LocalDate postingDate, final AppUser user) {
|
public List<Map<String, Object>> dueWithoutReport(final LocalDate postingDate, final AppUser user, final String scope) {
|
||||||
final List<Map<String, Object>> out = new ArrayList<>();
|
final List<Map<String, Object>> out = new ArrayList<>();
|
||||||
// A future date has nothing due yet, and offering to generate for it would label this cycle's
|
// A future date has nothing due yet, and offering to generate for it would label this cycle's
|
||||||
// part-earned interest with a date it has not reached.
|
// part-earned interest with a date it has not reached.
|
||||||
@@ -386,15 +436,21 @@ public class SavingsInterestPostingReportService {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
final Long userOffice = officeOf(user);
|
final Long userOffice = officeOf(user);
|
||||||
for (final DueKey due : dueKeys(postingDate, isHeadOffice(userOffice) ? null : userOffice)) {
|
final boolean others = "others".equalsIgnoreCase(scope) && isHeadOffice(userOffice);
|
||||||
if (activeReportId(due.productId(), due.officeId(), postingDate) == null
|
for (final DueKey due : dueKeys(postingDate, others ? null : userOffice)) {
|
||||||
&& mayAct(userOffice, due.officeId(), businessDate)) {
|
if (others == due.officeId().equals(userOffice)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (activeReportId(due.productId(), due.officeId(), postingDate) == null) {
|
||||||
final Map<String, Object> row = new LinkedHashMap<>();
|
final Map<String, Object> row = new LinkedHashMap<>();
|
||||||
row.put("productId", due.productId());
|
row.put("productId", due.productId());
|
||||||
row.put("productName", productName(due.productId()));
|
row.put("productName", productName(due.productId()));
|
||||||
row.put("officeId", due.officeId());
|
row.put("officeId", due.officeId());
|
||||||
row.put("officeName", jdbcTemplate.queryForObject("SELECT name FROM m_office WHERE id = ?", String.class, due.officeId()));
|
row.put("officeName", officeName(due.officeId()));
|
||||||
row.put("postingDate", postingDate.toString());
|
row.put("postingDate", postingDate.toString());
|
||||||
|
final String blocked = actBlockedReason(userOffice, due.officeId(), businessDate);
|
||||||
|
row.put("canAct", blocked == null);
|
||||||
|
row.put("blockedReason", blocked);
|
||||||
out.add(row);
|
out.add(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,36 +464,79 @@ public class SavingsInterestPostingReportService {
|
|||||||
return !parent.isEmpty() && parent.get(0) == null;
|
return !parent.isEmpty() && parent.get(0) == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the branch opened its day on the date — the head office may act only for one that did not. */
|
/**
|
||||||
|
* Whether the branch is working on the date: it opened that day, or a day it opened earlier is still
|
||||||
|
* not closed. A branch with yesterday's day still open is open — it closes that day and goes on.
|
||||||
|
*/
|
||||||
public boolean branchOpenedOn(final Long officeId, final LocalDate businessDate) {
|
public boolean branchOpenedOn(final Long officeId, final LocalDate businessDate) {
|
||||||
final Long rows = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_branch_day_register WHERE office_id = ? AND business_date = ?",
|
final Long rows = jdbcTemplate.queryForObject("""
|
||||||
Long.class, officeId, businessDate);
|
SELECT COUNT(*) FROM m_branch_day_register
|
||||||
|
WHERE office_id = ? AND (business_date = ? OR (closed_at IS NULL AND business_date <= ?))
|
||||||
|
""", Long.class, officeId, businessDate, businessDate);
|
||||||
return rows != null && rows > 0;
|
return rows != null && rows > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean mayAct(final Long userOffice, final Long reportOffice, final LocalDate businessDate) {
|
/** The branches, other than those given, that still have a day open on or before the date. */
|
||||||
|
public List<String> branchesStillOpen(final LocalDate businessDate, final Long... excludeOfficeIds) {
|
||||||
|
final StringBuilder sql = new StringBuilder("""
|
||||||
|
SELECT DISTINCT o.name FROM m_branch_day_register r JOIN m_office o ON o.id = r.office_id
|
||||||
|
WHERE r.closed_at IS NULL AND r.business_date <= ?
|
||||||
|
""");
|
||||||
|
final List<Object> params = new ArrayList<>(List.of(businessDate));
|
||||||
|
for (final Long exclude : excludeOfficeIds) {
|
||||||
|
sql.append(" AND r.office_id <> ?");
|
||||||
|
params.add(exclude);
|
||||||
|
}
|
||||||
|
sql.append(" ORDER BY o.name");
|
||||||
|
return jdbcTemplate.queryForList(sql.toString(), String.class, params.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null where the user may generate or approve the branch's report, otherwise why not.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A branch always acts for itself. The head office acts for another branch only when that branch
|
||||||
|
* has not opened at all, and only once every branch that did open has closed — the head office's
|
||||||
|
* own close being the last of the day, so nothing is taken out of a branch that may still open.
|
||||||
|
*/
|
||||||
|
private String actBlockedReason(final Long userOffice, final Long reportOffice, final LocalDate businessDate) {
|
||||||
if (userOffice == null) {
|
if (userOffice == null) {
|
||||||
return false;
|
return "No branch is set for this user.";
|
||||||
}
|
}
|
||||||
if (userOffice.equals(reportOffice)) {
|
if (userOffice.equals(reportOffice)) {
|
||||||
return true;
|
return null;
|
||||||
}
|
}
|
||||||
return isHeadOffice(userOffice) && !branchOpenedOn(reportOffice, businessDate);
|
final String branch = officeName(reportOffice);
|
||||||
|
if (!isHeadOffice(userOffice)) {
|
||||||
|
return "Only " + branch + " can generate or approve its own interest posting report.";
|
||||||
|
}
|
||||||
|
if (branchOpenedOn(reportOffice, businessDate)) {
|
||||||
|
return branch + " is open — it generates and approves its own report.";
|
||||||
|
}
|
||||||
|
final List<String> stillOpen = branchesStillOpen(businessDate, userOffice, reportOffice);
|
||||||
|
if (!stillOpen.isEmpty()) {
|
||||||
|
return "The head office can act for " + branch + " only after every open branch has closed. Still open: "
|
||||||
|
+ String.join(", ", stillOpen) + ".";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean mayAct(final Long userOffice, final Long reportOffice, final LocalDate businessDate) {
|
||||||
|
return actBlockedReason(userOffice, reportOffice, businessDate) == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return true where the head office is acting on a branch's behalf */
|
/** @return true where the head office is acting on a branch's behalf */
|
||||||
private boolean authorize(final AppUser user, final Long reportOffice, final LocalDate businessDate) {
|
private boolean authorize(final AppUser user, final Long reportOffice, final LocalDate businessDate) {
|
||||||
final Long userOffice = officeOf(user);
|
final Long userOffice = officeOf(user);
|
||||||
if (userOffice != null && userOffice.equals(reportOffice)) {
|
final String blocked = actBlockedReason(userOffice, reportOffice, businessDate);
|
||||||
return false;
|
if (blocked != null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.savingsinterestreport.other.branch", blocked);
|
||||||
}
|
}
|
||||||
if (mayAct(userOffice, reportOffice, businessDate)) {
|
return !reportOffice.equals(userOffice);
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
final String branch = jdbcTemplate.queryForObject("SELECT name FROM m_office WHERE id = ?", String.class, reportOffice);
|
private String officeName(final Long officeId) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.savingsinterestreport.other.branch",
|
return jdbcTemplate.queryForObject("SELECT name FROM m_office WHERE id = ?", String.class, officeId);
|
||||||
"Only " + branch + " can generate or approve its own interest posting report. The head office may do so only on a day "
|
|
||||||
+ branch + " has not opened.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Long officeOf(final AppUser user) {
|
private static Long officeOf(final AppUser user) {
|
||||||
|
|||||||
+27
-4
@@ -65,20 +65,34 @@ public class SavingsInterestPostingReportApiResource {
|
|||||||
/** A branch user sees their own branch; a head office user every branch, or one with officeId. */
|
/** A branch user sees their own branch; a head office user every branch, or one with officeId. */
|
||||||
@GET
|
@GET
|
||||||
public String list(@QueryParam("postingDate") final String postingDate, @QueryParam("status") final String status,
|
public String list(@QueryParam("postingDate") final String postingDate, @QueryParam("status") final String status,
|
||||||
@QueryParam("productId") final Long productId, @QueryParam("officeId") final Long officeId) {
|
@QueryParam("productId") final Long productId, @QueryParam("officeId") final Long officeId,
|
||||||
|
@QueryParam("scope") final String scope) {
|
||||||
final AppUser user = context.authenticatedUser();
|
final AppUser user = context.authenticatedUser();
|
||||||
user.validateHasReadPermission(RESOURCE);
|
user.validateHasReadPermission(RESOURCE);
|
||||||
return gson.toJson(reportService.list(parseDate(postingDate), status, productId, officeId, user).stream().map(this::plain).toList());
|
return gson.toJson(reportService.list(parseDate(postingDate), status, productId, officeId, scope, user).stream().map(this::plain).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Who is looking: their branch, and whether it is the head office (which gets the Other branches tab). */
|
||||||
|
@GET
|
||||||
|
@Path("/context")
|
||||||
|
public String userContext() {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
final Long officeId = user.getOffice().getId();
|
||||||
|
final Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("officeId", officeId);
|
||||||
|
result.put("officeName", user.getOffice().getName());
|
||||||
|
result.put("headOffice", reportService.isHeadOffice(officeId));
|
||||||
|
return gson.toJson(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Branch and product pairs with interest due by the date and no report yet, that this user may generate. */
|
/** Branch and product pairs with interest due by the date and no report yet, that this user may generate. */
|
||||||
@GET
|
@GET
|
||||||
@Path("/due-without-report")
|
@Path("/due-without-report")
|
||||||
public String dueWithoutReport(@QueryParam("postingDate") final String postingDate) {
|
public String dueWithoutReport(@QueryParam("postingDate") final String postingDate, @QueryParam("scope") final String scope) {
|
||||||
final AppUser user = context.authenticatedUser();
|
final AppUser user = context.authenticatedUser();
|
||||||
user.validateHasReadPermission(RESOURCE);
|
user.validateHasReadPermission(RESOURCE);
|
||||||
final LocalDate date = postingDate == null || postingDate.isBlank() ? DateUtils.getBusinessLocalDate() : parseDate(postingDate);
|
final LocalDate date = postingDate == null || postingDate.isBlank() ? DateUtils.getBusinessLocalDate() : parseDate(postingDate);
|
||||||
return gson.toJson(reportService.dueWithoutReport(date, user));
|
return gson.toJson(reportService.dueWithoutReport(date, user, scope));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +113,15 @@ public class SavingsInterestPostingReportApiResource {
|
|||||||
return gson.toJson(result);
|
return gson.toJson(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interest each account has earned so far this cycle, as if posted today. View only — nothing is saved. */
|
||||||
|
@GET
|
||||||
|
@Path("/preview")
|
||||||
|
public String preview(@QueryParam("officeId") final Long officeId) {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
user.validateHasReadPermission(RESOURCE);
|
||||||
|
return gson.toJson(reportService.preview(officeId, user).stream().map(this::plain).toList());
|
||||||
|
}
|
||||||
|
|
||||||
/** The head office's view: per branch and product, customers paid and interest, over a date range. */
|
/** The head office's view: per branch and product, customers paid and interest, over a date range. */
|
||||||
@GET
|
@GET
|
||||||
@Path("/summary")
|
@Path("/summary")
|
||||||
|
|||||||
+2
-10
@@ -250,11 +250,7 @@ public class CurrentAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see ShareAccountEditWritePlatformServiceJpaRepositoryImpl and
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement elsewhere.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,11 +306,7 @@ public class CurrentAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see ShareAccountEditWritePlatformServiceJpaRepositoryImpl and
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement elsewhere.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -260,11 +260,7 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,11 +350,7 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -264,11 +264,7 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,11 +357,7 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -203,11 +203,7 @@ public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +265,7 @@ public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -420,11 +420,7 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,11 +524,7 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-12
@@ -172,12 +172,7 @@ public class ShareAccountEditWritePlatformServiceJpaRepositoryImpl implements Sh
|
|||||||
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - that permission still lets them approve someone ELSE's request,
|
|
||||||
// it just no longer lets them bypass the hold on their own (matches
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction(), the
|
|
||||||
// equivalent rule for the classic command pipeline).
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +214,7 @@ public class ShareAccountEditWritePlatformServiceJpaRepositoryImpl implements Sh
|
|||||||
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - that permission still lets them approve someone ELSE's request,
|
|
||||||
// it just no longer lets them bypass the hold on their own (matches
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction(), the
|
|
||||||
// equivalent rule for the classic command pipeline).
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -882,11 +882,7 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,11 +968,7 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||||
|
|
||||||
// Maker-identity rule: the same user can never approve their own submission, even when they
|
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||||
// hold CHECKER_SUPER_USER - see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction() for the
|
|
||||||
// equivalent enforcement in the classic command pipeline.
|
|
||||||
if (pendingEdit.getMaker().getId().equals(checker.getId())) {
|
|
||||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,5 +163,8 @@
|
|||||||
<include file="parts/0588_add_premature_closure_reason.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0588_add_premature_closure_reason.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0589_savings_interest_posting_report.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0589_savings_interest_posting_report.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0590_savings_interest_posting_report_by_branch.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0590_savings_interest_posting_report_by_branch.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0588_persist_manually_enabled_maker_checker_permissions.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0591_cashier_allocation_time_and_deposit_counter_office.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0592_enable_teller_cashier_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0593_investment_masters.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
<?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">
|
|
||||||
|
|
||||||
<!-- Converts manually-applied Configure Maker Checker Tasks changes into a repeatable migration.
|
|
||||||
During Admin -> Organization and Admin -> System testing, can_maker_checker was switched on
|
|
||||||
(via System -> Configure Maker Checker Tasks) for the 28 permissions below, directly in the
|
|
||||||
dev database - none of that was ever captured by a migration, so a fresh/other environment
|
|
||||||
restored from this changelog history would come up with every one of them still at its
|
|
||||||
originally-seeded false value. Every code here was independently confirmed, via read-only
|
|
||||||
inspection of both its origin changeset and the live database, to be seeded can_maker_checker
|
|
||||||
= false and currently true in the running dev DB with no existing changeset already covering
|
|
||||||
the flip (the one precedent for this pattern, DELETE_OFFICE, was flipped by changeset
|
|
||||||
0538_enable_maker_checker_for_delete_office and is deliberately left untouched here).
|
|
||||||
|
|
||||||
This changeset only ever moves can_maker_checker from false to true for these exact codes -
|
|
||||||
entity_name, action_name, grouping and every other column are left exactly as their origin
|
|
||||||
changeset defined them (including the pre-existing DELETE_ACCOUNTNUMBERFORMAT entity_name
|
|
||||||
drift, which is out of scope for this change and is not corrected here). No _CHECKER row is
|
|
||||||
touched: those already exist for every code below (added earlier by 0583/0586, or by base
|
|
||||||
Fineract for the stock codes), and can_maker_checker is not a column on a _CHECKER row.
|
|
||||||
|
|
||||||
Grouped by the screen each permission belongs to:
|
|
||||||
- Currency Configuration: UPDATE_CURRENCY
|
|
||||||
- Manage Funds: CREATE_FUND
|
|
||||||
- Working Days: UPDATE_WORKINGDAYS
|
|
||||||
- Payment Type: CREATE_PAYMENTTYPE, UPDATE_PAYMENTTYPE, DELETE_PAYMENTTYPE
|
|
||||||
- Sponsor Banks: CREATE_SPONSORBANK, UPDATE_SPONSORBANK
|
|
||||||
- Branch Profile: UPDATE_BRANCHPROFILE
|
|
||||||
- Manage Data Tables: CREATE_DATATABLE, UPDATE_DATATABLE, DELETE_DATATABLE
|
|
||||||
- Manage Codes: CREATE_CODE, UPDATE_CODE, DELETE_CODE,
|
|
||||||
CREATE_CODEVALUE, UPDATE_CODEVALUE, DELETE_CODEVALUE
|
|
||||||
- Manage Reports: CREATE_REPORT, UPDATE_REPORT, DELETE_REPORT
|
|
||||||
- Manage Hooks: CREATE_HOOK, UPDATE_HOOK, DELETE_HOOK
|
|
||||||
- Account Number Preferences: CREATE_ACCOUNTNUMBERFORMAT, DELETE_ACCOUNTNUMBERFORMAT
|
|
||||||
- Manage User Agreement: CREATE_USERAGREEMENT
|
|
||||||
- Manage Jobs (COB Workflow Step Reorder): UPDATE_BATCH_BUSINESS_STEP
|
|
||||||
|
|
||||||
Deliberately NOT included (confirmed still false, or out of scope, and left untouched):
|
|
||||||
- UPDATE_PERMISSION, UPDATE_CONFIGURATION (the Configure Maker Checker Tasks self-lockout
|
|
||||||
exclusion added by PermissionReadPlatformServiceImpl stays in effect; these remain false)
|
|
||||||
- UPDATE_SPONSORDDGLMAPPING, all Entity-to-Entity Mapping, External Services and Manage
|
|
||||||
Survey permissions (confirmed still false - never actually toggled on)
|
|
||||||
- CREATE_ROLE/UPDATE_ROLE/DELETE_ROLE/etc., CREATE_GLACCOUNT, CREATE_CHARGE,
|
|
||||||
CREATE_LOANPRODUCT, CREATE_SAVINGSPRODUCT and other permissions observed to already be
|
|
||||||
true in the live DB but belonging to Accounting/Products/Roles modules, not Admin ->
|
|
||||||
Organization or Admin -> System - out of scope for this change and left as pre-existing
|
|
||||||
drift for a separate decision. -->
|
|
||||||
<changeSet id="0588_persist_manually_enabled_maker_checker_permissions" author="cbs-anshul">
|
|
||||||
<update tableName="m_permission">
|
|
||||||
<column name="can_maker_checker" valueBoolean="true"/>
|
|
||||||
<where>code IN (
|
|
||||||
'UPDATE_CURRENCY',
|
|
||||||
'CREATE_FUND',
|
|
||||||
'UPDATE_WORKINGDAYS',
|
|
||||||
'CREATE_PAYMENTTYPE', 'UPDATE_PAYMENTTYPE', 'DELETE_PAYMENTTYPE',
|
|
||||||
'CREATE_SPONSORBANK', 'UPDATE_SPONSORBANK',
|
|
||||||
'UPDATE_BRANCHPROFILE',
|
|
||||||
'CREATE_DATATABLE', 'UPDATE_DATATABLE', 'DELETE_DATATABLE',
|
|
||||||
'CREATE_CODE', 'UPDATE_CODE', 'DELETE_CODE',
|
|
||||||
'CREATE_CODEVALUE', 'UPDATE_CODEVALUE', 'DELETE_CODEVALUE',
|
|
||||||
'CREATE_REPORT', 'UPDATE_REPORT', 'DELETE_REPORT',
|
|
||||||
'CREATE_HOOK', 'UPDATE_HOOK', 'DELETE_HOOK',
|
|
||||||
'CREATE_ACCOUNTNUMBERFORMAT', 'DELETE_ACCOUNTNUMBERFORMAT',
|
|
||||||
'CREATE_USERAGREEMENT',
|
|
||||||
'UPDATE_BATCH_BUSINESS_STEP'
|
|
||||||
)</where>
|
|
||||||
</update>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
</databaseChangeLog>
|
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Cash taken over a counter belongs to the drawer and the branch that took it, not to whoever the
|
||||||
|
same login is allocated to today.
|
||||||
|
|
||||||
|
m_cashiers.created_at: a cashier position is built from the counter deposits and withdrawals of
|
||||||
|
the cashier's login, so a person moved from one branch's teller to another's carried every
|
||||||
|
rupee they had ever taken into the new drawer. The position now counts only what was taken
|
||||||
|
once the allocation existed. Existing rows take the time their ALLOCATECASHIER command ran;
|
||||||
|
a row with no such command stays NULL and keeps its whole history, as before.
|
||||||
|
|
||||||
|
m_cashier_deposit_request.counter_office_id: savings deposits are submitted with no office_id,
|
||||||
|
and the day close counted every such deposit at every branch. The counter's office is now
|
||||||
|
stored at submit. Existing rows take the teller office of the submitter's allocation if it
|
||||||
|
already existed when the deposit was submitted, otherwise the submitter's own office.
|
||||||
|
-->
|
||||||
|
<changeSet id="0591_cashier_created_at" author="fineract">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_cashiers" columnName="created_at"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_cashiers">
|
||||||
|
<column name="created_at" type="TIMESTAMP"/>
|
||||||
|
</addColumn>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_cashiers mc
|
||||||
|
SET created_at = (SELECT CAST(MIN(cs.made_on_date_utc) AS TIMESTAMP)
|
||||||
|
FROM m_portfolio_command_source cs
|
||||||
|
WHERE cs.entity_name = 'TELLER' AND cs.action_name = 'ALLOCATECASHIER'
|
||||||
|
AND cs.subresource_id = mc.id)
|
||||||
|
</sql>
|
||||||
|
<addDefaultValue tableName="m_cashiers" columnName="created_at" columnDataType="TIMESTAMP"
|
||||||
|
defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0591_deposit_request_counter_office" author="fineract">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_cashier_deposit_request" columnName="counter_office_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_cashier_deposit_request">
|
||||||
|
<column name="counter_office_id" type="BIGINT"/>
|
||||||
|
</addColumn>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_cashier_deposit_request r
|
||||||
|
SET counter_office_id = COALESCE(
|
||||||
|
(SELECT t.office_id FROM m_appuser au
|
||||||
|
JOIN m_cashiers c ON c.staff_id = au.staff_id
|
||||||
|
JOIN m_tellers t ON t.id = c.teller_id
|
||||||
|
WHERE au.id = r.cashier_id
|
||||||
|
AND (c.created_at IS NULL OR r.submitted_at IS NULL OR r.submitted_at >= c.created_at)
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT au.office_id FROM m_appuser au WHERE au.id = r.cashier_id),
|
||||||
|
r.office_id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</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">
|
||||||
|
|
||||||
|
<!-- Same pattern as 0588_persist_manually_enabled_maker_checker_permissions: converts a
|
||||||
|
manually-applied Configure Maker Checker Tasks change into a repeatable migration, this time
|
||||||
|
for the Teller/Cashier module. Found by diffing can_maker_checker across every permission code
|
||||||
|
common to fineract_lincpay and a reference environment (fineract_default) - these 4 were true
|
||||||
|
in the reference but still seeded false here, confirming they were toggled on manually there
|
||||||
|
and never captured for any other environment (including this one and UAT).
|
||||||
|
|
||||||
|
Grouped by the screen each permission belongs to (Organization -> Manage Tellers -> allocate/
|
||||||
|
settle cash for a cashier):
|
||||||
|
ALLOCATECASHIER_TELLER, ALLOCATECASHTOCASHIER_TELLER,
|
||||||
|
SETTLECASHFROMCASHIER_TELLER, UPDATECASHIERALLOCATION_TELLER
|
||||||
|
|
||||||
|
No _CHECKER row is touched: those already exist for every code below (seeded by base
|
||||||
|
Fineract), and can_maker_checker is not a column on a _CHECKER row. -->
|
||||||
|
<changeSet id="0592_enable_teller_cashier_maker_checker" author="cbs-nitesh">
|
||||||
|
<update tableName="m_permission">
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
<where>code IN (
|
||||||
|
'ALLOCATECASHIER_TELLER',
|
||||||
|
'ALLOCATECASHTOCASHIER_TELLER',
|
||||||
|
'SETTLECASHFROMCASHIER_TELLER',
|
||||||
|
'UPDATECASHIERALLOCATION_TELLER'
|
||||||
|
)</where>
|
||||||
|
</update>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<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.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Treasury Investment masters, following the bank's existing CBS screens:
|
||||||
|
- m_investment_party : Bank / Party Master (whom an investment is bought from / placed with)
|
||||||
|
- m_investment_script : Script Master (what is bought - Govt Security, Bond, Mutual Fund, ...)
|
||||||
|
Party and script codes are generated by the application (PTY-001, G00001). Both rows are
|
||||||
|
tied to the investment (asset) GL they are booked to.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0593-1-create-investment-party">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_investment_party"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_investment_party">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||||
|
<column name="party_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="party_name" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
||||||
|
<column name="address1" type="VARCHAR(150)"/>
|
||||||
|
<column name="address2" type="VARCHAR(150)"/>
|
||||||
|
<column name="address3" type="VARCHAR(150)"/>
|
||||||
|
<column name="city" type="VARCHAR(100)"/>
|
||||||
|
<column name="pincode" type="VARCHAR(10)"/>
|
||||||
|
<column name="phone" type="VARCHAR(20)"/>
|
||||||
|
<column name="fax" type="VARCHAR(20)"/>
|
||||||
|
<column name="bank_type" type="VARCHAR(60)"/>
|
||||||
|
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="rbi_code" type="VARCHAR(30)"/>
|
||||||
|
<column name="exposure_limit" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="lei_no" type="VARCHAR(30)"/>
|
||||||
|
<column name="cp_bank_name" type="VARCHAR(150)"/>
|
||||||
|
<column name="cp_branch_name" type="VARCHAR(150)"/>
|
||||||
|
<column name="cp_account_no" type="VARCHAR(40)"/>
|
||||||
|
<column name="cp_ifsc" type="VARCHAR(11)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
</createTable>
|
||||||
|
<addUniqueConstraint tableName="m_investment_party" columnNames="party_code" constraintName="uq_investment_party_code"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_party" baseColumnNames="gl_account_id" constraintName="fk_investment_party_gl_account"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<createIndex tableName="m_investment_party" indexName="idx_investment_party_bond_type">
|
||||||
|
<column name="bond_type"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0593-2-create-investment-script">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_investment_script"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_investment_script">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||||
|
<column name="script_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="slr_type" type="VARCHAR(20)"/>
|
||||||
|
<column name="script_desc" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
||||||
|
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="isin" type="VARCHAR(20)"/>
|
||||||
|
<column name="face_value_per_unit" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="int_rate" type="DECIMAL(9,4)"/>
|
||||||
|
<column name="issue_date" type="DATE"/>
|
||||||
|
<column name="maturity_date" type="DATE"/>
|
||||||
|
<column name="company_name" type="VARCHAR(150)"/>
|
||||||
|
<column name="address1" type="VARCHAR(150)"/>
|
||||||
|
<column name="address2" type="VARCHAR(150)"/>
|
||||||
|
<column name="address3" type="VARCHAR(150)"/>
|
||||||
|
<column name="city" type="VARCHAR(100)"/>
|
||||||
|
<column name="pincode" type="VARCHAR(10)"/>
|
||||||
|
<column name="phone" type="VARCHAR(20)"/>
|
||||||
|
<column name="fax" type="VARCHAR(20)"/>
|
||||||
|
<column name="corp_code" type="VARCHAR(30)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
</createTable>
|
||||||
|
<addUniqueConstraint tableName="m_investment_script" columnNames="script_code" constraintName="uq_investment_script_code"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_script" baseColumnNames="gl_account_id" constraintName="fk_investment_script_gl_account"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<createIndex tableName="m_investment_script" indexName="idx_investment_script_bond_type">
|
||||||
|
<column name="bond_type"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0593-3-investment-master-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT', 'CREATE_INVESTMENTPARTY_CHECKER', 'UPDATE_INVESTMENTPARTY_CHECKER', 'DELETE_INVESTMENTPARTY_CHECKER', 'CREATE_INVESTMENTSCRIPT_CHECKER', 'UPDATE_INVESTMENTSCRIPT_CHECKER')</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_INVESTMENTPARTY"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTPARTY"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="CREATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="UPDATE_INVESTMENTPARTY"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="UPDATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="DELETE_INVESTMENTPARTY"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="DELETE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_INVESTMENTSCRIPT"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTSCRIPT"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||||
|
<column name="action_name" value="CREATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="UPDATE_INVESTMENTSCRIPT"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||||
|
<column name="action_name" value="UPDATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTPARTY_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="CREATE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="UPDATE_INVESTMENTPARTY_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="UPDATE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="DELETE_INVESTMENTPARTY_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||||
|
<column name="action_name" value="DELETE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTSCRIPT_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||||
|
<column name="action_name" value="CREATE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="UPDATE_INVESTMENTSCRIPT_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||||
|
<column name="action_name" value="UPDATE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Whoever already sees the Investment menu gets to read and maintain its masters. Maker-checker
|
||||||
|
is left off (can_maker_checker=false); it can be switched on per action from
|
||||||
|
Configure Maker Checker Tasks, and the _CHECKER rows above let a checker role approve. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0593-4-grant-investment-master-permissions">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<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.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Investment Purchase Entry: one row per deal, which is the holding that interest, sale and
|
||||||
|
redemption later work against (balance_units tracks what is still held). Units, book value,
|
||||||
|
accrued interest and settlement amount are computed by the application. The optional legacy
|
||||||
|
deal fields (trade no, authorities, lien, delivery mode, ...) are kept in other_details as JSON.
|
||||||
|
No GL entry is posted by this table yet.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0594-1-create-investment-purchase">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_investment_purchase"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_investment_purchase">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||||
|
<column name="deal_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="cert_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="party_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
|
||||||
|
<column name="deal_time" type="VARCHAR(8)"/>
|
||||||
|
<column name="settlement_type" type="VARCHAR(5)"><constraints nullable="false"/></column>
|
||||||
|
<column name="settlement_date" type="DATE"><constraints nullable="false"/></column>
|
||||||
|
<column name="face_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="purchase_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="balance_units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="brokerage" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="stamp_duty" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="book_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="premium_discount" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="int_rate" type="DECIMAL(9,4)"/>
|
||||||
|
<column name="issue_date" type="DATE"/>
|
||||||
|
<column name="maturity_date" type="DATE"/>
|
||||||
|
<column name="last_int_date" type="DATE"/>
|
||||||
|
<column name="accrued_days" type="INT"/>
|
||||||
|
<column name="accrued_interest" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="settlement_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="ytm" type="DECIMAL(9,4)"/>
|
||||||
|
<column name="category" type="VARCHAR(40)"><constraints nullable="false"/></column>
|
||||||
|
<column name="sub_type" type="VARCHAR(40)"/>
|
||||||
|
<column name="investment_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="cr_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="other_details" type="VARCHAR(4000)"/>
|
||||||
|
<column name="status" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
</createTable>
|
||||||
|
<addUniqueConstraint tableName="m_investment_purchase" columnNames="deal_no" constraintName="uq_investment_purchase_deal_no"/>
|
||||||
|
<addUniqueConstraint tableName="m_investment_purchase" columnNames="cert_no" constraintName="uq_investment_purchase_cert_no"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="script_id" constraintName="fk_investment_purchase_script"
|
||||||
|
referencedTableName="m_investment_script" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="party_id" constraintName="fk_investment_purchase_party"
|
||||||
|
referencedTableName="m_investment_party" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="investment_gl_account_id" constraintName="fk_investment_purchase_inv_gl"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="cr_gl_account_id" constraintName="fk_investment_purchase_cr_gl"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_script">
|
||||||
|
<column name="script_id"/>
|
||||||
|
</createIndex>
|
||||||
|
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_bond_type_status">
|
||||||
|
<column name="bond_type"/>
|
||||||
|
<column name="status"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0594-2-investment-purchase-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE_CHECKER', 'DELETE_INVESTMENTPURCHASE_CHECKER')</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_INVESTMENTPURCHASE"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTPURCHASE"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||||
|
<column name="action_name" value="CREATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="DELETE_INVESTMENTPURCHASE"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||||
|
<column name="action_name" value="DELETE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_INVESTMENTPURCHASE_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||||
|
<column name="action_name" value="CREATE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="DELETE_INVESTMENTPURCHASE_CHECKER"/>
|
||||||
|
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||||
|
<column name="action_name" value="DELETE_CHECKER"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Same roles as the masters: whoever sees the Investment menu. Maker-checker stays off until
|
||||||
|
switched on from Configure Maker Checker Tasks. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0594-3-grant-investment-purchase-permissions">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+6
-12
@@ -116,11 +116,7 @@ public class CommandSourceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProcessCommand_NonGLAccount_SuperUser_OwnSubmission_DoesNotBypassMakerChecker() {
|
public void testProcessCommand_NonGLAccount_SuperUser_BypassesMakerChecker() {
|
||||||
// A maker who happens to hold CHECKER_SUPER_USER must not auto-approve their own submission -
|
|
||||||
// CHECKER_SUPER_USER only ever lets its holder approve a DIFFERENT user's request (see
|
|
||||||
// PortfolioCommandSourceWritePlatformServiceImpl.validateMakerCheckerTransaction()). This
|
|
||||||
// replaces the old bypass this test used to assert.
|
|
||||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
@@ -129,8 +125,6 @@ public class CommandSourceServiceTest {
|
|||||||
Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT");
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT");
|
||||||
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT");
|
Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT");
|
||||||
Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW");
|
Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW");
|
||||||
Mockito.when(commandSource.getId()).thenReturn(300L);
|
|
||||||
Mockito.when(commandSource.getResourceId()).thenReturn(400L);
|
|
||||||
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true);
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true);
|
||||||
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
@@ -138,12 +132,12 @@ public class CommandSourceServiceTest {
|
|||||||
.withCommandId(1L).build();
|
.withCommandId(1L).build();
|
||||||
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
commandSource, superUser, false);
|
||||||
});
|
|
||||||
|
|
||||||
Mockito.verify(commandSource).markAsAwaitingApproval();
|
Assertions.assertNotNull(result);
|
||||||
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
Mockito.verify(commandSource).markAsChecked(superUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* 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.investment.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.TYPE_COUPON;
|
||||||
|
import static org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.TYPE_PROVISION;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.ScheduleRow;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figures from Jitendra Sir's worked example: 7.50% GOI 2034, face value 1 crore, coupons 10 Feb / 10 Aug, bought on
|
||||||
|
* 08-10-2026.
|
||||||
|
*/
|
||||||
|
class InvestmentInterestCalculatorTest {
|
||||||
|
|
||||||
|
private static final BigDecimal ONE_CRORE = new BigDecimal("10000000");
|
||||||
|
private static final BigDecimal RATE = new BigDecimal("7.50");
|
||||||
|
private static final LocalDate MATURITY = LocalDate.of(2034, 8, 10);
|
||||||
|
private static final LocalDate LAST_COUPON = LocalDate.of(2026, 8, 10);
|
||||||
|
private static final LocalDate DEAL = LocalDate.of(2026, 10, 8);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void days360CountsEveryMonthAsThirtyDays() {
|
||||||
|
assertEquals(50, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2026, 9, 30)));
|
||||||
|
assertEquals(58, InvestmentInterestCalculator.days360(LAST_COUPON, DEAL));
|
||||||
|
assertEquals(90, InvestmentInterestCalculator.days360(LocalDate.of(2026, 6, 30), LocalDate.of(2026, 9, 30)));
|
||||||
|
assertEquals(50, InvestmentInterestCalculator.days360(LocalDate.of(2027, 2, 10), LocalDate.of(2027, 3, 31)));
|
||||||
|
assertEquals(180, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2027, 2, 10)));
|
||||||
|
assertEquals(360, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2027, 8, 10)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void days360IsZeroWhenDatesAreMissingOrReversed() {
|
||||||
|
assertEquals(0, InvestmentInterestCalculator.days360(null, DEAL));
|
||||||
|
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, null));
|
||||||
|
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, DEAL));
|
||||||
|
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, LAST_COUPON));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void interestForDaysRoundsToTheRupee() {
|
||||||
|
assertEquals(new BigDecimal("120833"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58));
|
||||||
|
assertEquals(new BigDecimal("104167"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50));
|
||||||
|
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void halfYearCouponIsFaceValueTimesRateOverTwo() {
|
||||||
|
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE));
|
||||||
|
assertEquals(new BigDecimal("362500"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void couponDatesRunSixMonthlyFromMaturity() {
|
||||||
|
assertEquals(LAST_COUPON, InvestmentInterestCalculator.lastCouponOnOrBefore(MATURITY, DEAL));
|
||||||
|
assertEquals(LAST_COUPON, InvestmentInterestCalculator.lastCouponOnOrBefore(MATURITY, LAST_COUPON));
|
||||||
|
|
||||||
|
final List<LocalDate> coupons = InvestmentInterestCalculator.couponDatesAfter(MATURITY, LAST_COUPON);
|
||||||
|
assertEquals(16, coupons.size());
|
||||||
|
assertEquals(LocalDate.of(2027, 2, 10), coupons.get(0));
|
||||||
|
assertEquals(LocalDate.of(2027, 8, 10), coupons.get(1));
|
||||||
|
assertEquals(MATURITY, coupons.get(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void monthEndMaturityKeepsMonthEndCoupons() {
|
||||||
|
final LocalDate maturity = LocalDate.of(2030, 8, 31);
|
||||||
|
final List<LocalDate> coupons = InvestmentInterestCalculator.couponDatesAfter(maturity, LocalDate.of(2029, 8, 31));
|
||||||
|
assertEquals(List.of(LocalDate.of(2030, 2, 28), maturity), coupons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closingDatesAreMarchAndSeptemberEnds() {
|
||||||
|
assertEquals(List.of(LocalDate.of(2027, 3, 31), LocalDate.of(2027, 9, 30)),
|
||||||
|
InvestmentInterestCalculator.closingDates(DEAL, LocalDate.of(2028, 3, 30)));
|
||||||
|
assertEquals(List.of(LocalDate.of(2026, 9, 30)),
|
||||||
|
InvestmentInterestCalculator.closingDates(LocalDate.of(2026, 9, 30), LocalDate.of(2026, 9, 30)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scheduleClearsPurchasedInterestAndProvisionsAtEachClosing() {
|
||||||
|
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL,
|
||||||
|
new BigDecimal("120833"));
|
||||||
|
|
||||||
|
// 16 coupons + 15 closings (Mar 2027 … Mar 2034); 30-09-2026 is before the deal, so it is the seller's.
|
||||||
|
assertEquals(31, rows.size());
|
||||||
|
|
||||||
|
// First coupon: 1,20,833 paid to the seller is recovered, only the rest is our income.
|
||||||
|
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "120833", "254167");
|
||||||
|
// 31 March closing: 50 days earned since 10 Feb, nothing booked yet.
|
||||||
|
assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104167", "0", "104167");
|
||||||
|
// August coupon clears the March provision.
|
||||||
|
assertRow(rows.get(2), TYPE_COUPON, LocalDate.of(2027, 8, 10), LocalDate.of(2027, 2, 11), 180, "375000", "104167", "270833");
|
||||||
|
assertRow(rows.get(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104167", "0", "104167");
|
||||||
|
|
||||||
|
final ScheduleRow last = rows.get(rows.size() - 1);
|
||||||
|
assertEquals(TYPE_COUPON, last.type());
|
||||||
|
assertEquals(MATURITY, last.date());
|
||||||
|
|
||||||
|
// Over the life, income booked = all coupons − interest bought from the seller.
|
||||||
|
final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).subtract(new BigDecimal("120833")), totalIncome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closingOnACouponDateIsNotProvisionedTwice() {
|
||||||
|
// Coupons on 31 Mar / 30 Sep: every closing is a coupon day, so there are no provision lines.
|
||||||
|
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, LocalDate.of(2028, 3, 31),
|
||||||
|
LocalDate.of(2026, 9, 30), DEAL, BigDecimal.ZERO);
|
||||||
|
assertEquals(3, rows.size());
|
||||||
|
assertTrue(rows.stream().allMatch(r -> TYPE_COUPON.equals(r.type())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scheduleIsEmptyWithoutRateOrDates() {
|
||||||
|
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL, null).isEmpty());
|
||||||
|
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL, null).isEmpty());
|
||||||
|
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL, null).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dealNumberFollowsTheAprilToMarchFinancialYear() {
|
||||||
|
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2026, 4, 1)));
|
||||||
|
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(DEAL));
|
||||||
|
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2027, 3, 31)));
|
||||||
|
assertEquals("202728", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2027, 4, 1)));
|
||||||
|
assertEquals("209900", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2099, 12, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void approxYtmRisesWhenBoughtAtADiscount() {
|
||||||
|
final LocalDate issue = LocalDate.of(2024, 8, 10);
|
||||||
|
assertEquals(new BigDecimal("7.59"),
|
||||||
|
InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("99.50"), issue, MATURITY));
|
||||||
|
assertEquals(new BigDecimal("7.50"),
|
||||||
|
InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("100"), issue, MATURITY));
|
||||||
|
assertEquals(RATE, InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("99.50"), null, MATURITY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRow(final ScheduleRow row, final String type, final LocalDate date, final LocalDate from, final int days,
|
||||||
|
final String gross, final String alreadyBooked, final String amount) {
|
||||||
|
assertEquals(type, row.type());
|
||||||
|
assertEquals(date, row.date());
|
||||||
|
assertEquals(from, row.fromDate());
|
||||||
|
assertEquals(date, row.toDate());
|
||||||
|
assertEquals(days, row.days());
|
||||||
|
assertEquals(new BigDecimal(gross), row.grossInterest());
|
||||||
|
assertEquals(new BigDecimal(alreadyBooked), row.alreadyBooked());
|
||||||
|
assertEquals(new BigDecimal(amount), row.amount());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user