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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6e31ac9acd
commit
9d944d0196
+424
@@ -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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> resp = new LinkedHashMap<>();
|
||||||
|
resp.put("collected", false);
|
||||||
|
resp.put("shareAccountId", shareAccountId);
|
||||||
|
return Response.ok(gson.toJson(resp)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// Private helpers
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private Map<String, Object> 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<String, Object> glEntryMap(final String account, final String type,
|
||||||
|
final BigDecimal amount, final String currency) {
|
||||||
|
final Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+563
-506
File diff suppressed because it is too large
Load Diff
+124
-123
@@ -1,123 +1,124 @@
|
|||||||
/**
|
/**
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
* or more contributor license agreements. See the NOTICE file
|
* or more contributor license agreements. See the NOTICE file
|
||||||
* distributed with this work for additional information
|
* distributed with this work for additional information
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
* to you under the Apache License, Version 2.0 (the
|
* to you under the Apache License, Version 2.0 (the
|
||||||
* "License"); you may not use this file except in compliance
|
* "License"); you may not use this file except in compliance
|
||||||
* with the License. You may obtain a copy of the License at
|
* with the License. You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing,
|
* Unless required by applicable law or agreed to in writing,
|
||||||
* software distributed under the License is distributed on an
|
* software distributed under the License is distributed on an
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
* KIND, either express or implied. See the License for the
|
* KIND, either express or implied. See the License for the
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.shareaccounts.start;
|
package org.apache.fineract.portfolio.shareaccounts.start;
|
||||||
|
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
|
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||||
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
|
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
|
||||||
import org.apache.fineract.portfolio.accounts.constants.AccountsApiConstants;
|
import org.apache.fineract.portfolio.accounts.constants.AccountsApiConstants;
|
||||||
import org.apache.fineract.portfolio.accounts.service.AccountsCommandsService;
|
import org.apache.fineract.portfolio.accounts.service.AccountsCommandsService;
|
||||||
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository;
|
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper;
|
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer;
|
import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService;
|
import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformService;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularServiceImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularServiceImpl;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformService;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformServiceJpaRepositoryImpl;
|
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountWritePlatformServiceJpaRepositoryImpl;
|
||||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
|
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService;
|
import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class ShareAccountsConfiguration {
|
public class ShareAccountsConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(PurchasedSharesReadPlatformService.class)
|
@ConditionalOnMissingBean(PurchasedSharesReadPlatformService.class)
|
||||||
public PurchasedSharesReadPlatformService purchasedSharesReadPlatformService(JdbcTemplate jdbcTemplate) {
|
public PurchasedSharesReadPlatformService purchasedSharesReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||||
return new PurchasedSharesReadPlatformServiceImpl(jdbcTemplate);
|
return new PurchasedSharesReadPlatformServiceImpl(jdbcTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(ShareAccountChargeReadPlatformService.class)
|
@ConditionalOnMissingBean(ShareAccountChargeReadPlatformService.class)
|
||||||
public ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService(JdbcTemplate jdbcTemplate) {
|
public ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||||
return new ShareAccountChargeReadPlatformServiceImpl(jdbcTemplate);
|
return new ShareAccountChargeReadPlatformServiceImpl(jdbcTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(value = "SHAREACCOUNT_COMMANDSERVICE")
|
@Bean(value = "SHAREACCOUNT_COMMANDSERVICE")
|
||||||
@ConditionalOnMissingBean(AccountsCommandsService.class)
|
@ConditionalOnMissingBean(AccountsCommandsService.class)
|
||||||
public AccountsCommandsService accountsCommandsService(FromJsonHelper fromApiJsonHelper,
|
public AccountsCommandsService accountsCommandsService(FromJsonHelper fromApiJsonHelper,
|
||||||
ShareAccountDataSerializer shareAccountDataSerializer) {
|
ShareAccountDataSerializer shareAccountDataSerializer) {
|
||||||
return new ShareAccountCommandsServiceImpl(fromApiJsonHelper, shareAccountDataSerializer);
|
return new ShareAccountCommandsServiceImpl(fromApiJsonHelper, shareAccountDataSerializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(ShareAccountDividendReadPlatformService.class)
|
@ConditionalOnMissingBean(ShareAccountDividendReadPlatformService.class)
|
||||||
public ShareAccountDividendReadPlatformService shareAccountDividendReadPlatformService(JdbcTemplate jdbcTemplate,
|
public ShareAccountDividendReadPlatformService shareAccountDividendReadPlatformService(JdbcTemplate jdbcTemplate,
|
||||||
ColumnValidator columnValidator, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) {
|
ColumnValidator columnValidator, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
return new ShareAccountDividendReadPlatformServiceImpl(jdbcTemplate, columnValidator, paginationHelper, sqlGenerator);
|
return new ShareAccountDividendReadPlatformServiceImpl(jdbcTemplate, columnValidator, paginationHelper, sqlGenerator);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(value = "share" + AccountsApiConstants.READPLATFORM_NAME)
|
@Bean(value = "share" + AccountsApiConstants.READPLATFORM_NAME)
|
||||||
@ConditionalOnMissingBean(ShareAccountReadPlatformService.class)
|
@ConditionalOnMissingBean(ShareAccountReadPlatformService.class)
|
||||||
public ShareAccountReadPlatformService shareAccountReadPlatformService(ApplicationContext applicationContext,
|
public ShareAccountReadPlatformService shareAccountReadPlatformService(ApplicationContext applicationContext,
|
||||||
ChargeReadPlatformService chargeReadPlatformService,
|
ChargeReadPlatformService chargeReadPlatformService,
|
||||||
ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService,
|
ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService,
|
||||||
SavingsAccountReadPlatformService savingsAccountReadPlatformService, ClientReadPlatformService clientReadPlatformService,
|
SavingsAccountReadPlatformService savingsAccountReadPlatformService, ClientReadPlatformService clientReadPlatformService,
|
||||||
ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService,
|
ShareAccountChargeReadPlatformService shareAccountChargeReadPlatformService,
|
||||||
PurchasedSharesReadPlatformService purchasedSharesReadPlatformService, JdbcTemplate jdbcTemplate,
|
PurchasedSharesReadPlatformService purchasedSharesReadPlatformService, JdbcTemplate jdbcTemplate,
|
||||||
PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) {
|
PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
return new ShareAccountReadPlatformServiceImpl(applicationContext, chargeReadPlatformService,
|
return new ShareAccountReadPlatformServiceImpl(applicationContext, chargeReadPlatformService,
|
||||||
shareProductDropdownReadPlatformService, savingsAccountReadPlatformService, clientReadPlatformService,
|
shareProductDropdownReadPlatformService, savingsAccountReadPlatformService, clientReadPlatformService,
|
||||||
shareAccountChargeReadPlatformService, purchasedSharesReadPlatformService, jdbcTemplate, paginationHelper, sqlGenerator);
|
shareAccountChargeReadPlatformService, purchasedSharesReadPlatformService, jdbcTemplate, paginationHelper, sqlGenerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(ShareAccountSchedularService.class)
|
@ConditionalOnMissingBean(ShareAccountSchedularService.class)
|
||||||
public ShareAccountSchedularService shareAccountSchedularService(ShareAccountDividendRepository shareAccountDividendRepository,
|
public ShareAccountSchedularService shareAccountSchedularService(ShareAccountDividendRepository shareAccountDividendRepository,
|
||||||
SavingsAccountDomainService savingsAccountDomainService, SavingsAccountAssembler savingsAccountAssembler) {
|
SavingsAccountDomainService savingsAccountDomainService, SavingsAccountAssembler savingsAccountAssembler) {
|
||||||
return new ShareAccountSchedularServiceImpl(shareAccountDividendRepository, savingsAccountDomainService, savingsAccountAssembler);
|
return new ShareAccountSchedularServiceImpl(shareAccountDividendRepository, savingsAccountDomainService, savingsAccountAssembler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(ShareAccountWritePlatformService.class)
|
@ConditionalOnMissingBean(ShareAccountWritePlatformService.class)
|
||||||
public ShareAccountWritePlatformService shareAccountWritePlatformService(ShareAccountDataSerializer accountDataSerializer,
|
public ShareAccountWritePlatformService shareAccountWritePlatformService(ShareAccountDataSerializer accountDataSerializer,
|
||||||
ShareAccountRepositoryWrapper shareAccountRepository, ShareProductRepositoryWrapper shareProductRepository,
|
ShareAccountRepositoryWrapper shareAccountRepository, ShareProductRepositoryWrapper shareProductRepository,
|
||||||
AccountNumberGenerator accountNumberGenerator, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
|
AccountNumberGenerator accountNumberGenerator, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
|
||||||
JournalEntryWritePlatformService journalEntryWritePlatformService, NoteRepository noteRepository,
|
JournalEntryWritePlatformService journalEntryWritePlatformService, NoteRepository noteRepository,
|
||||||
BusinessEventNotifierService businessEventNotifierService) {
|
org.springframework.jdbc.core.JdbcTemplate jdbcTemplate,
|
||||||
return new ShareAccountWritePlatformServiceJpaRepositoryImpl(accountDataSerializer, shareAccountRepository, shareProductRepository,
|
BusinessEventNotifierService businessEventNotifierService) {
|
||||||
accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository,
|
return new ShareAccountWritePlatformServiceJpaRepositoryImpl(accountDataSerializer, shareAccountRepository, shareProductRepository,
|
||||||
businessEventNotifierService);
|
accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository,
|
||||||
}
|
jdbcTemplate, businessEventNotifierService);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user