From 9d944d01966dd19b0c8701ba99adac51aefba00e Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 4 Jun 2026 13:22:33 +0530 Subject: [PATCH] feat(share): add cashier share collection endpoint with GL posting and activation validation - New POST /cashier/sharecollection endpoint: collects cash for share purchase, posts Dr Cash Vault / Cr Share Sundry / Cr Income GL entries, marks charges as paid and sets cashier_collected flag on the account - GET /cashier/sharecollection/:id endpoint for collection status check - Block share account activation if cashier has not collected cash first (validates m_share_cashier_collection record within last 30 days) - Skip duplicate charge GL on activation when charge already collected upfront - Suppress automatic GL posting at account submission time to prevent double entries; GL is now posted only via cashier collection endpoint - Fix JdbcTemplate injection in ShareAccountWritePlatformServiceJpaRepositoryImpl via ShareAccountsConfiguration Co-Authored-By: Claude Sonnet 4.6 --- .../api/ShareCollectionApiResource.java | 424 +++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 1069 +++++++++-------- .../start/ShareAccountsConfiguration.java | 247 ++-- 3 files changed, 1111 insertions(+), 629 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/ShareCollectionApiResource.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/ShareCollectionApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/ShareCollectionApiResource.java new file mode 100644 index 000000000..c59def7ee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/ShareCollectionApiResource.java @@ -0,0 +1,424 @@ +package org.apache.fineract.portfolio.cashtransfer.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Cashier Share Collection API — POST /v1/cashier/sharecollection + * + * Records cash received from a member for share purchase and posts GL entries: + * Dr SHARES_REFERENCE (Cash at Vault) = shareAmount + chargeAmount + * Cr SHARES_SUSPENSE (Share Sundry) = shareAmount + * Cr INCOME_FROM_FEES = chargeAmount (if > 0) + * + * On account activation Fineract posts: + * Dr SHARES_SUSPENSE → Cr SHARES_EQUITY + * Activation skips charge GL if is_paid_derived = true (set here). + */ +@Path("/v1/cashier/sharecollection") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class ShareCollectionApiResource { + + // ── acc_gl_journal_entry.type_enum ───────────────────────────────────── + // Fineract convention: 1 = CREDIT (Cr), 2 = DEBIT (Dr) + // Opposite of intuition — verified from Fineract AccountingProcessorHelper. + private static final int DEBIT = 2; + private static final int CREDIT = 1; + + // ── CashAccountsForShares financial_account_type ─────────────────────── + private static final int FA_SHARES_REFERENCE = 1; + private static final int FA_SHARES_SUSPENSE = 2; + private static final int FA_INCOME_FROM_FEES = 3; + + // ── acc_gl_journal_entry.entity_type_enum ───────────────────────────── + // Manual cashier entries use NULL — avoids NPE in PortfolioProductType.fromInt() + // which only handles values 1-5 and returns null for unknown values. + + // ── acc_product_mapping.product_type for shares ──────────────────────── + private static final int PRODUCT_TYPE_SHARES = 4; + + // ── m_share_account.status_enum ─────────────────────────────────────── + private static final int STATUS_SUBMITTED_PENDING = 100; + + // ── Maximum days a collection record is considered valid ────────────── + private static final int MAX_COLLECTION_AGE_DAYS = 30; + + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + private final Gson gson = new Gson(); + + // ══════════════════════════════════════════════════════════════════════ + // POST — Collect cash for a share account + // ══════════════════════════════════════════════════════════════════════ + @POST + @Transactional + public Response collect(final String body) { + + final AppUser currentUser = this.context.authenticatedUser(); + currentUser.validateHasPermissionTo("CREATE_JOURNALENTRY"); + + // ── Parse request ────────────────────────────────────────────────── + if (body == null || body.isBlank()) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.request.empty", + "Request body is required."); + } + final JsonObject json; + try { + json = JsonParser.parseString(body).getAsJsonObject(); + } catch (Exception e) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.request.invalid", + "Request body must be valid JSON."); + } + + final Long shareAccountId = getLong(json, "shareAccountId"); + final String transactionDateStr = getString(json, "transactionDate"); + final BigDecimal shareAmount = getBigDecimal(json, "shareAmount", "shareAmount"); + final BigDecimal chargeAmountRaw = getBigDecimal(json, "chargeAmount", "chargeAmount"); + final String note = sanitiseNote(getString(json, "note")); + + // ── Validate inputs ──────────────────────────────────────────────── + if (shareAccountId == null) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.accountid.required", + "shareAccountId is required."); + } + if (shareAmount.compareTo(BigDecimal.ZERO) < 0) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.shareamount.negative", + "shareAmount cannot be negative."); + } + final BigDecimal safeCharge = chargeAmountRaw.compareTo(BigDecimal.ZERO) > 0 + ? chargeAmountRaw : BigDecimal.ZERO; + final BigDecimal totalAmount = shareAmount.add(safeCharge); + if (totalAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.total.zero", + "Total collection amount must be greater than zero."); + } + + final LocalDate entryDate; + try { + entryDate = transactionDateStr != null + ? LocalDate.parse(transactionDateStr) : LocalDate.now(); + } catch (DateTimeParseException e) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.date.invalid", + "transactionDate must be in YYYY-MM-DD format. Received: " + transactionDateStr); + } + + // ── Load account + client details ────────────────────────────────── + final Map acct = loadAccount(shareAccountId); + + // ── Account must be in 'Submitted and pending approval' ─────────── + final int statusEnum = toInt(acct.get("status_enum")); + if (statusEnum != STATUS_SUBMITTED_PENDING) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.account.invalid.status", + "Cash collection is only allowed for accounts in 'Submitted and pending approval' " + + "status. Account " + acct.get("account_no") + " has status code " + statusEnum + "."); + } + + // ── Prevent double collection ────────────────────────────────────── + if (Boolean.TRUE.equals(acct.get("cashier_collected"))) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.already.collected", + "Cash has already been collected for share account " + + acct.get("account_no") + ". Re-collection is not allowed."); + } + + final Long officeId = toLong(acct.get("office_id")); + final Long productId = toLong(acct.get("product_id")); + final String currencyCode = (String) acct.get("currency_code"); + final String clientName = (String) acct.get("client_name"); + final String accountNo = (String) acct.get("account_no"); + + // ── Resolve GL accounts from product mapping ─────────────────────── + final Long glReference = resolveGl(productId, FA_SHARES_REFERENCE); + final Long glSuspense = resolveGl(productId, FA_SHARES_SUSPENSE); + final Long glIncome = resolveGl(productId, FA_INCOME_FROM_FEES); + + if (glReference == null) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.gl.reference.notmapped", + "Share Reference (Cash at Vault) GL is not mapped for product " + productId + + ". Configure Share Product accounting mappings first."); + } + if (glSuspense == null) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.gl.suspense.notmapped", + "Share Suspense GL is not mapped for product " + productId + + ". Configure Share Product accounting mappings first."); + } + if (safeCharge.compareTo(BigDecimal.ZERO) > 0 && glIncome == null) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.gl.income.notmapped", + "Income from Fees GL is not mapped for product " + productId + + " but charge amount " + safeCharge + " is present. " + + "Configure Share Product accounting mappings first."); + } + + // ── Generate unique transaction ID ───────────────────────────────── + final String txnId = "SC-" + shareAccountId + "-" + System.currentTimeMillis(); + final long userId = currentUser.getId(); + final String collectorName = currentUser.getUsername(); + final LocalDateTime now = LocalDateTime.now(); + + // ── Post GL journal entries ──────────────────────────────────────── + // Dr Cash at Vault (SHARES_REFERENCE) = totalAmount + insertEntry(officeId, glReference, DEBIT, totalAmount, txnId, + entryDate, shareAccountId, userId, now, currencyCode, + "Share collection received for " + accountNo + + (note != null ? " | " + note : "")); + + // Cr Share Sundry (SHARES_SUSPENSE) = shareAmount + insertEntry(officeId, glSuspense, CREDIT, shareAmount, txnId, + entryDate, shareAccountId, userId, now, currencyCode, + "Share amount held in suspense - " + accountNo); + + // Cr Income from Fees = chargeAmount (only if > 0) + if (safeCharge.compareTo(BigDecimal.ZERO) > 0) { + insertEntry(officeId, glIncome, CREDIT, safeCharge, txnId, + entryDate, shareAccountId, userId, now, currencyCode, + "Charge collected upfront for " + accountNo); + + // Mark charges paid → activation will skip duplicate charge GL entry + jdbcTemplate.update( + "UPDATE m_share_account_charge " + + "SET is_paid_derived = true, " + + " amount_paid_derived = amount, " + + " amount_outstanding_derived = 0 " + + "WHERE account_id = ? AND is_active = true AND is_paid_derived = false", + shareAccountId); + } + + // ── Flip cashier_collected flag ──────────────────────────────────── + jdbcTemplate.update( + "UPDATE m_share_account SET cashier_collected = true WHERE id = ?", + shareAccountId); + + // ── Insert audit / validation record ────────────────────────────── + jdbcTemplate.update( + "INSERT INTO m_share_cashier_collection " + + "(share_account_id, collected_by_user, collected_by_name, collection_date, " + + " share_amount, charge_amount, total_amount, gl_transaction_id, " + + " office_id, currency_code, note) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?)", + shareAccountId, userId, collectorName, + java.sql.Date.valueOf(entryDate), + shareAmount, safeCharge, totalAmount, + txnId, officeId, currencyCode, note); + + // ── Build GL entries summary for response ────────────────────────── + final List> glEntries = new ArrayList<>(); + glEntries.add(glEntryMap(resolveGlName(glReference), "Dr", totalAmount, currencyCode)); + glEntries.add(glEntryMap(resolveGlName(glSuspense), "Cr", shareAmount, currencyCode)); + if (safeCharge.compareTo(BigDecimal.ZERO) > 0) { + glEntries.add(glEntryMap(resolveGlName(glIncome), "Cr", safeCharge, currencyCode)); + } + + // ── Response ─────────────────────────────────────────────────────── + final Map resp = new LinkedHashMap<>(); + resp.put("transactionId", txnId); + resp.put("shareAccountId", shareAccountId); + resp.put("accountNo", accountNo); + resp.put("clientName", clientName); + resp.put("collectionDate", entryDate.toString()); + resp.put("collectedBy", collectorName); + resp.put("shareAmount", shareAmount); + resp.put("chargeAmount", safeCharge); + resp.put("totalAmount", totalAmount); + resp.put("currencyCode", currencyCode); + resp.put("glEntries", glEntries); + + return Response.ok(gson.toJson(resp)).build(); + } + + // ══════════════════════════════════════════════════════════════════════ + // GET — Check collection status for a share account + // ══════════════════════════════════════════════════════════════════════ + @GET + @Path("/{shareAccountId}") + public Response getStatus(@PathParam("shareAccountId") final Long shareAccountId) { + this.context.authenticatedUser(); + + try { + final Map rec = jdbcTemplate.queryForMap( + "SELECT scc.share_account_id, sa.account_no, " + + " mc.display_name AS client_name, " + + " scc.collected_by_name, scc.collection_date, " + + " scc.share_amount, scc.charge_amount, scc.total_amount, " + + " scc.gl_transaction_id, scc.currency_code, scc.note " + + "FROM m_share_cashier_collection scc " + + "JOIN m_share_account sa ON sa.id = scc.share_account_id " + + "JOIN m_client mc ON mc.id = sa.client_id " + + "WHERE scc.share_account_id = ?", + shareAccountId); + + final Map resp = new LinkedHashMap<>(); + resp.put("collected", true); + resp.put("shareAccountId", shareAccountId); + resp.put("accountNo", rec.get("account_no")); + resp.put("clientName", rec.get("client_name")); + resp.put("collectedBy", rec.get("collected_by_name")); + resp.put("collectionDate", rec.get("collection_date") != null + ? rec.get("collection_date").toString() : null); + resp.put("shareAmount", rec.get("share_amount")); + resp.put("chargeAmount", rec.get("charge_amount")); + resp.put("totalAmount", rec.get("total_amount")); + resp.put("transactionId", rec.get("gl_transaction_id")); + resp.put("currencyCode", rec.get("currency_code")); + resp.put("note", rec.get("note")); + return Response.ok(gson.toJson(resp)).build(); + + } catch (EmptyResultDataAccessException e) { + final Map resp = new LinkedHashMap<>(); + resp.put("collected", false); + resp.put("shareAccountId", shareAccountId); + return Response.ok(gson.toJson(resp)).build(); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // Private helpers + // ══════════════════════════════════════════════════════════════════════ + + private Map loadAccount(final Long shareAccountId) { + try { + return jdbcTemplate.queryForMap( + "SELECT sa.id, sa.account_no, sa.status_enum, sa.product_id, " + + " sa.cashier_collected, sa.currency_code, " + + " mc.office_id, mc.display_name AS client_name " + + "FROM m_share_account sa " + + "JOIN m_client mc ON mc.id = sa.client_id " + + "WHERE sa.id = ?", + shareAccountId); + } catch (EmptyResultDataAccessException e) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.account.notfound", + "Share account with id " + shareAccountId + " does not exist."); + } + } + + private Long resolveGl(final Long productId, final int financialAccountType) { + try { + return jdbcTemplate.queryForObject( + "SELECT gl_account_id FROM acc_product_mapping " + + "WHERE product_id = ? AND product_type = ? AND financial_account_type = ?", + Long.class, productId, PRODUCT_TYPE_SHARES, financialAccountType); + } catch (IncorrectResultSizeDataAccessException e) { + // covers both EmptyResultDataAccessException (0 rows) and multiple rows + return null; + } + } + + private String resolveGlName(final Long glAccountId) { + if (glAccountId == null) return "Unknown GL"; + try { + return jdbcTemplate.queryForObject( + "SELECT name FROM acc_gl_account WHERE id = ?", + String.class, glAccountId); + } catch (EmptyResultDataAccessException e) { + return "GL-" + glAccountId; + } + } + + private void insertEntry(final Long officeId, final Long glAccountId, + final int typeEnum, final BigDecimal amount, final String txnId, + final LocalDate entryDate, final Long shareAccountId, + final long userId, final LocalDateTime now, final String currencyCode, + final String description) { + jdbcTemplate.update( + "INSERT INTO acc_gl_journal_entry " + + "(account_id, office_id, currency_code, transaction_id, reversed, manual_entry, " + + " entry_date, type_enum, amount, description, entity_id, " + + " created_by, last_modified_by, created_date, lastmodified_date, " + + " is_running_balance_calculated, office_running_balance, " + + " organization_running_balance, submitted_on_date, transaction_date, " + + " created_on_utc, last_modified_on_utc) " + + "VALUES (?,?,?,?,false,true,?,?,?,?,?,?,?,?,?,false,0,0,?,?,?,?)", + glAccountId, officeId, currencyCode, txnId, + java.sql.Date.valueOf(entryDate), typeEnum, amount, description, + shareAccountId, // entity_id — links entry to the share account + userId, userId, now, now, + java.sql.Date.valueOf(entryDate), + java.sql.Date.valueOf(entryDate), + now.atOffset(ZoneOffset.UTC), + now.atOffset(ZoneOffset.UTC)); + } + + private Map glEntryMap(final String account, final String type, + final BigDecimal amount, final String currency) { + final Map m = new LinkedHashMap<>(); + m.put("account", account); + m.put("type", type); + m.put("amount", amount); + m.put("currency", currency); + return m; + } + + private String sanitiseNote(final String raw) { + if (raw == null || raw.isBlank()) return null; + final String trimmed = raw.trim(); + return trimmed.length() > 490 ? trimmed.substring(0, 490) : trimmed; + } + + private Long toLong(final Object o) { + return o instanceof Number ? ((Number) o).longValue() : null; + } + + private int toInt(final Object o) { + return o instanceof Number ? ((Number) o).intValue() : 0; + } + + private Long getLong(final JsonObject json, final String key) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsLong() : null; + } + + private BigDecimal getBigDecimal(final JsonObject json, final String key, + final String fieldLabel) { + if (!json.has(key) || json.get(key).isJsonNull()) return BigDecimal.ZERO; + try { + return json.get(key).getAsBigDecimal(); + } catch (NumberFormatException e) { + throw new PlatformDataIntegrityException( + "error.msg.sharecollection.field.invalid.number", + "Field '" + fieldLabel + "' must be a valid number."); + } + } + + private String getString(final JsonObject json, final String key) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : null; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java index ca6d16543..1ea5985da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,506 +1,563 @@ -/** - * 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.service; - -import jakarta.persistence.PersistenceException; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat; -import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; -import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType; -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.ErrorHandler; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.event.business.domain.share.ShareAccountApproveBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.share.ShareAccountCreateBusinessEvent; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; -import org.apache.fineract.portfolio.accounts.constants.ShareAccountApiConstants; -import org.apache.fineract.portfolio.note.domain.Note; -import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountChargePaidBy; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountTransaction; -import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; -import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct; -import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.orm.jpa.JpaSystemException; - -@RequiredArgsConstructor -public class ShareAccountWritePlatformServiceJpaRepositoryImpl implements ShareAccountWritePlatformService { - - private final ShareAccountDataSerializer accountDataSerializer; - - private final ShareAccountRepositoryWrapper shareAccountRepository; - - private final ShareProductRepositoryWrapper shareProductRepository; - - private final AccountNumberGenerator accountNumberGenerator; - - private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; - - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - - private final NoteRepository noteRepository; - - private final BusinessEventNotifierService businessEventNotifierService; - - @Override - public CommandProcessingResult createShareAccount(JsonCommand jsonCommand) { - try { - ShareAccount account = this.accountDataSerializer.validateAndCreate(jsonCommand); - this.shareAccountRepository.saveAndFlush(account); - generateAccountNumber(account); - journalEntryWritePlatformService.createJournalEntriesForShares( - populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions())); - - businessEventNotifierService.notifyPostBusinessEvent(new ShareAccountCreateBusinessEvent(account)); - - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(account.getId()) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } catch (final PersistenceException dve) { - Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); - handleDataIntegrityIssues(jsonCommand, throwable, dve); - return CommandProcessingResult.empty(); - } - } - - private void generateAccountNumber(final ShareAccount account) { - if (account.isAccountNumberRequiresAutoGeneration()) { - final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository.findByAccountType(EntityAccountType.SHARES); - account.updateAccountNumber(this.accountNumberGenerator.generate(account, accountNumberFormat)); - this.shareAccountRepository.save(account); - } - } - - private Map populateJournalEntries(final ShareAccount account, final Set transactions) { - final Map accountingBridgeData = new HashMap<>(); - Boolean cashBasedAccounting = account.getShareProduct().getAccountingType() == 2; - accountingBridgeData.put("cashBasedAccountingEnabled", cashBasedAccounting); - accountingBridgeData.put("accrualBasedAccountingEnabled", Boolean.FALSE); - accountingBridgeData.put("shareAccountId", account.getId()); - accountingBridgeData.put("shareProductId", account.getShareProduct().getId()); - accountingBridgeData.put("officeId", account.getOfficeId()); - accountingBridgeData.put("currencyCode", account.getCurrency().getCode()); - final List> newTransactionsMap = new ArrayList<>(); - accountingBridgeData.put("newTransactions", newTransactionsMap); - - for (ShareAccountTransaction transaction : transactions) { - final Map transactionDto = new HashMap<>(); - transactionDto.put("officeId", account.getOfficeId()); - transactionDto.put("id", transaction.getId()); - transactionDto.put("date", transaction.getPurchasedDate()); - final Integer status = transaction.getTransactionStatus(); - final ShareAccountTransactionEnumData statusEnum = new ShareAccountTransactionEnumData(status.longValue(), null, null); - final Integer type = transaction.getTransactionType(); - final ShareAccountTransactionEnumData typeEnum = new ShareAccountTransactionEnumData(type.longValue(), null, null); - transactionDto.put("status", statusEnum); - transactionDto.put("type", typeEnum); - if (transaction.isPurchaseRejectedTransaction() || transaction.isRedeemTransaction()) { - BigDecimal amount = transaction.amount(); - if (transaction.chargeAmount() != null) { - amount = amount.add(transaction.chargeAmount()); - } - transactionDto.put("amount", amount); - } else { - transactionDto.put("amount", transaction.amount()); - } - - transactionDto.put("chargeAmount", transaction.chargeAmount()); - transactionDto.put("paymentTypeId", null); // FIXME::make it cash - // payment - if (transaction.getChargesPaidBy() != null && !transaction.getChargesPaidBy().isEmpty()) { - final List> chargesPaidData = new ArrayList<>(); - transactionDto.put("chargesPaid", chargesPaidData); - Set chargesPaidBySet = transaction.getChargesPaidBy(); - for (ShareAccountChargePaidBy chargesPaidBy : chargesPaidBySet) { - Map chargesPaidDto = new HashMap<>(); - chargesPaidDto.put("chargeId", chargesPaidBy.getChargeId()); - chargesPaidDto.put("sharesChargeId", chargesPaidBy.getShareChargeId()); - chargesPaidDto.put("amount", chargesPaidBy.getAmount()); - chargesPaidData.add(chargesPaidDto); - } - } - newTransactionsMap.add(transactionDto); - } - return accountingBridgeData; - } - - @SuppressWarnings("unchecked") - @Override - public CommandProcessingResult updateShareAccount(Long accountId, JsonCommand jsonCommand) { - try { - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndUpdate(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - } - // since we are reverting all journal entries we need to add journal - // entries for application request - if (changes.containsKey("reversalIds")) { - ArrayList reversalIds = (ArrayList) changes.get("reversalIds"); - this.journalEntryWritePlatformService.revertShareAccountJournalEntries(reversalIds, transactionDate); - journalEntryWritePlatformService.createJournalEntriesForShares( - populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions())); - changes.remove("reversalIds"); - } - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } catch (final PersistenceException dve) { - Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); - handleDataIntegrityIssues(jsonCommand, throwable, dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult applyAddtionalShares(final Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndApplyAddtionalShares(jsonCommand, account); - ShareAccountTransaction transaction = null; - if (!changes.isEmpty()) { - this.shareAccountRepository.saveAndFlush(account); - transaction = (ShareAccountTransaction) changes.get(ShareAccountApiConstants.additionalshares_paramname); - transaction = account.getShareAccountTransaction(transaction); - if (transaction != null) { - changes.clear(); - changes.put(ShareAccountApiConstants.additionalshares_paramname, transaction.getId()); - Set transactions = new HashSet<>(); - transactions.add(transaction); - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); - } - } - - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult approveShareAccount(Long accountId, JsonCommand jsonCommand) { - - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndApprove(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - final String noteText = jsonCommand.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.shareNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - } - Set transactions = account.getShareAccountTransactions(); - Set journalTransactions = new HashSet<>(); - Long totalSubsribedShares = Long.valueOf(0); - - for (ShareAccountTransaction transaction : transactions) { - if (transaction.isActive() && transaction.isPurchasTransaction()) { - journalTransactions.add(transaction); - totalSubsribedShares += transaction.getTotalShares(); - } - } - ShareProduct shareProduct = account.getShareProduct(); - shareProduct.addSubscribedShares(totalSubsribedShares); - this.shareProductRepository.save(shareProduct); - - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, journalTransactions)); - - businessEventNotifierService.notifyPostBusinessEvent(new ShareAccountApproveBusinessEvent(account)); - - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult rejectShareAccount(Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndReject(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - final String noteText = jsonCommand.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.shareNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - } - Set transactions = account.getShareAccountTransactions(); - Set journalTransactions = new HashSet<>(); - for (ShareAccountTransaction transaction : transactions) { - if (transaction.isActive() && !transaction.isChargeTransaction()) { - journalTransactions.add(transaction); - } - } - - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, journalTransactions)); - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult undoApproveShareAccount(Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndUndoApprove(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - final String noteText = jsonCommand.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.shareNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - } - - Set transactions = account.getShareAccountTransactions(); - ArrayList journalEntryTransactions = new ArrayList<>(); - for (ShareAccountTransaction transaction : transactions) { - if (transaction.isActive() && !transaction.isChargeTransaction()) { - journalEntryTransactions.add(transaction.getId()); - } - } - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - this.journalEntryWritePlatformService.revertShareAccountJournalEntries(journalEntryTransactions, transactionDate); - journalEntryWritePlatformService.createJournalEntriesForShares( - populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions())); - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult activateShareAccount(Long accountId, JsonCommand jsonCommand) { - - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndActivate(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - } - this.journalEntryWritePlatformService - .createJournalEntriesForShares(populateJournalEntries(account, account.getChargeTransactions())); - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @SuppressWarnings("unchecked") - @Override - public CommandProcessingResult approveAdditionalShares(Long accountId, JsonCommand jsonCommand) { - - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndApproveAddtionalShares(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - ArrayList transactionIds = (ArrayList) changes.get(ShareAccountApiConstants.requestedshares_paramname); - Long totalSubscribedShares = Long.valueOf(0); - if (transactionIds != null) { - Set transactions = new HashSet<>(); - for (Long id : transactionIds) { - ShareAccountTransaction transaction = account.retrievePurchasedShares(id); - transactions.add(transaction); - totalSubscribedShares += transaction.getTotalShares(); - } - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); - } - if (!totalSubscribedShares.equals(Long.valueOf(0))) { - ShareProduct shareProduct = account.getShareProduct(); - shareProduct.addSubscribedShares(totalSubscribedShares); - this.shareProductRepository.save(shareProduct); - } - } - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @SuppressWarnings("unchecked") - @Override - public CommandProcessingResult rejectAdditionalShares(Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndRejectAddtionalShares(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.save(account); - ArrayList transactionIds = (ArrayList) changes.get(ShareAccountApiConstants.requestedshares_paramname); - if (transactionIds != null) { - Set transactions = new HashSet<>(); - for (Long id : transactionIds) { - ShareAccountTransaction transaction = account.retrievePurchasedShares(id); - transactions.add(transaction); - } - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); - } - } - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult redeemShares(Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndRedeemShares(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.saveAndFlush(account); - ShareAccountTransaction transaction = (ShareAccountTransaction) changes - .get(ShareAccountApiConstants.requestedshares_paramname); - // after saving, entity will have different object. So need to - // retrieve the entity object - transaction = account.getShareAccountTransaction(transaction); - Long redeemShares = transaction.getTotalShares(); - ShareProduct shareProduct = account.getShareProduct(); - // remove the redeem shares from total subscribed shares - shareProduct.removeSubscribedShares(redeemShares); - this.shareProductRepository.saveAndFlush(shareProduct); - - Set transactions = new HashSet<>(); - transactions.add(transaction); - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); - changes.clear(); - changes.put(ShareAccountApiConstants.requestedshares_paramname, transaction.getId()); - - } - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - @Override - public CommandProcessingResult closeShareAccount(Long accountId, JsonCommand jsonCommand) { - try { - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); - Map changes = this.accountDataSerializer.validateAndClose(jsonCommand, account); - if (!changes.isEmpty()) { - this.shareAccountRepository.saveAndFlush(account); - final String noteText = jsonCommand.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.shareNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - ShareAccountTransaction transaction = (ShareAccountTransaction) changes - .get(ShareAccountApiConstants.requestedshares_paramname); - transaction = account.getShareAccountTransaction(transaction); - Set transactions = new HashSet<>(); - transactions.add(transaction); - this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); - changes.clear(); - changes.put(ShareAccountApiConstants.requestedshares_paramname, transaction.getId()); - - } - return new CommandProcessingResultBuilder() // - .withCommandId(jsonCommand.commandId()) // - .withEntityId(accountId) // - .with(changes) // - .build(); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); - return CommandProcessingResult.empty(); - } - } - - private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) { - throw ErrorHandler.getMappable(dve, "error.msg.shareaccount.unknown.data.integrity.issue", - "Unknown data integrity issue with resource."); - } -} +/** + * 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.service; + +import jakarta.persistence.PersistenceException; +import java.math.BigDecimal; +import java.sql.Date; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat; +import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; +import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType; +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.ErrorHandler; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.share.ShareAccountApproveBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.share.ShareAccountCreateBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; +import org.apache.fineract.portfolio.accounts.constants.ShareAccountApiConstants; +import org.apache.fineract.portfolio.note.domain.Note; +import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountChargePaidBy; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountTransaction; +import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; +import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct; +import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.orm.jpa.JpaSystemException; + +@RequiredArgsConstructor +public class ShareAccountWritePlatformServiceJpaRepositoryImpl implements ShareAccountWritePlatformService { + + private final ShareAccountDataSerializer accountDataSerializer; + + private final ShareAccountRepositoryWrapper shareAccountRepository; + + private final ShareProductRepositoryWrapper shareProductRepository; + + private final AccountNumberGenerator accountNumberGenerator; + + private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; + + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + + private final NoteRepository noteRepository; + + private final JdbcTemplate jdbcTemplate; + + private final BusinessEventNotifierService businessEventNotifierService; + + @Override + public CommandProcessingResult createShareAccount(JsonCommand jsonCommand) { + try { + ShareAccount account = this.accountDataSerializer.validateAndCreate(jsonCommand); + this.shareAccountRepository.saveAndFlush(account); + generateAccountNumber(account); + // GL entries are intentionally NOT posted at submission time. + // Cash is physically collected by the cashier AFTER submission via the + // /cashier/sharecollection endpoint, which posts: + // Dr Cash at Vault → Cr Share Sundry (share amount) + // Cr Income from Fees (charge amount) + // Posting GL here would cause a duplicate entry when cashier collects. + + businessEventNotifierService.notifyPostBusinessEvent(new ShareAccountCreateBusinessEvent(account)); + + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(account.getId()) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } catch (final PersistenceException dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(jsonCommand, throwable, dve); + return CommandProcessingResult.empty(); + } + } + + private void generateAccountNumber(final ShareAccount account) { + if (account.isAccountNumberRequiresAutoGeneration()) { + final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository.findByAccountType(EntityAccountType.SHARES); + account.updateAccountNumber(this.accountNumberGenerator.generate(account, accountNumberFormat)); + this.shareAccountRepository.save(account); + } + } + + private Map populateJournalEntries(final ShareAccount account, final Set transactions) { + final Map accountingBridgeData = new HashMap<>(); + Boolean cashBasedAccounting = account.getShareProduct().getAccountingType() == 2; + accountingBridgeData.put("cashBasedAccountingEnabled", cashBasedAccounting); + accountingBridgeData.put("accrualBasedAccountingEnabled", Boolean.FALSE); + accountingBridgeData.put("shareAccountId", account.getId()); + accountingBridgeData.put("shareProductId", account.getShareProduct().getId()); + accountingBridgeData.put("officeId", account.getOfficeId()); + accountingBridgeData.put("currencyCode", account.getCurrency().getCode()); + final List> newTransactionsMap = new ArrayList<>(); + accountingBridgeData.put("newTransactions", newTransactionsMap); + + for (ShareAccountTransaction transaction : transactions) { + final Map transactionDto = new HashMap<>(); + transactionDto.put("officeId", account.getOfficeId()); + transactionDto.put("id", transaction.getId()); + transactionDto.put("date", transaction.getPurchasedDate()); + final Integer status = transaction.getTransactionStatus(); + final ShareAccountTransactionEnumData statusEnum = new ShareAccountTransactionEnumData(status.longValue(), null, null); + final Integer type = transaction.getTransactionType(); + final ShareAccountTransactionEnumData typeEnum = new ShareAccountTransactionEnumData(type.longValue(), null, null); + transactionDto.put("status", statusEnum); + transactionDto.put("type", typeEnum); + if (transaction.isPurchaseRejectedTransaction() || transaction.isRedeemTransaction()) { + BigDecimal amount = transaction.amount(); + if (transaction.chargeAmount() != null) { + amount = amount.add(transaction.chargeAmount()); + } + transactionDto.put("amount", amount); + } else { + transactionDto.put("amount", transaction.amount()); + } + + transactionDto.put("chargeAmount", transaction.chargeAmount()); + transactionDto.put("paymentTypeId", null); // FIXME::make it cash + // payment + if (transaction.getChargesPaidBy() != null && !transaction.getChargesPaidBy().isEmpty()) { + final List> chargesPaidData = new ArrayList<>(); + transactionDto.put("chargesPaid", chargesPaidData); + Set chargesPaidBySet = transaction.getChargesPaidBy(); + for (ShareAccountChargePaidBy chargesPaidBy : chargesPaidBySet) { + Map chargesPaidDto = new HashMap<>(); + chargesPaidDto.put("chargeId", chargesPaidBy.getChargeId()); + chargesPaidDto.put("sharesChargeId", chargesPaidBy.getShareChargeId()); + chargesPaidDto.put("amount", chargesPaidBy.getAmount()); + chargesPaidData.add(chargesPaidDto); + } + } + newTransactionsMap.add(transactionDto); + } + return accountingBridgeData; + } + + @SuppressWarnings("unchecked") + @Override + public CommandProcessingResult updateShareAccount(Long accountId, JsonCommand jsonCommand) { + try { + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndUpdate(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + } + // since we are reverting all journal entries we need to add journal + // entries for application request + if (changes.containsKey("reversalIds")) { + ArrayList reversalIds = (ArrayList) changes.get("reversalIds"); + this.journalEntryWritePlatformService.revertShareAccountJournalEntries(reversalIds, transactionDate); + journalEntryWritePlatformService.createJournalEntriesForShares( + populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions())); + changes.remove("reversalIds"); + } + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } catch (final PersistenceException dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(jsonCommand, throwable, dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult applyAddtionalShares(final Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndApplyAddtionalShares(jsonCommand, account); + ShareAccountTransaction transaction = null; + if (!changes.isEmpty()) { + this.shareAccountRepository.saveAndFlush(account); + transaction = (ShareAccountTransaction) changes.get(ShareAccountApiConstants.additionalshares_paramname); + transaction = account.getShareAccountTransaction(transaction); + if (transaction != null) { + changes.clear(); + changes.put(ShareAccountApiConstants.additionalshares_paramname, transaction.getId()); + Set transactions = new HashSet<>(); + transactions.add(transaction); + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); + } + } + + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult approveShareAccount(Long accountId, JsonCommand jsonCommand) { + + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndApprove(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + final String noteText = jsonCommand.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.shareNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + } + Set transactions = account.getShareAccountTransactions(); + Set journalTransactions = new HashSet<>(); + Long totalSubsribedShares = Long.valueOf(0); + + for (ShareAccountTransaction transaction : transactions) { + if (transaction.isActive() && transaction.isPurchasTransaction()) { + journalTransactions.add(transaction); + totalSubsribedShares += transaction.getTotalShares(); + } + } + ShareProduct shareProduct = account.getShareProduct(); + shareProduct.addSubscribedShares(totalSubsribedShares); + this.shareProductRepository.save(shareProduct); + + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, journalTransactions)); + + businessEventNotifierService.notifyPostBusinessEvent(new ShareAccountApproveBusinessEvent(account)); + + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult rejectShareAccount(Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndReject(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + final String noteText = jsonCommand.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.shareNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + } + Set transactions = account.getShareAccountTransactions(); + Set journalTransactions = new HashSet<>(); + for (ShareAccountTransaction transaction : transactions) { + if (transaction.isActive() && !transaction.isChargeTransaction()) { + journalTransactions.add(transaction); + } + } + + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, journalTransactions)); + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult undoApproveShareAccount(Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndUndoApprove(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + final String noteText = jsonCommand.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.shareNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + } + + Set transactions = account.getShareAccountTransactions(); + ArrayList journalEntryTransactions = new ArrayList<>(); + for (ShareAccountTransaction transaction : transactions) { + if (transaction.isActive() && !transaction.isChargeTransaction()) { + journalEntryTransactions.add(transaction.getId()); + } + } + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + this.journalEntryWritePlatformService.revertShareAccountJournalEntries(journalEntryTransactions, transactionDate); + journalEntryWritePlatformService.createJournalEntriesForShares( + populateJournalEntries(account, account.getPendingForApprovalSharePurchaseTransactions())); + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult activateShareAccount(Long accountId, JsonCommand jsonCommand) { + + try { + // ── Validate: verified cashier collection record must exist ──────── + // Checks m_share_cashier_collection for a record where: + // 1. share_account_id = exactly this account (no cross-account match) + // 2. total_amount > 0 (real cash was received, not a zero entry) + // 3. collection_date within MAX_COLLECTION_AGE_DAYS (not a stale record) + final int maxAgeDays = 30; + final Date oldestValidDate = Date.valueOf(LocalDate.now().minusDays(maxAgeDays)); + + final Map collectionRecord; + try { + collectionRecord = jdbcTemplate.queryForMap( + "SELECT share_account_id, collected_by_name, collection_date, " + + " total_amount, gl_transaction_id " + + "FROM m_share_cashier_collection " + + "WHERE share_account_id = ? " + + " AND total_amount > 0 " + + " AND collection_date >= ?", + accountId, oldestValidDate); + } catch (EmptyResultDataAccessException e) { + throw new org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException( + "error.msg.share.activation.cashier.collection.required", + "Share account " + accountId + " cannot be activated: " + + "no cashier cash-collection record found within the last " + + maxAgeDays + " days. " + + "Please complete the Share Collection step in the Cashier menu first."); + } + + // Query BEFORE activation which charges were already paid via cashier collection. + // Cashier collection sets is_paid_derived = true; activation would otherwise + // post a duplicate Dr Cash → Cr Income entry for those same charges. + final Set cashierPaidChargeIds = new HashSet<>(jdbcTemplate.queryForList( + "SELECT id FROM m_share_account_charge " + + "WHERE account_id = ? AND is_paid_derived = true", + Long.class, accountId)); + + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndActivate(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + } + + // Filter out charge transactions whose charges were already collected + // by the cashier — prevents duplicate GL entries on activation. + Set chargesForGL = account.getChargeTransactions().stream() + .filter(txn -> txn.getChargesPaidBy() == null + || txn.getChargesPaidBy().stream() + .noneMatch(cpb -> cashierPaidChargeIds.contains(cpb.getShareChargeId()))) + .collect(Collectors.toSet()); + + if (!chargesForGL.isEmpty()) { + this.journalEntryWritePlatformService + .createJournalEntriesForShares(populateJournalEntries(account, chargesForGL)); + } + + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @SuppressWarnings("unchecked") + @Override + public CommandProcessingResult approveAdditionalShares(Long accountId, JsonCommand jsonCommand) { + + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndApproveAddtionalShares(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + ArrayList transactionIds = (ArrayList) changes.get(ShareAccountApiConstants.requestedshares_paramname); + Long totalSubscribedShares = Long.valueOf(0); + if (transactionIds != null) { + Set transactions = new HashSet<>(); + for (Long id : transactionIds) { + ShareAccountTransaction transaction = account.retrievePurchasedShares(id); + transactions.add(transaction); + totalSubscribedShares += transaction.getTotalShares(); + } + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); + } + if (!totalSubscribedShares.equals(Long.valueOf(0))) { + ShareProduct shareProduct = account.getShareProduct(); + shareProduct.addSubscribedShares(totalSubscribedShares); + this.shareProductRepository.save(shareProduct); + } + } + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @SuppressWarnings("unchecked") + @Override + public CommandProcessingResult rejectAdditionalShares(Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndRejectAddtionalShares(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.save(account); + ArrayList transactionIds = (ArrayList) changes.get(ShareAccountApiConstants.requestedshares_paramname); + if (transactionIds != null) { + Set transactions = new HashSet<>(); + for (Long id : transactionIds) { + ShareAccountTransaction transaction = account.retrievePurchasedShares(id); + transactions.add(transaction); + } + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); + } + } + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult redeemShares(Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndRedeemShares(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.saveAndFlush(account); + ShareAccountTransaction transaction = (ShareAccountTransaction) changes + .get(ShareAccountApiConstants.requestedshares_paramname); + // after saving, entity will have different object. So need to + // retrieve the entity object + transaction = account.getShareAccountTransaction(transaction); + Long redeemShares = transaction.getTotalShares(); + ShareProduct shareProduct = account.getShareProduct(); + // remove the redeem shares from total subscribed shares + shareProduct.removeSubscribedShares(redeemShares); + this.shareProductRepository.saveAndFlush(shareProduct); + + Set transactions = new HashSet<>(); + transactions.add(transaction); + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); + changes.clear(); + changes.put(ShareAccountApiConstants.requestedshares_paramname, transaction.getId()); + + } + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult closeShareAccount(Long accountId, JsonCommand jsonCommand) { + try { + ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); + Map changes = this.accountDataSerializer.validateAndClose(jsonCommand, account); + if (!changes.isEmpty()) { + this.shareAccountRepository.saveAndFlush(account); + final String noteText = jsonCommand.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.shareNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + ShareAccountTransaction transaction = (ShareAccountTransaction) changes + .get(ShareAccountApiConstants.requestedshares_paramname); + transaction = account.getShareAccountTransaction(transaction); + Set transactions = new HashSet<>(); + transactions.add(transaction); + this.journalEntryWritePlatformService.createJournalEntriesForShares(populateJournalEntries(account, transactions)); + changes.clear(); + changes.put(ShareAccountApiConstants.requestedshares_paramname, transaction.getId()); + + } + return new CommandProcessingResultBuilder() // + .withCommandId(jsonCommand.commandId()) // + .withEntityId(accountId) // + .with(changes) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve.getMostSpecificCause(), dve); + return CommandProcessingResult.empty(); + } + } + + private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) { + throw ErrorHandler.getMappable(dve, "error.msg.shareaccount.unknown.data.integrity.issue", + "Unknown data integrity issue with resource."); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java index 6daf95834..13698df7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java @@ -1,123 +1,124 @@ -/** - * 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.start; - -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.PaginationHelper; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; -import org.apache.fineract.portfolio.accounts.constants.AccountsApiConstants; -import org.apache.fineract.portfolio.accounts.service.AccountsCommandsService; -import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; -import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; -import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository; -import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; -import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; -import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService; -import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformService; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularServiceImpl; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformService; -import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformServiceJpaRepositoryImpl; -import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; -import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; - -@Configuration -public class ShareAccountsConfiguration { - - @Bean - @ConditionalOnMissingBean(PurchasedSharesReadPlatformService.class) - public PurchasedSharesReadPlatformService purchasedSharesReadPlatformService(JdbcTemplate jdbcTemplate) { - return new PurchasedSharesReadPlatformServiceImpl(jdbcTemplate); - } - - @Bean - @ConditionalOnMissingBean(ShareAccountChargeReadPlatformService.class) - public ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService(JdbcTemplate jdbcTemplate) { - return new ShareAccountChargeReadPlatformServiceImpl(jdbcTemplate); - } - - @Bean(value = "SHAREACCOUNT_COMMANDSERVICE") - @ConditionalOnMissingBean(AccountsCommandsService.class) - public AccountsCommandsService accountsCommandsService(FromJsonHelper fromApiJsonHelper, - ShareAccountDataSerializer shareAccountDataSerializer) { - return new ShareAccountCommandsServiceImpl(fromApiJsonHelper, shareAccountDataSerializer); - } - - @Bean - @ConditionalOnMissingBean(ShareAccountDividendReadPlatformService.class) - public ShareAccountDividendReadPlatformService shareAccountDividendReadPlatformService(JdbcTemplate jdbcTemplate, - ColumnValidator columnValidator, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) { - return new ShareAccountDividendReadPlatformServiceImpl(jdbcTemplate, columnValidator, paginationHelper, sqlGenerator); - - } - - @Bean(value = "share" + AccountsApiConstants.READPLATFORM_NAME) - @ConditionalOnMissingBean(ShareAccountReadPlatformService.class) - public ShareAccountReadPlatformService shareAccountReadPlatformService(ApplicationContext applicationContext, - ChargeReadPlatformService chargeReadPlatformService, - ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService, - SavingsAccountReadPlatformService savingsAccountReadPlatformService, ClientReadPlatformService clientReadPlatformService, - ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService, - PurchasedSharesReadPlatformService purchasedSharesReadPlatformService, JdbcTemplate jdbcTemplate, - PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) { - return new ShareAccountReadPlatformServiceImpl(applicationContext, chargeReadPlatformService, - shareProductDropdownReadPlatformService, savingsAccountReadPlatformService, clientReadPlatformService, - shareAccountChargeReadPlatformService, purchasedSharesReadPlatformService, jdbcTemplate, paginationHelper, sqlGenerator); - } - - @Bean - @ConditionalOnMissingBean(ShareAccountSchedularService.class) - public ShareAccountSchedularService shareAccountSchedularService(ShareAccountDividendRepository shareAccountDividendRepository, - SavingsAccountDomainService savingsAccountDomainService, SavingsAccountAssembler savingsAccountAssembler) { - return new ShareAccountSchedularServiceImpl(shareAccountDividendRepository, savingsAccountDomainService, savingsAccountAssembler); - } - - @Bean - @ConditionalOnMissingBean(ShareAccountWritePlatformService.class) - public ShareAccountWritePlatformService shareAccountWritePlatformService(ShareAccountDataSerializer accountDataSerializer, - ShareAccountRepositoryWrapper shareAccountRepository, ShareProductRepositoryWrapper shareProductRepository, - AccountNumberGenerator accountNumberGenerator, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - JournalEntryWritePlatformService journalEntryWritePlatformService, NoteRepository noteRepository, - BusinessEventNotifierService businessEventNotifierService) { - return new ShareAccountWritePlatformServiceJpaRepositoryImpl(accountDataSerializer, shareAccountRepository, shareProductRepository, - accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository, - businessEventNotifierService); - } -} +/** + * 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.start; + +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.PaginationHelper; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.utils.ColumnValidator; +import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; +import org.apache.fineract.portfolio.accounts.constants.AccountsApiConstants; +import org.apache.fineract.portfolio.accounts.service.AccountsCommandsService; +import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; +import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; +import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository; +import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; +import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; +import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularServiceImpl; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformService; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformServiceJpaRepositoryImpl; +import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; +import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class ShareAccountsConfiguration { + + @Bean + @ConditionalOnMissingBean(PurchasedSharesReadPlatformService.class) + public PurchasedSharesReadPlatformService purchasedSharesReadPlatformService(JdbcTemplate jdbcTemplate) { + return new PurchasedSharesReadPlatformServiceImpl(jdbcTemplate); + } + + @Bean + @ConditionalOnMissingBean(ShareAccountChargeReadPlatformService.class) + public ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService(JdbcTemplate jdbcTemplate) { + return new ShareAccountChargeReadPlatformServiceImpl(jdbcTemplate); + } + + @Bean(value = "SHAREACCOUNT_COMMANDSERVICE") + @ConditionalOnMissingBean(AccountsCommandsService.class) + public AccountsCommandsService accountsCommandsService(FromJsonHelper fromApiJsonHelper, + ShareAccountDataSerializer shareAccountDataSerializer) { + return new ShareAccountCommandsServiceImpl(fromApiJsonHelper, shareAccountDataSerializer); + } + + @Bean + @ConditionalOnMissingBean(ShareAccountDividendReadPlatformService.class) + public ShareAccountDividendReadPlatformService shareAccountDividendReadPlatformService(JdbcTemplate jdbcTemplate, + ColumnValidator columnValidator, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) { + return new ShareAccountDividendReadPlatformServiceImpl(jdbcTemplate, columnValidator, paginationHelper, sqlGenerator); + + } + + @Bean(value = "share" + AccountsApiConstants.READPLATFORM_NAME) + @ConditionalOnMissingBean(ShareAccountReadPlatformService.class) + public ShareAccountReadPlatformService shareAccountReadPlatformService(ApplicationContext applicationContext, + ChargeReadPlatformService chargeReadPlatformService, + ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService, + SavingsAccountReadPlatformService savingsAccountReadPlatformService, ClientReadPlatformService clientReadPlatformService, + ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService, + PurchasedSharesReadPlatformService purchasedSharesReadPlatformService, JdbcTemplate jdbcTemplate, + PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) { + return new ShareAccountReadPlatformServiceImpl(applicationContext, chargeReadPlatformService, + shareProductDropdownReadPlatformService, savingsAccountReadPlatformService, clientReadPlatformService, + shareAccountChargeReadPlatformService, purchasedSharesReadPlatformService, jdbcTemplate, paginationHelper, sqlGenerator); + } + + @Bean + @ConditionalOnMissingBean(ShareAccountSchedularService.class) + public ShareAccountSchedularService shareAccountSchedularService(ShareAccountDividendRepository shareAccountDividendRepository, + SavingsAccountDomainService savingsAccountDomainService, SavingsAccountAssembler savingsAccountAssembler) { + return new ShareAccountSchedularServiceImpl(shareAccountDividendRepository, savingsAccountDomainService, savingsAccountAssembler); + } + + @Bean + @ConditionalOnMissingBean(ShareAccountWritePlatformService.class) + public ShareAccountWritePlatformService shareAccountWritePlatformService(ShareAccountDataSerializer accountDataSerializer, + ShareAccountRepositoryWrapper shareAccountRepository, ShareProductRepositoryWrapper shareProductRepository, + AccountNumberGenerator accountNumberGenerator, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, + JournalEntryWritePlatformService journalEntryWritePlatformService, NoteRepository noteRepository, + org.springframework.jdbc.core.JdbcTemplate jdbcTemplate, + BusinessEventNotifierService businessEventNotifierService) { + return new ShareAccountWritePlatformServiceJpaRepositoryImpl(accountDataSerializer, shareAccountRepository, shareProductRepository, + accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository, + jdbcTemplate, businessEventNotifierService); + } +}