diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index 8197a2477..1bde1ad04 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -229,6 +229,7 @@ public class SavingsApiConstants { public static final String endDateParamName = "endDate"; public static final String introducerRequiredParamName = "introducerRequired"; public static final String shareLinkingEnabledParamName = "shareLinkingEnabled"; + public static final String shareAccountIdParamName = "shareAccountId"; public static final String nominationFacilityParamName = "nominationFacility"; public static final String dormancyAlertParamName = "dormancyAlert"; public static final String interestCalculateSelectorParamName = "interestCalculateSelector"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java index cd36ae858..ab0a6d421 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java @@ -212,6 +212,24 @@ public class BranchDayRegisterApiResource { final List> denominations = extractDenominations(root, "denominations"); final String openingDifferenceReason = this.fromJsonHelper.extractStringNamed("openingDifferenceReason", root); + final Map res = openDayInternal(currentUser, officeId, businessDate, openingCash, denominations, + openingDifferenceReason); + return gson.toJson(res); + } + + /** + * The actual open-day work, factored out of {@link #openDay(String)} so + * {@link #migrateCashDenomination(String)} can drive it directly with a denomination breakdown it + * has already reconciled against the cash GL account, without round-tripping through JSON. + */ + private Map openDayInternal(final AppUser currentUser, final Long officeId, final LocalDate businessDate, + final BigDecimal openingCash, final List> denominations, final String openingDifferenceReason) { + // A branch with no vault yet (a freshly set-up institution, before anyone has visited Manage + // Tellers) cannot record a denomination breakdown at all — vault-position tracking is + // per-teller. Rather than blocking the very first day open on a separate setup step, open a + // day self-provisions the office's vault the same way its GL was self-provisioned. + ensureTellerExists(officeId, businessDate); + // A branch runs one day at a time. Opening a second while the first is still open is how a // day gets stranded, so the previous one has to be closed first. final Map open = findOpenDay(officeId); @@ -281,9 +299,164 @@ public class BranchDayRegisterApiResource { res.put("businessDate", businessDate.toString()); res.put("openingCash", carried == null ? BigDecimal.ZERO : carried); res.put("status", "OPEN"); + return res; + } + + // ── POST /v1/branchday/migrate-cash-denomination ──────────────────────────── + + /** + * GET /v1/branchday/cash-gl-status?officeId=X + * + * What the office's cash-at-vault GL account currently holds, so the migration screen can show + * "put this much into the drawer" before anyone starts counting, and so the frontend knows + * whether this is a first-time setup (no balance posted yet, the count itself will define it) or + * a top-up (a balance already exists, the count must match it exactly). + */ + @GET + @Path("cash-gl-status") + public String cashGlStatus(@QueryParam("officeId") final Long officeIdParam) { + final AppUser currentUser = this.context.authenticatedUser(); + final Long officeId = resolveOffice(officeIdParam, currentUser); + + final Map result = new LinkedHashMap<>(); + result.put("officeId", officeId); + try { + final Map gl = resolveCashGlAccount(); + final Long glAccountId = ((Number) gl.get("id")).longValue(); + final BigDecimal currentBalance = currentGlBalance(glAccountId, officeId); + result.put("glAccountId", glAccountId); + result.put("glAccountName", gl.get("name")); + result.put("glCode", gl.get("gl_code")); + result.put("currentBalance", currentBalance); + result.put("hasBalance", currentBalance.compareTo(BigDecimal.ZERO) != 0); + } catch (final PlatformDataIntegrityException e) { + result.put("glAccountId", null); + result.put("configured", false); + result.put("message", e.getDefaultUserMessage()); + } + return gson.toJson(result); + } + + /** + * POST /v1/branchday/migrate-cash-denomination + * + * The one-step "new institution" path from a GL that was just seeded (Migrate Opening Balances, + * or nothing at all yet) straight to a countable vault: count the drawer, and this either + * (a) defines the cash GL's opening balance FROM that count, when nothing has been posted to it + * yet, or (b) requires the count to match what was already posted, when it has - the same + * "physical count is the source of truth, but it still has to reconcile" rule the ordinary + * Open Business Day screen applies against yesterday's closing figure. Either way, it then opens + * the business day with that same breakdown, so this single action is the whole "day 1" story. + */ + @POST + @Path("migrate-cash-denomination") + @Consumes(MediaType.APPLICATION_JSON) + public String migrateCashDenomination(final String body) { + final AppUser currentUser = this.context.authenticatedUser(); + requireDayAuthority(currentUser); + + final var root = this.fromJsonHelper.parse(body); + final Long officeId = resolveOffice(this.fromJsonHelper.extractLongNamed("officeId", root), currentUser); + final LocalDate businessDate = extractDate(root, "businessDate", + LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); + final List> denominations = extractDenominations(root, "denominations"); + final String openingDifferenceReason = this.fromJsonHelper.extractStringNamed("openingDifferenceReason", root); + + final BigDecimal denominationTotal = sumDenominations(denominations); + if (denominationTotal == null || denominationTotal.compareTo(BigDecimal.ZERO) <= 0) { + throw new PlatformDataIntegrityException("error.msg.branchday.denomination.required", + "Count at least one denomination before migrating."); + } + + final Map gl = resolveCashGlAccount(); + final Long glAccountId = ((Number) gl.get("id")).longValue(); + final BigDecimal currentBalance = currentGlBalance(glAccountId, officeId); + final boolean glAlreadyHasABalance = currentBalance.compareTo(BigDecimal.ZERO) != 0; + + if (glAlreadyHasABalance) { + if (denominationTotal.compareTo(currentBalance) != 0) { + throw new PlatformDataIntegrityException("error.msg.branchday.cash.gl.mismatch", + "The cash GL account (" + gl.get("name") + ") already carries Rs. " + currentBalance + + " — the counted denomination total (Rs. " + denominationTotal + + ") must match it exactly."); + } + } else { + postCashOpeningBalance(officeId, businessDate, glAccountId, denominationTotal, currentUser); + } + + final Map res = openDayInternal(currentUser, officeId, businessDate, null, denominations, + openingDifferenceReason); + res.put("glAccountId", glAccountId); + res.put("glAccountName", gl.get("name")); + res.put("glOpeningBalancePosted", !glAlreadyHasABalance); return gson.toJson(res); } + /** The GL account this tenant's cash-at-vault / cash-at-teller financial activities are mapped + * to — the one account this office's counted cash is supposed to agree with. */ + private Map resolveCashGlAccount() { + final List> rows = jdbcTemplate.queryForList(""" + SELECT g.id, g.name, g.gl_code FROM acc_gl_financial_activity_account f + JOIN acc_gl_account g ON g.id = f.gl_account_id + WHERE f.financial_activity_type IN (101, 102) + ORDER BY f.financial_activity_type LIMIT 1 + """); + if (rows.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.branchday.cash.account.not.configured", + "No Cash at Vault / Cash at Teller account is configured for this tenant yet. " + + "Map Financial Activity 101 (or 102) to a GL account first."); + } + return rows.get(0); + } + + /** Net Dr-minus-Cr posted to this GL account for this office so far — zero means nothing has + * ever been posted to it, the ordinary "brand new ledger" state. */ + private BigDecimal currentGlBalance(final Long glAccountId, final Long officeId) { + final BigDecimal net = jdbcTemplate.queryForObject(""" + SELECT COALESCE(SUM(CASE WHEN type_enum = 2 THEN amount ELSE -amount END), 0) + FROM acc_gl_journal_entry + WHERE account_id = ? AND office_id = ? AND reversed = false + """, BigDecimal.class, glAccountId, officeId); + return net == null ? BigDecimal.ZERO : net; + } + + /** Posts a single-account opening balance: Dr the cash GL, Cr the Opening Balances Transfer + * Contra account (financial activity 300) - the same contra leg Fineract's own "Define Opening + * Balances" journal entry uses, just scoped to the one account this screen is responsible for + * instead of the whole chart of accounts. */ + private void postCashOpeningBalance(final Long officeId, final LocalDate businessDate, final Long cashGlAccountId, + final BigDecimal amount, final AppUser currentUser) { + final List contraIds = jdbcTemplate.queryForList(""" + SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type = 300 + """, Long.class); + if (contraIds.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.branchday.opening.balance.contra.not.configured", + "The Opening Balances Transfer Contra account (Financial Activity 300) is not configured for this tenant."); + } + final Long contraId = contraIds.get(0); + final String currencyCode = jdbcTemplate.queryForObject( + "SELECT code FROM m_organisation_currency ORDER BY code LIMIT 1", String.class); + final String transactionId = "CASHMIG-" + officeId + "-" + businessDate.toString().replace("-", "") + "-" + + System.nanoTime(); + + insertJournalEntry(officeId, cashGlAccountId, currencyCode, transactionId, businessDate, 2, amount, currentUser); + insertJournalEntry(officeId, contraId, currencyCode, transactionId, businessDate, 1, amount, currentUser); + } + + /** type_enum: 2 = DEBIT, 1 = CREDIT (matches org.apache.fineract.accounting.journalentry.domain.JournalEntryType). */ + private void insertJournalEntry(final Long officeId, final Long accountId, final String currencyCode, + final String transactionId, final LocalDate entryDate, final int typeEnum, final BigDecimal amount, + final AppUser currentUser) { + jdbcTemplate.update(""" + INSERT INTO acc_gl_journal_entry + (office_id, account_id, currency_code, transaction_id, reversed, manual_entry, entry_date, type_enum, + amount, created_by, last_modified_by, created_date, lastmodified_date, created_on_utc, + last_modified_on_utc, submitted_on_date, transaction_date) + VALUES (?,?,?,?,false,true,?,?,?,?,?,now(),now(),now(),now(),?,?) + """, officeId, accountId, currencyCode, transactionId, Date.valueOf(entryDate), typeEnum, amount, + currentUser.getId(), currentUser.getId(), Date.valueOf(DateUtils.getBusinessLocalDate()), Date.valueOf(entryDate)); + } + // ── POST /v1/branchday/close ────────────────────────────────────────────── @POST @@ -384,10 +557,17 @@ public class BranchDayRegisterApiResource { } final Long registerId = registerIds.get(0); + // Vault-only legs (OPENING_VAULT/CLOSING_VAULT - see VaultPositionService) live in this same + // table alongside the branch-total OPENING/CLOSING legs this endpoint is actually about. An + // office with a single teller records the same denomination breakdown under both pairs of + // legs, so an unfiltered "everything that isn't OPENING must be closing" catch-all silently + // folds the vault-only rows into "closing" too and doubles every count - explicit leg names, + // not an else branch, is what keeps this endpoint to the branch total it was always meant to + // report. final List> rows = jdbcTemplate.queryForList(""" SELECT leg, denomination_id, denomination_value, count, total_amount FROM m_branch_day_register_detail - WHERE register_id = ? + WHERE register_id = ? AND leg IN ('OPENING', 'CLOSING') ORDER BY denomination_value DESC """, registerId); @@ -707,6 +887,41 @@ public class BranchDayRegisterApiResource { return v == null ? BigDecimal.ZERO : v; } + /** + * Creates the office's own vault (teller) the first time its business day is ever opened, so a + * freshly set-up institution can go straight from Migrate Opening Balances to Open Business Day + * without a separate trip through Manage Tellers first. Left untouched once one exists — this + * only provisions, it never re-configures an office's existing vault setup. + */ + private void ensureTellerExists(final Long officeId, final LocalDate businessDate) { + final Integer existing = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_tellers WHERE office_id = ?", + Integer.class, officeId); + if (existing != null && existing > 0) { + return; + } + + // The same GL account the branch's own cash-at-vault/cash-at-teller financial activities are + // mapped to (set up alongside the office's chart of accounts) - reusing it here is what keeps + // the vault's physical cash and the branch's own "Cash on Hand" ledger balance pointed at the + // same account, rather than inventing a second one nobody configured. + final Long cashAccountId = ((Number) resolveCashGlAccount().get("id")).longValue(); + + final String officeName = jdbcTemplate.queryForObject("SELECT name FROM m_office WHERE id = ?", String.class, + officeId); + String tellerName = officeName + " Vault"; + final Integer nameTaken = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_tellers WHERE name = ?", Integer.class, + tellerName); + if (nameTaken != null && nameTaken > 0) { + tellerName = tellerName + " " + officeId; + } + + jdbcTemplate.update(""" + INSERT INTO m_tellers (name, office_id, description, state, valid_from, debit_account_id, credit_account_id) + VALUES (?, ?, ?, 300, ?, ?, ?) + """, tellerName, officeId, "Auto-provisioned on first business day open", Date.valueOf(businessDate), + cashAccountId, cashAccountId); + } + private Long resolveOffice(final Long requested, final AppUser user) { if (requested != null) { return requested; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 4125e1df2..8453b6e92 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -43,6 +43,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominati import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareAccountIdParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; @@ -81,6 +82,7 @@ import java.util.List; import java.util.Set; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; @@ -406,6 +408,28 @@ public class SavingsAccountAssembler { account.setShareLinkingEnabled(account.savingsProduct().isShareLinkingEnabled()); } + // Which share account this savings account is linked to - required once Share Linking is + // enabled, since a "Yes" with nothing chosen would leave the toggle meaning nothing. Checked + // against m_share_account directly (raw SQL, not the JPA entity - fineract-savings has no + // dependency on the shareaccounts domain class) rather than trusted blindly, the same way an + // account number typed for a linked savings/loan account elsewhere in this form is verified + // before being accepted. + if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) { + final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element); + if (shareAccountId == null) { + throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required", + "A share account must be chosen when Share Linking is enabled."); + } + final Integer matches = this.jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class, + shareAccountId, account.clientId()); + if (matches == null || matches == 0) { + throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client", + "Share account " + shareAccountId + " is not an active share account for this client."); + } + account.setShareAccountId(shareAccountId); + } + // Senior citizen rate — runs after validateDomainRules so it is the final persisted value if (command.parameterExists(isSeniorCitizenParamName)) { final Boolean scFlag = this.fromApiJsonHelper.extractBooleanNamed(isSeniorCitizenParamName, element); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 002a025ed..b6268af91 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -116,6 +116,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl private final GroupRepositoryWrapper groupRepositoryWrapper; private final GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService; private final ClientNomineeWritePlatformService clientNomineeWritePlatformService; + private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate; @Transactional @Override @@ -257,6 +258,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl } final Long savingsId = account.getId(); + + // Mirrors the link back onto the share account this savings account was linked to at + // creation (share account -> savings account already existed; this is the reverse + // pointer), so either side can be navigated to from the other, the same way the "Default + // Savings Account" chosen on a share account points back at it. + if (account.getShareAccountId() != null) { + this.jdbcTemplate.update("UPDATE m_share_account SET savings_account_id = ? WHERE id = ?", savingsId, + account.getShareAccountId()); + } + if (command.parameterExists(SavingsApiConstants.datatables)) { this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getValue(), EntityTables.SAVINGS.getName(), savingsId, account.productId(), command.arrayOfParameterNamed(SavingsApiConstants.datatables)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index d732e933a..180bfc0da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -518,13 +518,13 @@ public class SavingsConfiguration { AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository, GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService, - ClientNomineeWritePlatformService clientNomineeWritePlatformService) { + ClientNomineeWritePlatformService clientNomineeWritePlatformService, org.springframework.jdbc.core.JdbcTemplate jdbcTemplate) { return new SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, savingAccountAssembler, savingsAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository, savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler, commandProcessingService, savingsAccountDomainService, savingsAccountWritePlatformService, accountNumberFormatRepository, businessEventNotifierService, entityDatatableChecksWritePlatformService, gsimRepository, groupRepositoryWrapper, - gsimWritePlatformService, clientNomineeWritePlatformService); + gsimWritePlatformService, clientNomineeWritePlatformService, jdbcTemplate); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ClientActiveShareAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ClientActiveShareAccountsApiResource.java new file mode 100644 index 000000000..f5b35ab1f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ClientActiveShareAccountsApiResource.java @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.shareaccounts.api; + +import com.google.gson.Gson; +import jakarta.ws.rs.GET; +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 java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** + * GET /v1/clients/{clientId}/activeshareaccounts + * + * A client's active share accounts, for the "Default Share Account" picker the Savings Account + * creation screen shows once Share Linking is enabled - the reverse of the "Default Savings + * Account" picker the Share Account creation screen already shows (which comes from + * {@code ShareAccountReadPlatformServiceImpl#retrieveActiveForLookup}). Deliberately its own small + * endpoint rather than folded into the savings account template response, which is shared by + * Savings, Fixed Deposit, Recurring Deposit and Current accounts alike - bolting a share-specific + * list onto that one shared, deeply-nested DTO would touch every one of those account types for a + * field only Savings actually uses. + */ +@Path("/v1/clients/{clientId}/activeshareaccounts") +@Component +@RequiredArgsConstructor +@Produces({ MediaType.APPLICATION_JSON }) +public class ClientActiveShareAccountsApiResource { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @GET + public String retrieve(@PathParam("clientId") final Long clientId) { + this.context.authenticatedUser(); + final List> rows = jdbcTemplate.queryForList(""" + SELECT sa.id AS id, sa.account_no AS "accountNo", sp.name AS "productName" + FROM m_share_account sa JOIN m_share_product sp ON sp.id = sa.product_id + WHERE sa.client_id = ? AND sa.status_enum = 300 + ORDER BY sa.account_no + """, clientId); + return gson.toJson(rows); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java index fec686e0e..fa87d7cfc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java @@ -368,7 +368,7 @@ public class ShareAccount extends AbstractPersistableCustom { } public String getSavingsAccountNo() { - return this.savingsAccount.getAccountNumber(); + return this.savingsAccount == null ? null : this.savingsAccount.getAccountNumber(); } public void addAddtionalShares(Set additionalShares) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java index 30a3a348d..145123138 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java @@ -158,8 +158,10 @@ public class ShareAccountDataSerializer { baseDataValidator.reset().parameter(ShareAccountApiConstants.resolutionnumber_paramname).value(resolutionNumber).notBlank(); Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element); - baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull() - .longGreaterThanZero(); + if (savingsAccountId != null) { + baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId) + .longGreaterThanZero(); + } final Long requestedShares = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.requestedshares_paramname, element); baseDataValidator.reset().parameter(ShareAccountApiConstants.requestedshares_paramname).value(requestedShares).notNull() .longGreaterThanZero(); @@ -200,12 +202,15 @@ public class ShareAccountDataSerializer { } Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); - if (!this.savingsAccountReadPlatformService.isAccountBelongsToClient(clientId, savingsAccountId, DepositAccountType.SAVINGS_DEPOSIT, - shareProduct.getCurrency().getCode())) { - throw new SavingsAccountNotFoundException(savingsAccountId); + SavingsAccount savingsAccount = null; + if (savingsAccountId != null) { + if (!this.savingsAccountReadPlatformService.isAccountBelongsToClient(clientId, savingsAccountId, + DepositAccountType.SAVINGS_DEPOSIT, shareProduct.getCurrency().getCode())) { + throw new SavingsAccountNotFoundException(savingsAccountId); + } + savingsAccount = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId, + DepositAccountType.SAVINGS_DEPOSIT); } - SavingsAccount savingsAccount = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId, - DepositAccountType.SAVINGS_DEPOSIT); final MonetaryCurrency currency = shareProduct.getCurrency(); Set charges = assembleListOfAccountCharges(element, currency.getCode()); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 09a09f1a0..ed092bed4 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -50,4 +50,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0524_seed_basoda_bank_income_expense_accounts.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0524_seed_basoda_bank_income_expense_accounts.xml new file mode 100644 index 000000000..7cfe456fe --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0524_seed_basoda_bank_income_expense_accounts.xml @@ -0,0 +1,45 @@ + + + + + + + + + SELECT COUNT(*) FROM acc_gl_account WHERE gl_code = '301000' + SELECT COUNT(*) FROM acc_gl_account WHERE gl_code = '401000' + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java index 599ac65c7..83a44b611 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java @@ -48,7 +48,8 @@ public class SavingsAccountConstant extends SavingsApiConstants { introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, accountFacilityConfigParamName, jointHoldersParamName, - balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName, introducerRequiredParamName, shareLinkingEnabledParamName)); + balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName, introducerRequiredParamName, shareLinkingEnabledParamName, + shareAccountIdParamName)); /** * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 5c6eea230..3eb427dcf 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -4426,6 +4426,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @Column(name = "share_linking_enabled", nullable = false, columnDefinition = "boolean default false") private Boolean shareLinkingEnabled = false; + /** + * Plain id, not a JPA relationship - fineract-savings cannot depend on the shareaccounts domain + * class (it lives in fineract-provider, which depends on fineract-savings, not the other way + * round). The write side resolves and validates this id against m_share_account directly; this + * column just carries it back to the share account's own {@code savings_account_id}, mirroring + * the link from both ends. + */ + @Column(name = "share_account_id") + private Long shareAccountId; + @Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false") private Boolean nominationFacility = false; @@ -4532,6 +4542,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom public Boolean getShareLinkingEnabled() { return shareLinkingEnabled; } public void setShareLinkingEnabled(Boolean v) { this.shareLinkingEnabled = v; } + public Long getShareAccountId() { return shareAccountId; } + public void setShareAccountId(Long v) { this.shareAccountId = v; } + public boolean isSeniorCitizen() { return isSeniorCitizen; } public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; } diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml index ec16ad31d..d2b0790a5 100644 --- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml @@ -53,4 +53,5 @@ + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2032_add_share_account_id_to_savings_account.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2032_add_share_account_id_to_savings_account.xml new file mode 100644 index 000000000..bcb206aae --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2032_add_share_account_id_to_savings_account.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + +