package com.mfsys.uco.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.mfsys.comm.util.MapValueExtractorUtil; import com.mfsys.uco.UCOURI; import com.mfsys.uco.dto.*; import com.mfsys.uco.dto.Transaction.TransactionOtpRequestModel; import com.mfsys.uco.exception.*; import com.mfsys.uco.model.*; import com.mfsys.uco.repository.CustomerProfileRepository; import com.mfsys.uco.repository.GLAccountMasterTransactionTrailRepository; import com.mfsys.uco.repository.PinRepository; import com.mfsys.uco.repository.TransactionTrailRepository; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.Currency; import java.util.*; @Service @Data @RequiredArgsConstructor public class TransactionService { private final CustomerProfileRepository customerProfileRepository; private final NotificationService notificationService; private final TransactionPinService transactionPinService; private final TransactionTrailRepository transactionTrailRepository; private final PinRepository pinRepository; private final UcoAccountService ucoAccountService; private final WebClientDepositService webClientDepositService; private final GLAccountMasterTransactionTrailRepository glAccountMasterTransactionTrailRepository; public TransactionPinResponseModel sendOtpAndValidateTranPin(TransactionOtpRequestModel transactionOtpRequestModel, boolean isResendOtp) { CustomerProfile customerProfile = verifyOldPinAndGetCmpProfile(transactionOtpRequestModel.getPorOrgacode(), transactionOtpRequestModel.getTransPincode(), transactionOtpRequestModel.getCmpCustcode(), isResendOtp); Long notificationId = transactionPinService.sendOtp(customerProfile, transactionOtpRequestModel.getChannelCode(), transactionOtpRequestModel.getPinType(), "Transaction Verification OTP", transactionOtpRequestModel.isOtpRequired()); Pin pin = pinRepository.findLatestActiveOtpByUserName(transactionOtpRequestModel.getEmail(), transactionOtpRequestModel.getPinType()) .orElseThrow(()->new TranVerificationPinException()); return TransactionPinResponseModel.builder() .notificationId(notificationId) .obpPincode(pin.getPincode()) .pinCreation(pin.getPinCreatedate()) .pinExpiry(pin.getPinExpirydate()) .build(); } public Map cashInTransaction(CashInTransactionRequest transactionRequest) { if(transactionRequest.getSgtGntramtfc()<=0){ throw new InvalidTransactionAmountException(); } validation(transactionRequest); TransactionTrail transactionTrail = TransactionTrail.builder() .porOrgacode(transactionRequest.getPorOrgacode()) .drMbmBkmsnumber(transactionRequest.getDrMbmBkmsnumber()) .crMbmBkmsnumber(transactionRequest.getCrMbmBkmsnumber()) .dmpProdcode(transactionRequest.getDmpProdCode()) .drmbmBkmstitle(transactionRequest.getDrMbmBkmstitle()) .drpcrCurrdesc(transactionRequest.getDrPcrCurrdesc()) .drPcrCurrshort(transactionRequest.getDrPcrCurrshort()) .cmpCustcode(transactionRequest.getCmpCustcode()) .drPcrCurrcode(transactionRequest.getDrPcrCurrcode()) .crMbmBkmstitle(transactionRequest.getCrMbmBkmstitle()) .drSgtGntramt(BigDecimal.valueOf(convertToPKR(transactionRequest.getDrPcrCurrcode(),transactionRequest.getSgtGntramtfc(), transactionRequest.getPorOrgacode()))) .crSgtGntramt(null) .crPcrCurrdesc(transactionRequest.getCrPcrCurrdesc()) .crPcrCurrcode(transactionRequest.getCrPcrCurrcode()) .crPcrCurrshort(transactionRequest.getCrPcrCurrshort()) .sgtSentGntrnumber(null) .drSgtGntrdate(LocalDate.now()) .sgtGntramt(BigDecimal.valueOf(transactionRequest.getSgtGntramtfc())) .batAcnttranSend(false) .batAcnttranReceived(false) .sgtReceiveGntrnumber(null) .build(); CoreCashInTransaction coreCashInTransaction = CoreCashInTransaction.builder() .drMbmBkmsnumber(transactionRequest.getDrMbmBkmsnumber()) .drPcrCurrcode(transactionTrail.getDrPcrCurrcode()) .accSgtGntramtfc(transactionRequest.getSgtGntramtfc()) .sgtGntramtfc(transactionTrail.getCrSgtGntramt()) .otdTrancomment(transactionRequest.getOtdTrancomment()) .porOrgacode(transactionRequest.getPorOrgacode()) .pcaGlaccode("A01011003") .build(); Map response = (Map) webClientDepositService.postTransaction(coreCashInTransaction, UCOURI.BANKING_CASH_IN, transactionRequest.getPorOrgacode()); transactionTrail.setSgtSentGntrnumber(String.valueOf(response.get("tran_id"))); transactionTrail.setSgtSentNodeId(String.valueOf(response.get("node_id"))); transactionTrail.setBatAcnttranSend(true); if(Objects.nonNull(transactionRequest.getCmpRefcode())) { transactionTrail.setCmpRefcode(transactionTrail.getSgtSentGntrnumber() + transactionRequest.getCmpRefcode()); } transactionTrail.setDrMbmBkmsbalance(MapValueExtractorUtil.getValueAsBigDecimal(response, "mbmBkmsbalance")); transactionTrailRepository.save(transactionTrail); return response; } private void validation(CashInTransactionRequest transactionRequest) { if (transactionRequest.getCrMbmBkmsnumber().equals(transactionRequest.getDrMbmBkmsnumber())) { throw new SameCrDrAccountExistsException(); } notificationService.verifyOtpViaOtpId(transactionRequest.getNotificationId(), transactionRequest.getPinType(), transactionRequest.getObpPincode()); } public List fetchPendingCrTransactions(String porOrgacode, String mbmBkmsnumber) { List transactionTrails = findPendingCrTransactions(porOrgacode,mbmBkmsnumber); if(!transactionTrails.isEmpty()){ transactionTrails.stream().forEach(t -> { LocalDate datePlus14Days = t.getDrSgtGntrdate().plusDays(14); t.setExpSgtGntrdate((datePlus14Days.minusDays(ChronoUnit.DAYS.between(LocalDate.now(),datePlus14Days)))); t.setDaysToExpire(Math.toIntExact(ChronoUnit.DAYS.between(LocalDate.now(), datePlus14Days))); }); } return transactionTrails; } public List findPendingCrTransactions(String porOrgacode , String mbmBkmsnumber) { Optional> optionalTransactions = transactionTrailRepository.findByPorOrgacodeAndCrMbmBkmsnumberAndBatAcnttranReceivedFalse(porOrgacode, mbmBkmsnumber); return optionalTransactions.orElseGet(Collections::emptyList); } public List fetchDepositAccountStatement(String porOrgacode, String mbmBkmsnumber) { Optional> optionalTransactions = transactionTrailRepository.fetchDepositAccountStatement(mbmBkmsnumber); return optionalTransactions.orElseGet(Collections::emptyList); } public Map cashOutTransaction(CashOutTransactionRequest cashOutTransactionRequest) { verifyOldPinAndGetCmpProfile(cashOutTransactionRequest.getPorOrgacode(), cashOutTransactionRequest.getCmpTranpin(), cashOutTransactionRequest.getCmpCustcode(), true); Map response = new HashMap<>(); Optional transactionTrail = transactionTrailRepository.findById(Math.toIntExact(cashOutTransactionRequest.getId())); if (transactionTrail.isPresent()) { if(Objects.nonNull(cashOutTransactionRequest.getCmpRefcode())) { if (!(transactionTrail.get().getSgtSentGntrnumber() + cashOutTransactionRequest.getCmpRefcode()).equals(transactionTrail.get().getCmpRefcode())) { throw new ReferenceNumberNotValidException(); } } transactionTrail.get().setCrSgtGntramt(BigDecimal.valueOf(convertFromPKR(transactionTrail.get().getCrPcrCurrcode(),Double.valueOf(String.valueOf(transactionTrail.get().getDrSgtGntramt())),transactionTrail.get().getPorOrgacode())));; CoreCashOutTransaction cashOutTransaction = CoreCashOutTransaction.builder() .crPcrCurrcode(transactionTrail.get().getCrPcrCurrcode()) .crMbmBkmsnumber(transactionTrail.get().getCrMbmBkmsnumber()) .porOrgacode(cashOutTransactionRequest.getPorOrgacode()) .otdTrancomment(cashOutTransactionRequest.getId() + "_Received") .sgtGntramtfc(transactionTrail.get().getDrSgtGntramt()) .accSgtGntramtfc(transactionTrail.get().getCrSgtGntramt()) .pcaGlaccode("A01011003") .build(); response = (Map) webClientDepositService.postTransaction(cashOutTransaction, UCOURI.BANKING_CASH_OUT, transactionTrail.get().getPorOrgacode()); transactionTrail.get().setSgtReceiveGntrnumber((String.valueOf(response.get("tran_id")))); transactionTrail.get().setSgtSentNodeId(String.valueOf(response.get("node_id"))); transactionTrail.get().setBatAcnttranReceived(true); transactionTrail.get().setCrMbmBkmsbalance(MapValueExtractorUtil.getValueAsBigDecimal(response, "mbmBkmsbalance")); transactionTrailRepository.save(transactionTrail.get()); } return response; } private CustomerProfile verifyOldPinAndGetCmpProfile(String porOrgacode, String transPincode, String cmpCustcode, boolean isResendOtp) { CustomerProfile customerProfile = transactionPinService.fetchCustomer(porOrgacode, cmpCustcode); if (isResendOtp) { transactionPinService.validateOldPin(customerProfile, transPincode); } return customerProfile; } private CustomerProfile fetchCustomerBasedOnCmpCustcode(String porOrgacode, String cmpCustcode) { return customerProfileRepository.findById(new CustomerProfileId(porOrgacode, cmpCustcode)) .orElseThrow(() -> new IllegalArgumentException("Customer profile not found for ID: " + cmpCustcode)); } private String extractTranNumber(List args) { if (args != null && args.size() > 0) { return String.valueOf(args.get(0)).replace("[", "").replace("]", ""); } throw new RuntimeException("may day"); } public EvaluatedCurrencyReponse getEvaluatedCurrency(String porOrgacode, String baseCurrencyCode, String targetCurrencyCode, double sgtGntramtfc) { List exchangeRateModelList = fetchExchangeRate(porOrgacode); if (exchangeRateModelList == null || exchangeRateModelList.isEmpty()) { throw new RuntimeException("No exchange rates configured for organization " + porOrgacode + ". Please configure exchange rates first."); } String normalizedBase = normalizeCurrencyCode(baseCurrencyCode, exchangeRateModelList); String normalizedTarget = normalizeCurrencyCode(targetCurrencyCode, exchangeRateModelList); double pkrAmt = convertToPKR(normalizedBase, sgtGntramtfc, porOrgacode, exchangeRateModelList); double convertFromPkr = convertFromPKR(normalizedTarget, pkrAmt, porOrgacode, exchangeRateModelList); Optional rate = exchangeRateModelList.stream() .filter(x -> x.isPcrCurrbase() && currencyCodeMatches(x.getPcrCurrcode(), normalizedTarget)) .map(x -> { BigDecimal bd = BigDecimal.valueOf(1 / x.getPerEratrateact()); bd = bd.setScale(2, RoundingMode.HALF_UP); return bd.doubleValue(); }) .findFirst(); if (!rate.isPresent()) { rate = exchangeRateModelList.stream() .filter(x -> !x.isPcrCurrbase() && currencyCodeMatches(x.getPcrCurrcode(), normalizedTarget)) .map(x -> { BigDecimal bd = BigDecimal.valueOf(x.getPerEratrateact()); bd = bd.setScale(2, RoundingMode.HALF_UP); return bd.doubleValue(); }) .findFirst(); } double targetPerEratrateact = rate.orElse(0.0); return EvaluatedCurrencyReponse.builder() .targetPerEratrateact(targetPerEratrateact) .serviceCharges(0.0) .pcrCurrcode(targetCurrencyCode) .sgtGntramtfc(BigDecimal.valueOf(convertFromPkr).setScale(2, RoundingMode.HALF_UP)) .build(); } public double convertToPKR(String fromcrCurrcode, double amount, String porOrgacode) { List exchangeRateModelList = fetchExchangeRate(porOrgacode); return convertToPKR(fromcrCurrcode, amount, porOrgacode, exchangeRateModelList); } private double convertToPKR(String fromcrCurrcode, double amount, String porOrgacode, List exchangeRateModelList) { if (exchangeRateModelList == null || exchangeRateModelList.isEmpty()) { throw new RuntimeException("No exchange rates configured for organization " + porOrgacode + ". Please configure exchange rates first."); } String baseCurrencyCode = exchangeRateModelList.stream() .filter(ExchangeRateModel::isPcrCurrbase) .findFirst() .map(ExchangeRateModel::getPcrCurrcode) .orElse(null); if (currencyCodeMatches(baseCurrencyCode, fromcrCurrcode)) { return amount; } return exchangeRateModelList.stream() .filter(k -> currencyCodeMatches(k.getPcrCurrcode(), fromcrCurrcode)) .findFirst() .map(k -> amount * k.getPerEratrateact()) .orElseThrow(() -> new RuntimeException( "Exchange rate not found for currency " + fromcrCurrcode + " (organization: " + porOrgacode + "). Available: " + formatAvailableCurrencies(exchangeRateModelList) )); } public List fetchExchangeRate(String porOrgacode) { ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.convertValue(ucoAccountService.fetchExchangeRate(porOrgacode), new TypeReference>() { }); } public double convertFromPKR(String todrCurrcode, double amount, String porOrgacode) { List exchangeRateModelList = fetchExchangeRate(porOrgacode); return convertFromPKR(todrCurrcode, amount, porOrgacode, exchangeRateModelList); } private double convertFromPKR(String todrCurrcode, double amount, String porOrgacode, List exchangeRateModelList) { if (exchangeRateModelList == null || exchangeRateModelList.isEmpty()) { throw new RuntimeException("No exchange rates configured for organization " + porOrgacode + ". Please configure exchange rates first."); } String baseCurrencyCode = exchangeRateModelList.stream() .filter(ExchangeRateModel::isPcrCurrbase) .findFirst() .map(ExchangeRateModel::getPcrCurrcode) .orElse(null); if (currencyCodeMatches(baseCurrencyCode, todrCurrcode)) { return amount; } return exchangeRateModelList.stream() .filter(k -> currencyCodeMatches(k.getPcrCurrcode(), todrCurrcode)) .findFirst() .map(k -> amount / k.getPerEratrateact()) .orElseThrow(() -> new RuntimeException( "Exchange rate not found for currency " + todrCurrcode + " (organization: " + porOrgacode + "). Available: " + formatAvailableCurrencies(exchangeRateModelList) )); } private boolean currencyCodeMatches(String storedCode, String requestedCode) { if (requestedCode == null) return storedCode == null; if (storedCode == null) return false; return storedCode.trim().equals(requestedCode.trim()); } private String normalizeCurrencyCode(String currencyCode, List exchangeRates) { if (currencyCode == null) return null; String raw = currencyCode.trim(); if (raw.isEmpty()) return raw; String upper = raw.toUpperCase(Locale.ROOT); if (exchangeRates != null && exchangeRates.stream().anyMatch(r -> upper.equals(safeTrimUpper(r.getPcrCurrcode())))) { return upper; } if (exchangeRates != null) { Optional byShort = exchangeRates.stream() .filter(r -> upper.equals(safeTrimUpper(r.getPcrCurrshort()))) .map(ExchangeRateModel::getPcrCurrcode) .filter(Objects::nonNull) .findFirst(); if (byShort.isPresent()) { return byShort.get(); } } if (upper.length() == 3 && upper.chars().allMatch(Character::isLetter)) { try { int numeric = Currency.getInstance(upper).getNumericCode(); String numericStr = String.valueOf(numeric); if (exchangeRates != null && exchangeRates.stream().anyMatch(r -> numericStr.equals(safeTrimUpper(r.getPcrCurrcode())))) { return numericStr; } } catch (Exception ignored) { } } if (upper.chars().allMatch(Character::isDigit)) { try { int numeric = Integer.parseInt(upper); for (Currency c : Currency.getAvailableCurrencies()) { if (c.getNumericCode() == numeric) { String alpha = c.getCurrencyCode(); if (exchangeRates != null && exchangeRates.stream().anyMatch(r -> alpha.equals(safeTrimUpper(r.getPcrCurrcode())))) { return alpha; } break; } } } catch (Exception ignored) { } } return upper; } private String safeTrimUpper(String s) { return s == null ? null : s.trim().toUpperCase(Locale.ROOT); } private String formatAvailableCurrencies(List exchangeRates) { if (exchangeRates == null || exchangeRates.isEmpty()) return "[]"; return exchangeRates.stream() .map(r -> { String shortCode = (r.getPcrCurrshort() == null || r.getPcrCurrshort().isBlank()) ? "?" : r.getPcrCurrshort().trim(); String code = (r.getPcrCurrcode() == null || r.getPcrCurrcode().isBlank()) ? "?" : r.getPcrCurrcode().trim(); return shortCode + "(" + code + ")"; }) .distinct() .limit(20) .toList() .toString(); } public Map glAccountTransaction(GLAccontTranasctionRequestModel glAccontTranasctionRequestModel) { Map response = new HashMap<>(); if(glAccontTranasctionRequestModel.getSgtGntramtfc()<=0){ throw new InvalidTransactionAmountException(); } notificationService.verifyOtpViaOtpId(glAccontTranasctionRequestModel.getNotificationId(),glAccontTranasctionRequestModel.getPinType(),glAccontTranasctionRequestModel.getObpPincode()); CoreCashOutTransaction cashOutTransaction = CoreCashOutTransaction.builder() .pcaGlaccode(glAccontTranasctionRequestModel.getDrPcaGlaccode()) .crMbmBkmsnumber(glAccontTranasctionRequestModel.getCrMbmBkmsnumber()) .otdTrancomment(glAccontTranasctionRequestModel.getOtdTrancomment()) .porOrgacode(glAccontTranasctionRequestModel.getPorOrgacode()) .accSgtGntramtfc(BigDecimal.valueOf(glAccontTranasctionRequestModel.getSgtGntramtfc())) .sgtGntramtfc(BigDecimal.valueOf(convertToPKR(glAccontTranasctionRequestModel.getCrPcrCurrcode(),glAccontTranasctionRequestModel.getSgtGntramtfc(),glAccontTranasctionRequestModel.getPorOrgacode()))) .crPcrCurrcode(glAccontTranasctionRequestModel.getCrPcrCurrcode()) .plcLocacode(glAccontTranasctionRequestModel.getPlcLocacode()) .dmpProdcode(glAccontTranasctionRequestModel.getDmpProdCode()) .build(); response = (Map) webClientDepositService.postTransaction(cashOutTransaction, UCOURI.BANKING_CASH_OUT, glAccontTranasctionRequestModel.getPorOrgacode()); TransactionTrail transactionTrail = TransactionTrail.builder() .porOrgacode(glAccontTranasctionRequestModel.getPorOrgacode()) .drMbmBkmsnumber(glAccontTranasctionRequestModel.getDrPcaGlaccode()) .crMbmBkmsnumber(glAccontTranasctionRequestModel.getCrMbmBkmsnumber()) .dmpProdcode(glAccontTranasctionRequestModel.getDmpProdCode()) .drmbmBkmstitle(glAccontTranasctionRequestModel.getDrPcaGlacdesc()) .drpcrCurrdesc(glAccontTranasctionRequestModel.getCrPcrCurrdesc()) .drPcrCurrshort(glAccontTranasctionRequestModel.getCrPcrCurrshort()) .cmpCustcode(glAccontTranasctionRequestModel.getCmpCustcode()) .drPcrCurrcode(glAccontTranasctionRequestModel.getCrPcrCurrcode()) .crMbmBkmstitle(glAccontTranasctionRequestModel.getCrMbmBkmstitle()) .drSgtGntramt(BigDecimal.valueOf(glAccontTranasctionRequestModel.getSgtGntramtfc())) .crSgtGntramt(null) .crPcrCurrdesc(glAccontTranasctionRequestModel.getCrPcrCurrdesc()) .crPcrCurrcode(glAccontTranasctionRequestModel.getCrPcrCurrcode()) .crPcrCurrshort(glAccontTranasctionRequestModel.getCrPcrCurrshort()) .sgtSentGntrnumber((String.valueOf(response.get("tran_id")))) .sgtSentNodeId(String.valueOf(response.get("node_id"))) .drSgtGntrdate(LocalDate.now()) .sgtGntramt(BigDecimal.valueOf(glAccontTranasctionRequestModel.getSgtGntramtfc())) .batAcnttranSend(true) .batAcnttranReceived(true) .sgtReceiveGntrnumber((String.valueOf(response.get("tran_id")))) .sgtReceiveNodeId(String.valueOf(response.get("node_id"))) .build(); transactionTrailRepository.save(transactionTrail); GLAccountMasterTransaction glAccountMasterTransaction = GLAccountMasterTransaction.builder() .porOrgacode(glAccontTranasctionRequestModel.getPorOrgacode()) .cmpCustcode(glAccontTranasctionRequestModel.getCmpCustcode()) .crMbmBkmsnumber(glAccontTranasctionRequestModel.getCrMbmBkmsnumber()) .crMbmBkmstitle(glAccontTranasctionRequestModel.getCrMbmBkmstitle()) .crPcrCurrcode(glAccontTranasctionRequestModel.getCrPcrCurrcode()) .crPcrCurrdesc(glAccontTranasctionRequestModel.getCrPcrCurrdesc()) .crPcrCurrshort(glAccontTranasctionRequestModel.getCrPcrCurrshort()) .drPcaGlaccode(glAccontTranasctionRequestModel.getDrPcaGlaccode()) .drPcaGlacdesc(glAccontTranasctionRequestModel.getDrPcaGlacdesc()) .dmpProdcode(glAccontTranasctionRequestModel.getDmpProdCode()) .sgtGntramt(BigDecimal.valueOf(glAccontTranasctionRequestModel.getSgtGntramtfc())) .drSgtGntramt(BigDecimal.valueOf(glAccontTranasctionRequestModel.getSgtGntramtfc())) .drSgtGntrdate(LocalDate.now()) .sgtSentGntrnumber(String.valueOf(response.get("node_id"))+(response.get("tran_id"))) .build(); glAccountMasterTransactionTrailRepository.save(glAccountMasterTransaction); return response; } public Object reverseTransacion(String porOrgacode, String nodeId, String sgtGntrnumber) { Object reponse = webClientDepositService.postTransaction(CoreReverseTransaction.builder().porOrgacode(porOrgacode).sgtGntrtranlink(sgtGntrnumber).nodeId(nodeId).sgtGntrcreateat("2003").build(), UCOURI.CORE_REVERSE_TRANSACTION, porOrgacode); TransactionTrail trail = transactionTrailRepository.findByPorOrgacodeAndSgtSentNodeIdAndSgtSentGntrnumber(porOrgacode,nodeId,sgtGntrnumber); Double balance = ucoAccountService.fetchAccountBalance(trail.getPorOrgacode(), trail.getDrMbmBkmsnumber()); trail.setBatAcnttranReversed(true); trail.setBatAcnttranReceived(true); trail.setDrMbmBkmsbalance(BigDecimal.valueOf(balance)); transactionTrailRepository.save(trail); return reponse; } }