From c8bc6e7f12bdc45772621ca63227e6e09ac6cda9 Mon Sep 17 00:00:00 2001 From: Lincpay Tech Date: Wed, 17 Dec 2025 15:30:33 +0530 Subject: [PATCH 1/4] half commited --- .../bbps/entites/AuditListener.java | 39 ++-- .../bbps/entites/Auditable.java | 12 +- .../bbps/entites/PaymentInitiate.java | 37 +--- .../OutboundTraceLogRepository.java | 9 +- .../repository/PaymentIntiateRepository.java | 19 +- .../TransactionMasterRepository.java | 7 + .../bbps/scheduler/PaymentRetryScheduler.java | 162 ++++++++++++++ .../bbps/serviceImpl/PaymentServiceImpl.java | 208 ++++++++++++++---- .../serviceImpl/PaymentStartServiceImpl.java | 4 +- .../bbps/util/BBPSConstant.java | 4 +- .../bbps/util/BillerMapperHelper.java | 38 ++-- src/main/resources/application.properties | 2 +- 12 files changed, 409 insertions(+), 132 deletions(-) create mode 100644 src/main/java/com/billpaymentservice/bbps/scheduler/PaymentRetryScheduler.java diff --git a/src/main/java/com/billpaymentservice/bbps/entites/AuditListener.java b/src/main/java/com/billpaymentservice/bbps/entites/AuditListener.java index 4249710..dc9e859 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/AuditListener.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/AuditListener.java @@ -6,29 +6,37 @@ import java.time.LocalDateTime; import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; +import java.lang.reflect.Field; +import java.util.Date; + +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; + public class AuditListener { @PrePersist public void prePersist(Object entity) { - LocalDateTime now = LocalDateTime.now(); + Date now = new Date(); // current date + time - // Creation fields + // creation fields setIfNull(entity, "createDateTime", now); - setIfNull(entity, "createdBy", null); // handle manually in service if needed - setIfNull(entity, "createdEpochDate", System.currentTimeMillis()); + setIfNull(entity, "createdBy", null); + setIfNull(entity, "createdEpochDate", now.getTime()); - // Set isDeleted = false on creation + // default soft delete flag setAlways(entity, "isDeleted", Boolean.FALSE); } - /* - * @PreUpdate public void preUpdate(Object entity) { LocalDateTime now = - * LocalDateTime.now(); - * - * // Update fields setAlways(entity, "updatedDateTime", now); setIfNull(entity, - * "updatedBy", null); // you can override manually in service setAlways(entity, - * "modifiedEpochDate", System.currentTimeMillis()); } - */ + @PreUpdate + public void preUpdate(Object entity) { + Date now = new Date(); + + // update fields + setAlways(entity, "updatedDateTime", now); + setIfNull(entity, "updatedBy", null); + setAlways(entity, "modifiedEpochDate", now.getTime()); + } + private void setIfNull(Object entity, String fieldName, Object value) { try { Field f = findField(entity.getClass(), fieldName); @@ -37,8 +45,7 @@ public class AuditListener { boolean accessible = f.canAccess(entity); f.setAccessible(true); - Object currentValue = f.get(entity); - if (currentValue == null) { + if (f.get(entity) == null) { f.set(entity, value); } @@ -53,9 +60,7 @@ public class AuditListener { boolean accessible = f.canAccess(entity); f.setAccessible(true); - f.set(entity, value); - f.setAccessible(accessible); } catch (Exception ignored) {} } diff --git a/src/main/java/com/billpaymentservice/bbps/entites/Auditable.java b/src/main/java/com/billpaymentservice/bbps/entites/Auditable.java index 2359759..3ab0c03 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/Auditable.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/Auditable.java @@ -1,6 +1,8 @@ package com.billpaymentservice.bbps.entites; +import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Date; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; @@ -16,18 +18,18 @@ import lombok.Setter; @EntityListeners(AuditListener.class) @Getter @Setter -public abstract class Auditable { +public abstract class Auditable implements Serializable { @Column(name = "create_date_time") - private LocalDateTime createDateTime; + private Date createDateTime; @Column(name = "updated_date_time") - private LocalDateTime updatedDateTime; + private Date updatedDateTime; @Column(name = "create_by") - private LocalDateTime createdBy; + private String createdBy; @Column(name = "updated_by") - private LocalDateTime updatedBy; + private String updatedBy; @Column(name = "created_epoch_date") private Long createdEpochDate; diff --git a/src/main/java/com/billpaymentservice/bbps/entites/PaymentInitiate.java b/src/main/java/com/billpaymentservice/bbps/entites/PaymentInitiate.java index 0c0135c..97b7851 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/PaymentInitiate.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/PaymentInitiate.java @@ -1,9 +1,5 @@ package com.billpaymentservice.bbps.entites; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,30 +7,15 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -@Entity -@Table(name = "bbps_payment_initiate") +public class PaymentInitiate { -public class PaymentInitiate extends Auditable { + // Kept as a POJO for compatibility but not annotated as @Entity so JPA won't map it + private String reqId; + private String bankTxnId; + private String clientUserId; + private String mid; + private String txnId; + private String status; + private String userId; - @Id - @Column(name = "req_id") - private String reqId; - - @Column(name = "bank_txn_Id", nullable = false) - private String bankTxnId; - - @Column(name = "client_user_id") - private String clientUserId; - - @Column(name = "mid") - private String mid; - - @Column(name = "txn_id") - private String txnId; - - @Column(name = "status") - private String status; - - @Column(name = "user_id") - private String userId; } \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/repository/OutboundTraceLogRepository.java b/src/main/java/com/billpaymentservice/bbps/repository/OutboundTraceLogRepository.java index f39d8cc..0013458 100644 --- a/src/main/java/com/billpaymentservice/bbps/repository/OutboundTraceLogRepository.java +++ b/src/main/java/com/billpaymentservice/bbps/repository/OutboundTraceLogRepository.java @@ -1,9 +1,16 @@ package com.billpaymentservice.bbps.repository; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.billpaymentservice.bbps.entites.OutboundTraceLog; public interface OutboundTraceLogRepository extends JpaRepository { -} + Optional findTopByTxnIdAndApiNameOrderByIdDesc(String txnId, String apiName); + + List findByTxnId(String txnId); + +} \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/repository/PaymentIntiateRepository.java b/src/main/java/com/billpaymentservice/bbps/repository/PaymentIntiateRepository.java index f91d6f9..bfc9d0a 100644 --- a/src/main/java/com/billpaymentservice/bbps/repository/PaymentIntiateRepository.java +++ b/src/main/java/com/billpaymentservice/bbps/repository/PaymentIntiateRepository.java @@ -1,17 +1,6 @@ package com.billpaymentservice.bbps.repository; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import com.billpaymentservice.bbps.entites.PaymentInitiate; - -public interface PaymentIntiateRepository extends JpaRepository { - - // Optional findByReqId(String clientReqId); - - @Query(value = "SELECT * FROM bbps_payment_initiate WHERE req_id = :clientReqId", nativeQuery = true) - Optional findByReqId(String clientReqId); - -} +// Legacy stub: PaymentInitiate table removed; keep a placeholder class to avoid compilation errors if referenced. +public class PaymentIntiateRepository { + // Intentionally empty - PaymentInitiate is no longer a JPA entity and this repo is retired. +} \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java index ea41e2d..1faa4b7 100644 --- a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java +++ b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java @@ -30,4 +30,11 @@ public interface TransactionMasterRepository extends JpaRepository findByPaymentRefId(String paymentRefId); + // New finders to replace PaymentInitiate usage + List findByClientReqId(String clientReqId); + + List findByStatus(String status); + + List findByStatusIn(List statuses); + } \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/scheduler/PaymentRetryScheduler.java b/src/main/java/com/billpaymentservice/bbps/scheduler/PaymentRetryScheduler.java new file mode 100644 index 0000000..045f472 --- /dev/null +++ b/src/main/java/com/billpaymentservice/bbps/scheduler/PaymentRetryScheduler.java @@ -0,0 +1,162 @@ +package com.billpaymentservice.bbps.scheduler; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.modelmapper.ModelMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.billpaymentservice.bbps.dto.GeneralResp; +import com.billpaymentservice.bbps.dto.response.BillPaymentResponse; +import com.billpaymentservice.bbps.dto.request.TransactionStatusRequest; +import com.billpaymentservice.bbps.entites.BillPayment; +import com.billpaymentservice.bbps.entites.BharatConnectMaster; +import com.billpaymentservice.bbps.entites.OutboundTraceLog; +import com.billpaymentservice.bbps.entites.TransactionMaster; +import com.billpaymentservice.bbps.repository.BharatConnectMasterRepository; +import com.billpaymentservice.bbps.repository.OutboundTraceLogRepository; +import com.billpaymentservice.bbps.repository.TransactionMasterRepository; +import com.billpaymentservice.bbps.repository.PaymentRepository; +import com.billpaymentservice.bbps.service.PaymentService; +import com.billpaymentservice.bbps.serviceImpl.HttpService; +import com.billpaymentservice.bbps.util.BBPSConstant; +import com.billpaymentservice.bbps.util.Helper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class PaymentRetryScheduler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentRetryScheduler.class); + + private final TransactionMasterRepository transactionMasterRepository; + private final OutboundTraceLogRepository outboundTraceLogRepository; + private final BharatConnectMasterRepository beneficiaryDetailsRepository; + private final Helper helper; + private final HttpService httpService; + private final PaymentRepository paymentRepository; + private final PaymentService paymentService; + private final ObjectMapper objectMapper; + private final ModelMapper modelMapper; + + @Autowired + public PaymentRetryScheduler(TransactionMasterRepository transactionMasterRepository, + OutboundTraceLogRepository outboundTraceLogRepository, + BharatConnectMasterRepository beneficiaryDetailsRepository, Helper helper, HttpService httpService, + PaymentRepository paymentRepository, PaymentService paymentService) { + this.transactionMasterRepository = transactionMasterRepository; + this.outboundTraceLogRepository = outboundTraceLogRepository; + this.beneficiaryDetailsRepository = beneficiaryDetailsRepository; + this.helper = helper; + this.httpService = httpService; + this.paymentRepository = paymentRepository; + this.paymentService = paymentService; + this.objectMapper = new ObjectMapper(); + this.modelMapper = new ModelMapper(); + } + + // runs every 60 seconds by default; configurable via property scheduler.retry.delay.ms + @Scheduled(fixedDelayString = "${scheduler.retry.delay.ms:60000}") + public void retryPendingPayments() { + LOGGER.info("PaymentRetryScheduler triggered"); + List statuses = Arrays.asList("PENDING_UPSTREAM", "INVALID_RESPONSE_FROM_BBPS", + "INVALID_JSON_FROM_UPSTREAM", "Failed", "FAILED"); + + List pendingList = transactionMasterRepository.findByStatusIn(statuses); + if (pendingList == null || pendingList.isEmpty()) { + LOGGER.debug("No pending TransactionMaster entries found"); + return; + } + + for (TransactionMaster tm : pendingList) { + try { + LOGGER.info("Retrying transaction txnId={} status={}", tm.getTxnId(), tm.getStatus()); + + Optional outOpt = outboundTraceLogRepository + .findTopByTxnIdAndApiNameOrderByIdDesc(tm.getTxnId(), BBPSConstant.API_INITIATE_PAYMENT); + if (outOpt.isEmpty()) { + LOGGER.warn("No outbound trace log found for txnId={}", tm.getTxnId()); + continue; + } + + OutboundTraceLog outbound = outOpt.get(); + String payload = outbound.getRequestPayload(); + if (payload == null || payload.isEmpty()) { + LOGGER.warn("Outbound request payload empty for txnId={}", tm.getTxnId()); + continue; + } + + BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); + HttpHeaders headers = helper.buildHeaders(headersData); + + LOGGER.info("Resending request to BBPS for txnId={} url={}", tm.getTxnId(), outbound.getEndpointUrl()); + Map responseMap = httpService.postRequest(outbound.getEndpointUrl(), payload, headers); + String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); + LOGGER.info("Retry response for txnId={} => {}", tm.getTxnId(), responseData); + + helper.updateOutboundLogResponse(outbound, responseData); + outboundTraceLogRepository.save(outbound); + + if (responseData == null || responseData.isEmpty()) { + LOGGER.warn("Empty response on retry for txnId={}", tm.getTxnId()); + // leave as pending for next run + continue; + } + + Map dataMap = objectMapper.readValue(responseData, new TypeReference>() {}); + if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERIC_KEY)) { + LOGGER.warn("Retry response invalid structure for txnId={}", tm.getTxnId()); + // update TransactionMaster status for visibility + tm.setStatus("INVALID_RESPONSE_FROM_BBPS"); + transactionMasterRepository.save(tm); + continue; + } + + BillPaymentResponse billResp = objectMapper.convertValue(dataMap, BillPaymentResponse.class); + GeneralResp gen = billResp.getGenericResponse(); + + if (gen == null) { + LOGGER.warn("Generic response null after retry for txnId={}", tm.getTxnId()); + continue; + } + + if (!"Failed".equalsIgnoreCase(gen.getStatus())) { + // success path + BillPayment entity = modelMapper.map(billResp, BillPayment.class); + paymentRepository.save(entity); + + tm.setBankTxnId(billResp.getTxnId()); + tm.setStatus(billResp.getStatus()); + transactionMasterRepository.save(tm); + + // trigger transaction status check (best-effort) + try { + TransactionStatusRequest tsReq = new TransactionStatusRequest(); + tsReq.setTxnId(billResp.getTxnId()); + tsReq.setReqId(tm.getReqId()); + tsReq.setMid(tm.getMid()); + tsReq.setClientUserId(tm.getClientUserId()); + paymentService.checkTrasactionStatus(tsReq); + } catch (Exception e) { + LOGGER.warn("Transaction status check failed after retry for txnId={} : {}", tm.getTxnId(), e.getMessage()); + } + } else { + // upstream returned failed status + tm.setBankTxnId(billResp.getTxnId()); + tm.setStatus(gen.getStatus()); + transactionMasterRepository.save(tm); + } + + } catch (Exception ex) { + LOGGER.error("Error while retrying transaction txnId={}: {}", tm.getTxnId(), ex.getMessage(), ex); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java index dee3150..4549eef 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java @@ -1,5 +1,6 @@ package com.billpaymentservice.bbps.serviceImpl; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -23,14 +24,12 @@ import com.billpaymentservice.bbps.entites.BharatConnectMaster; import com.billpaymentservice.bbps.entites.BillPayment; import com.billpaymentservice.bbps.entites.InboundTraceLog; import com.billpaymentservice.bbps.entites.OutboundTraceLog; -import com.billpaymentservice.bbps.entites.PaymentInitiate; import com.billpaymentservice.bbps.entites.TransactionMaster; import com.billpaymentservice.bbps.exception.CustomException; import com.billpaymentservice.bbps.repository.AgentFundTransferRepository; import com.billpaymentservice.bbps.repository.BharatConnectMasterRepository; import com.billpaymentservice.bbps.repository.InboundTraceLogRepository; import com.billpaymentservice.bbps.repository.OutboundTraceLogRepository; -import com.billpaymentservice.bbps.repository.PaymentIntiateRepository; import com.billpaymentservice.bbps.repository.PaymentRepository; import com.billpaymentservice.bbps.repository.TransactionMasterRepository; import com.billpaymentservice.bbps.service.PaymentService; @@ -54,7 +53,6 @@ public class PaymentServiceImpl implements PaymentService { private final PaymentRepository paymentRepository; private final InboundTraceLogRepository inboundTraceLogRepository; private final OutboundTraceLogRepository outboundTraceLogRepository; - private final PaymentIntiateRepository paymentIntiateRepository; private final TransactionMasterRepository transactionMasterRepository; private final AgentFundTransferRepository agentFundTransferRepository; private final BillerMapperHelper mapperHelper; @@ -66,8 +64,7 @@ public class PaymentServiceImpl implements PaymentService { public PaymentServiceImpl(BharatConnectMasterRepository beneficiaryDetailsRepository, HttpService httpService, Helper helper, PaymentRepository paymentRepository, InboundTraceLogRepository inboundTraceLogRepository, - OutboundTraceLogRepository outboundTraceLogRepository, PaymentIntiateRepository paymentIntiateRepository, - TransactionMasterRepository transactionMasterRepository, + OutboundTraceLogRepository outboundTraceLogRepository, TransactionMasterRepository transactionMasterRepository, AgentFundTransferRepository agentFundTransferRepository, BillerMapperHelper mapperHelper) { this.beneficiaryDetailsRepository = beneficiaryDetailsRepository; this.httpService = httpService; @@ -75,7 +72,6 @@ public class PaymentServiceImpl implements PaymentService { this.paymentRepository = paymentRepository; this.inboundTraceLogRepository = inboundTraceLogRepository; this.outboundTraceLogRepository = outboundTraceLogRepository; - this.paymentIntiateRepository = paymentIntiateRepository; this.transactionMasterRepository = transactionMasterRepository; this.agentFundTransferRepository = agentFundTransferRepository; this.mapperHelper = mapperHelper; @@ -114,6 +110,26 @@ public class PaymentServiceImpl implements PaymentService { forwardjson); LOGGER.debug("Outbound log created for api={} | reqId={} | txnId={}", apiName, requestId, txnId); + // Persist a TransactionMaster BEFORE making the external call so we have a DB record + // even if the HTTP call fails later. This is intentionally tolerant: failure to + // save here should not prevent the outgoing request, so we catch and log errors. + try { + TransactionMaster preTransaction = new TransactionMaster(); + preTransaction.setBankTxnId("INITIATED_" + txnId); + preTransaction.setTxnId(txnId); + preTransaction.setReqId(requestId); + preTransaction.setMid(billPaymentRequest.getMid()); + preTransaction.setClientUserId(billPaymentRequest.getClientUserId()); + preTransaction.setClientReqId(billPaymentRequest.getClientReqId()); + preTransaction.setUserId(userId); + preTransaction.setStatus("INITIATED"); + EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction); + saveTransactionMasterWithRetry(preTransaction); + LOGGER.info("Pre-request TransactionMaster saved with status=INITIATED for txnId={}", txnId); + } catch (Exception e) { + LOGGER.warn("Failed to persist pre-request TransactionMaster for txnId={}: {}", txnId, e.getMessage()); + } + try { LOGGER.info("Calling BBPS payment URL: {}", url); Map responseMap = httpService.postRequest(url, forwardjson, headers); @@ -125,14 +141,21 @@ public class PaymentServiceImpl implements PaymentService { if (responseData == null || responseData.isEmpty()) { helper.updateInboundLogFailure(inboundLog, BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, - BBPSConstant.FAILED_STR); + BBPSConstant.FAILED_STR); LOGGER.warn("Empty response received from BBPS for txnId={}", txnId); - throw new CustomException( - BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, // message - BBPSConstant.FAILED_STATUS, // status - 502, // statusCode - "BBPS upstream returned empty response" - ); + // Persist a TransactionMaster placeholder so scheduler can pick it up later + TransactionMaster tm = new TransactionMaster(); + tm.setBankTxnId("PENDING_" + txnId); + tm.setTxnId(txnId); + tm.setReqId(requestId); + tm.setMid(billPaymentRequest.getMid()); + tm.setClientUserId(billPaymentRequest.getClientUserId()); + tm.setClientReqId(billPaymentRequest.getClientReqId()); + tm.setUserId(userId); + tm.setStatus("PENDING_UPSTREAM"); + transactionMasterRepository.save(tm); + LOGGER.info("TransactionMaster placeholder saved with status=PENDING_UPSTREAM for txnId={}", txnId); + return new ResponseModel(BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, "PENDING_UPSTREAM", 502, null); } Map dataMap = objectMapper.readValue(responseData, @@ -141,16 +164,42 @@ public class PaymentServiceImpl implements PaymentService { if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERIC_KEY)) { helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_STRUCTURE); LOGGER.warn("Invalid response structure received for txnId={}", txnId); - throw new CustomException(BBPSConstant.INVALID_STRUCTURE); + // Save a TransactionMaster placeholder so scheduler/admin can reconcile later + TransactionMaster tm = new TransactionMaster(); + tm.setBankTxnId("INVALID_" + txnId); + tm.setTxnId(txnId); + tm.setReqId(requestId); + tm.setMid(billPaymentRequest.getMid()); + tm.setClientUserId(billPaymentRequest.getClientUserId()); + tm.setClientReqId(billPaymentRequest.getClientReqId()); + tm.setUserId(userId); + tm.setStatus("INVALID_RESPONSE_FROM_BBPS"); + transactionMasterRepository.save(tm); + LOGGER.info("TransactionMaster placeholder saved with status=INVALID_RESPONSE_FROM_BBPS for txnId={}", txnId); + return new ResponseModel(BBPSConstant.INVALID_STRUCTURE, "INVALID_RESPONSE_FROM_BBPS", 502, null); } BillPaymentResponse billPaymentResponse = objectMapper.convertValue(dataMap, BillPaymentResponse.class); GeneralResp generalResponse = billPaymentResponse.getGenericResponse(); LOGGER.info("genric Response" + generalResponse); if(generalResponse.getStatus().equalsIgnoreCase("Failed")) { - throw new CustomException(generalResponse.getMessage(), generalResponse.getStatus(), - Integer.parseInt(generalResponse.getStatusCode()), generalResponse.getRemarks()); - } + // Save TransactionMaster with failed status so it can be reconciled later + TransactionMaster tm = new TransactionMaster(); + tm.setBankTxnId(billPaymentResponse.getTxnId()); + tm.setTxnId(txnId); + tm.setReqId(requestId); + tm.setMid(billPaymentRequest.getMid()); + tm.setClientUserId(billPaymentRequest.getClientUserId()); + tm.setClientReqId(billPaymentRequest.getClientReqId()); + tm.setUserId(userId); + tm.setStatus(generalResponse.getStatus()); + // ensure internal txnId uniqueness and save + tm.setTxnId(makeUniqueTxnId(tm.getTxnId())); + saveTransactionMasterWithRetry(tm); + LOGGER.info("TransactionMaster saved with failed status={} for txnId={}", generalResponse.getStatus(), txnId); + return new ResponseModel(generalResponse.getMessage(), generalResponse.getStatus(), + Integer.parseInt(generalResponse.getStatusCode()), billPaymentResponse); + } BillPayment entity = modelMapper.map(billPaymentResponse, BillPayment.class); @@ -158,17 +207,29 @@ public class PaymentServiceImpl implements PaymentService { LOGGER.info("BillPayment entity saved successfully for txnId={}", txnId); helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus()); - PaymentInitiate paymentInitiate = new PaymentInitiate(); - paymentInitiate.setTxnId(txnId); - paymentInitiate.setUserId(userId); - paymentInitiate.setReqId(requestId); - paymentInitiate.setMid(billPaymentRequest.getMid()); - paymentInitiate.setClientUserId(billPaymentRequest.getClientUserId()); - paymentInitiate.setBankTxnId(billPaymentResponse.getTxnId()); - paymentInitiate.setStatus(billPaymentResponse.getStatus()); - paymentIntiateRepository.save(paymentInitiate); - LOGGER.info("PaymentInitiate entry saved successfully for txnId={}", txnId); - + // Create and save TransactionMaster for this initiate flow so everything is stored in the same table + try { + TransactionMaster transactionMaster = new TransactionMaster(); + // bankTxnId is BBPS txn id (primary key) + transactionMaster.setBankTxnId(billPaymentResponse.getTxnId()); + // use internal txnId generated earlier for tracing + transactionMaster.setTxnId(txnId); + transactionMaster.setReqId(requestId); + transactionMaster.setMid(billPaymentRequest.getMid()); + transactionMaster.setClientUserId(billPaymentRequest.getClientUserId()); + transactionMaster.setClientReqId(billPaymentRequest.getClientReqId()); + transactionMaster.setUserId(userId); + transactionMaster.setBillerId(billPaymentResponse.getBillerId()); + transactionMaster.setTxnAmount(billPaymentResponse.getTxnAmount()); + transactionMaster.setStatus(billPaymentResponse.getStatus()); + EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); + // ensure internal txnId uniqueness and save (handles collisions) + transactionMaster.setTxnId(makeUniqueTxnId(transactionMaster.getTxnId())); + saveTransactionMasterWithRetry(transactionMaster); + LOGGER.info("TransactionMaster created during initiatePayment for internalTxnId={} bankTxnId={}", transactionMaster.getTxnId(), transactionMaster.getBankTxnId()); + } catch (Exception e) { + LOGGER.warn("Failed to persist TransactionMaster during initiatePayment for txnId={}: {}", txnId, e.getMessage()); + } TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); transactionStatusRequest.setTxnId(billPaymentResponse.getTxnId()); transactionStatusRequest.setReqId(requestId); @@ -188,6 +249,18 @@ public class PaymentServiceImpl implements PaymentService { } catch (JsonProcessingException e) { LOGGER.error("JSON Parsing Error in initiatePayment: {}", e.getMessage(), e); helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); + // Persist TransactionMaster placeholder so admin/scheduler can reconcile later + TransactionMaster tm = new TransactionMaster(); + tm.setBankTxnId("INVALID_JSON_" + txnId); + tm.setTxnId(txnId); + tm.setUserId(billPaymentRequest.getClientReqId()); + tm.setReqId(billPaymentRequest.getReqId()); + tm.setMid(billPaymentRequest.getMid()); + tm.setClientUserId(billPaymentRequest.getClientUserId()); + tm.setClientReqId(billPaymentRequest.getClientReqId()); + tm.setStatus("INVALID_JSON_FROM_UPSTREAM"); + transactionMasterRepository.save(tm); + LOGGER.info("TransactionMaster saved with status=INVALID_JSON_FROM_UPSTREAM for txnId={}", txnId); throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM, BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); } } @@ -244,7 +317,9 @@ public class PaymentServiceImpl implements PaymentService { transactionMaster.setMid(transactionStatusRequest.getMid()); transactionMaster.setClientUserId(transactionStatusRequest.getClientUserId()); EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); - transactionMasterRepository.save(transactionMaster); + // Ensure internal txnId is unique; if collisions occur append suffix (_1, _2...) + transactionMaster.setTxnId(makeUniqueTxnId(transactionMaster.getTxnId())); + saveTransactionMasterWithRetry(transactionMaster); LOGGER.info("TransactionMaster saved successfully for txnId={}", txnId); AgentFundTransfer agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), @@ -306,7 +381,7 @@ public class PaymentServiceImpl implements PaymentService { }); if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { helper.updateInboundLogFailure(inboundLog, responseData, - String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); + String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); LOGGER.warn("Invalid transaction status response for txnId={} from BBPS", transactionStatusRequest.getTxnId()); throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), @@ -350,18 +425,29 @@ public class PaymentServiceImpl implements PaymentService { LOGGER.info("[{}] START | reqId={} | mid={} | clientReqId={}", apiName, requestId, transactionStatusRequest.getMid(), clientReqId); - Optional paymentInitiate = paymentIntiateRepository.findByReqId(clientReqId); - if (paymentInitiate.isEmpty()) { + // look up TransactionMaster(s) by clientReqId instead of PaymentInitiate + List transactionMasters = transactionMasterRepository.findByClientReqId(clientReqId); + if (transactionMasters == null || transactionMasters.isEmpty()) { throw new CustomException( - BBPSConstant.NO_BILL_FOUND, // message // status - BBPSConstant.FAILED_STATUS, - BBPSConstant.HTTP_NOT_FOUND, // statusCode - "Bill not found in database for given clientReqId" + BBPSConstant.NO_BILL_FOUND, // message // status + BBPSConstant.FAILED_STATUS, + BBPSConstant.HTTP_NOT_FOUND, // statusCode + "Bill not found in database for given clientReqId" ); } + // prefer a TransactionMaster with a real bankTxnId (not a placeholder) + TransactionMaster chosen = null; + for (TransactionMaster t : transactionMasters) { + if (t.getBankTxnId() != null && !t.getBankTxnId().startsWith("PENDING_") && !t.getBankTxnId().startsWith("INVALID_") && !t.getBankTxnId().startsWith("INVALID_JSON_")) { + chosen = t; + break; + } + } + if (chosen == null) chosen = transactionMasters.get(0); + String url = paymentUrl + BBPSConstant.CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL - + paymentInitiate.get().getTxnId(); + + chosen.getTxnId(); BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); HttpHeaders headers = helper.buildHeaders(headersData); @@ -419,4 +505,52 @@ public class PaymentServiceImpl implements PaymentService { } } + /** + * Ensure the provided internal txnId is unique in bbps_transaction_master.txn_id. + * If a record already exists with the same txnId, append _1, _2, ... until unique. + */ + private String makeUniqueTxnId(String baseTxnId) { + if (baseTxnId == null || baseTxnId.isBlank()) return baseTxnId; + List existing = transactionMasterRepository.findByTxnId(baseTxnId); + if (existing == null || existing.isEmpty()) { + return baseTxnId; + } + int suffix = 1; + String candidate; + while (true) { + candidate = baseTxnId + "_" + suffix; + List coll = transactionMasterRepository.findByTxnId(candidate); + if (coll == null || coll.isEmpty()) { + return candidate; + } + suffix++; + } + } + + /** + * Try to save TransactionMaster and handle unique txnId collisions that may occur under concurrency. + * This method will attempt to save up to maxAttempts; on duplicate collision it will append a numeric suffix. + */ + private void saveTransactionMasterWithRetry(TransactionMaster transactionMaster) { + final int maxAttempts = 5; + String baseTxnId = transactionMaster.getTxnId(); + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + if (attempt == 0) { + transactionMaster.setTxnId(makeUniqueTxnId(baseTxnId)); + } else { + transactionMaster.setTxnId(baseTxnId + "_" + attempt); + } + transactionMasterRepository.save(transactionMaster); + return; // success + } catch (org.springframework.dao.DataIntegrityViolationException dex) { + LOGGER.warn("Duplicate txnId detected when saving TransactionMaster (attempt {}) for baseTxnId={}: {}", attempt, baseTxnId, dex.getMessage()); + // retry with next suffix + } + } + // if we reach here, all attempts failed + LOGGER.error("Failed to save TransactionMaster after {} attempts for baseTxnId={}", maxAttempts, transactionMaster.getTxnId()); + throw new CustomException("Unable to save TransactionMaster due to txnId collisions", BBPSConstant.FAILED_STATUS, 500, "Duplicate txnId"); + } + } diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java index d36004d..6ce0311 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java @@ -87,7 +87,6 @@ public class PaymentStartServiceImpl implements PaymentStartService { String txnId = helper.generateTxnId(15); LOGGER.info("MID IS:{}",paymentMid); LOGGER.info("[startIntentPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); - helper.validateMerchant(request.getMid()); // inbound log @@ -102,7 +101,7 @@ public class PaymentStartServiceImpl implements PaymentStartService { sent.setEnckey(paymentEncryKey); String plaintext = helper.convertObjectToJson(sent); String encrypted = encryptionUtil.encryptMData(plaintext, paymentEncryKey); - + PaymentRequestPayload finalPayload = new PaymentRequestPayload(); finalPayload.setMid(paymentMid); finalPayload.setPayload(encrypted); @@ -217,7 +216,6 @@ public class PaymentStartServiceImpl implements PaymentStartService { String requestId = request.getReqId(); String apiName = BBPSConstant.API_START_PAYMENT; String txnId = helper.generateTxnId(15); - LOGGER.info("[startWebPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); helper.validateMerchant(request.getMid()); diff --git a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java index d8b5e09..c151e3d 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java @@ -51,8 +51,8 @@ public class BBPSConstant extends BBPS_RESPONSE_COD_CONSTANT { public static final String BILL_FETCH_BY_ID = "/couapp/bills/fetch/byId?billId="; public static final String BILL_FETCH_BY_CLIENT_REQ_ID = "/couapp/bills/fetch/byClientId?clientReqId="; public static final String BILL_VALIDATE_BY_CLIENT_REQ_ID = "/couapp/bills/validate/byClientId?clientReqId="; - public static final String PAYMENT_URL = "/agent/bills/payment"; - public static final String CHECK_TRANSACTION_URL = "/transactions/payment/byTxnId?txnId="; + public static final String PAYMENT_URL = "/agent/bills/paymhent"; + public static final String CHECK_TRANSACTION_URL = "/transactions/payment/byTxnId?txnId=j"; public static final String CHECK_TRANSACTION_BY_CLIENT_URL = "/bills/fetch/byClientId?clientReqId="; public static final String CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL = "/transactions/payment/byClientId?clientReqId="; diff --git a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java index 04d1af6..4581928 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java @@ -2,6 +2,7 @@ package com.billpaymentservice.bbps.util; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -719,9 +720,9 @@ public class BillerMapperHelper { parent.setActionType(responseWrapper.getActionType()); parent.setUserId(responseWrapper.getUserId()); // Map incoming DTO date strings into Auditable LocalDateTime fields - LocalDateTime parentCreatedOn = parseToLocalDateTime(responseWrapper.getCreationDate()); + Date parentCreatedOn = parseToDate(responseWrapper.getCreationDate()); - LocalDateTime parentUpdatedOn = parseToLocalDateTime(responseWrapper.getModifiedDate()); + Date parentUpdatedOn = parseToDate(responseWrapper.getModifiedDate()); parent.setCreatedEpochDate(responseWrapper.getCreatedEpochDate()); parent.setModifiedEpochDate(responseWrapper.getModifiedEpochDate()); parent.setReqId(reqId); @@ -963,9 +964,9 @@ public class BillerMapperHelper { // DATES // Map incoming DTO date strings into Auditable LocalDateTime fields - LocalDateTime bmCreatedOn = parseToLocalDateTime(responseWrapper.getCreationDate()); + Date bmCreatedOn = parseToDate(responseWrapper.getCreationDate()); - LocalDateTime bmUpdatedOn = parseToLocalDateTime(responseWrapper.getModifiedDate()); + Date bmUpdatedOn = parseToDate(responseWrapper.getModifiedDate()); if (bmUpdatedOn != null) { billMaster.setUpdatedDateTime(bmUpdatedOn); } @@ -977,26 +978,17 @@ public class BillerMapperHelper { // Helper to parse incoming ISO date/time strings (with optional offset) into // LocalDateTime - private LocalDateTime parseToLocalDateTime(String dateStr) { - if (dateStr == null || dateStr.isBlank()) { - return null; - } - try { - // Try parsing with offset first (e.g., 2023-04-19T17:28:05.508679Z or with - // +05:30) - OffsetDateTime odt = OffsetDateTime.parse(dateStr); - return odt.toLocalDateTime(); - } catch (Exception e) { - try { - // Fallback to LocalDateTime without offset - return LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); - } catch (Exception ex) { - LOGGER.warn("Unable to parse date string to LocalDateTime: {}", dateStr); - return null; - } - } + private Date parseToDate(String dateStr) { + try { + // example format: 2025-12-17T14:30:45 + LocalDateTime ldt = LocalDateTime.parse(dateStr); + return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + } catch (Exception e) { + return null; + } } + // Convert TransactionMaster to master DTO public TransactionHistoryMasterDTO toDTO(TransactionMaster transactionMaster) { if (transactionMaster == null) @@ -1104,7 +1096,7 @@ public class BillerMapperHelper { .orElse("DUMMY_TXN")); record.setQrString(Optional.ofNullable(apiResponse.getResponseData(). getQrString()).orElse("NO_QR")); // parse createdOn string into LocalDateTime and set on Auditable - LocalDateTime createdOn =parseToLocalDateTime(apiResponse.getResponseData().getCreatedOn()); + Date createdOn =parseToDate(apiResponse.getResponseData().getCreatedOn()); record.setTxnStatus("PENDING"); } else { record.setAmount("0.00"); record.setTxnStatus("PENDING"); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7efe143..3a7a57a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -76,7 +76,7 @@ jwt.secret=6C756148487475787A414646736D4B6E444E585A766145396E6A4555594A595566657 # Resilience4j Rate Limiter # =============================== -resilience4j.ratelimiter.configs.default.limit-for-period=5 +resilience4j.ratelimiter.configs.default.limit-for-period=1000 resilience4j.ratelimiter.configs.default.limit-refresh-period=1m resilience4j.ratelimiter.configs.default.timeout-duration=0 # Resilience4j Retry From d34b2e1b845cf444a3ee1cf66d6107c59c5705b7 Mon Sep 17 00:00:00 2001 From: Lincpay Tech Date: Thu, 18 Dec 2025 10:20:49 +0530 Subject: [PATCH 2/4] changed trasaction master saving --- .../bbps/entites/TransactionMaster.java | 11 +- .../TransactionMasterRepository.java | 3 + .../bbps/serviceImpl/PaymentServiceImpl.java | 528 ++++++++++-------- .../bbps/util/BBPSConstant.java | 4 +- 4 files changed, 306 insertions(+), 240 deletions(-) diff --git a/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java b/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java index d563f8d..88124d4 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java @@ -30,7 +30,7 @@ import lombok.ToString; public class TransactionMaster extends Auditable { // 👇 JSON me "txn_id" aayega, ye DB me bank_txn_id column me save hoga - @Id + @JsonProperty("txn_id") @Column(name = "bank_txn_id", nullable = false, unique = true) private String bankTxnId; @@ -38,6 +38,15 @@ public class TransactionMaster extends Auditable { // 👇 Apna internal transaction id, manually generate or system ke through @Column(name = "txn_id") private String txnId; + + // 👇 New: txn id generated at the time of initiatePayment + @Id + @Column(name = "initiator_txn_id") + private String initiatorTxnId; + + // 👇 New: txn id generated during internal check transaction status + @Column(name = "checker_txn_id") + private String checkerTxnId; @Column(name = "client_user_id") private String clientUserId; diff --git a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java index 1faa4b7..00ae295 100644 --- a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java +++ b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java @@ -37,4 +37,7 @@ public interface TransactionMasterRepository extends JpaRepository findByStatusIn(List statuses); + // Find by initiatorTxnId so services can look up records created during initiatePayment + TransactionMaster findByInitiatorTxnId(String initiatorTxnId); + } \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java index 4549eef..2443c0b 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java @@ -13,6 +13,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import com.billpaymentservice.bbps.dto.GeneralResp; import com.billpaymentservice.bbps.dto.ResponseModel; +import com.billpaymentservice.bbps.dto.TransactionDTO; import com.billpaymentservice.bbps.dto.request.BillPaymentRequest; import com.billpaymentservice.bbps.dto.request.TransactionStatusRequest; import com.billpaymentservice.bbps.dto.request.TrasactionStatusByClientReqIdRequest; @@ -115,22 +116,23 @@ public class PaymentServiceImpl implements PaymentService { // save here should not prevent the outgoing request, so we catch and log errors. try { TransactionMaster preTransaction = new TransactionMaster(); - preTransaction.setBankTxnId("INITIATED_" + txnId); + preTransaction.setBankTxnId(txnId+"bank"); preTransaction.setTxnId(txnId); + preTransaction.setInitiatorTxnId(txnId); preTransaction.setReqId(requestId); preTransaction.setMid(billPaymentRequest.getMid()); preTransaction.setClientUserId(billPaymentRequest.getClientUserId()); preTransaction.setClientReqId(billPaymentRequest.getClientReqId()); preTransaction.setUserId(userId); - preTransaction.setStatus("INITIATED"); + preTransaction.setStatus("PENDING"); EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction); - saveTransactionMasterWithRetry(preTransaction); + // ensure txnId exists (no uniqueness suffixing) + preTransaction.setTxnId(ensureTxnId(preTransaction.getTxnId())); + transactionMasterRepository.save(preTransaction); LOGGER.info("Pre-request TransactionMaster saved with status=INITIATED for txnId={}", txnId); } catch (Exception e) { LOGGER.warn("Failed to persist pre-request TransactionMaster for txnId={}: {}", txnId, e.getMessage()); } - - try { LOGGER.info("Calling BBPS payment URL: {}", url); Map responseMap = httpService.postRequest(url, forwardjson, headers); String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); @@ -139,139 +141,47 @@ public class PaymentServiceImpl implements PaymentService { helper.updateOutboundLogResponse(outboundLog, responseData); LOGGER.debug("Outbound log response updated for txnId={}", txnId); - if (responseData == null || responseData.isEmpty()) { - helper.updateInboundLogFailure(inboundLog, BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, - BBPSConstant.FAILED_STR); - LOGGER.warn("Empty response received from BBPS for txnId={}", txnId); - // Persist a TransactionMaster placeholder so scheduler can pick it up later - TransactionMaster tm = new TransactionMaster(); - tm.setBankTxnId("PENDING_" + txnId); - tm.setTxnId(txnId); - tm.setReqId(requestId); - tm.setMid(billPaymentRequest.getMid()); - tm.setClientUserId(billPaymentRequest.getClientUserId()); - tm.setClientReqId(billPaymentRequest.getClientReqId()); - tm.setUserId(userId); - tm.setStatus("PENDING_UPSTREAM"); - transactionMasterRepository.save(tm); - LOGGER.info("TransactionMaster placeholder saved with status=PENDING_UPSTREAM for txnId={}", txnId); - return new ResponseModel(BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, "PENDING_UPSTREAM", 502, null); - } - Map dataMap = objectMapper.readValue(responseData, + Map dataMap; + try { + dataMap = objectMapper.readValue(responseData, new TypeReference>() { }); - if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERIC_KEY)) { - helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_STRUCTURE); - LOGGER.warn("Invalid response structure received for txnId={}", txnId); - // Save a TransactionMaster placeholder so scheduler/admin can reconcile later - TransactionMaster tm = new TransactionMaster(); - tm.setBankTxnId("INVALID_" + txnId); - tm.setTxnId(txnId); - tm.setReqId(requestId); - tm.setMid(billPaymentRequest.getMid()); - tm.setClientUserId(billPaymentRequest.getClientUserId()); - tm.setClientReqId(billPaymentRequest.getClientReqId()); - tm.setUserId(userId); - tm.setStatus("INVALID_RESPONSE_FROM_BBPS"); - transactionMasterRepository.save(tm); - LOGGER.info("TransactionMaster placeholder saved with status=INVALID_RESPONSE_FROM_BBPS for txnId={}", txnId); - return new ResponseModel(BBPSConstant.INVALID_STRUCTURE, "INVALID_RESPONSE_FROM_BBPS", 502, null); - } + + BillPaymentResponse billPaymentResponse = objectMapper.convertValue(dataMap, BillPaymentResponse.class); GeneralResp generalResponse = billPaymentResponse.getGenericResponse(); LOGGER.info("genric Response" + generalResponse); - if(generalResponse.getStatus().equalsIgnoreCase("Failed")) { - // Save TransactionMaster with failed status so it can be reconciled later - TransactionMaster tm = new TransactionMaster(); - tm.setBankTxnId(billPaymentResponse.getTxnId()); - tm.setTxnId(txnId); - tm.setReqId(requestId); - tm.setMid(billPaymentRequest.getMid()); - tm.setClientUserId(billPaymentRequest.getClientUserId()); - tm.setClientReqId(billPaymentRequest.getClientReqId()); - tm.setUserId(userId); - tm.setStatus(generalResponse.getStatus()); - // ensure internal txnId uniqueness and save - tm.setTxnId(makeUniqueTxnId(tm.getTxnId())); - saveTransactionMasterWithRetry(tm); - LOGGER.info("TransactionMaster saved with failed status={} for txnId={}", generalResponse.getStatus(), txnId); - return new ResponseModel(generalResponse.getMessage(), generalResponse.getStatus(), - Integer.parseInt(generalResponse.getStatusCode()), billPaymentResponse); - } - + BillPayment entity = modelMapper.map(billPaymentResponse, BillPayment.class); paymentRepository.save(entity); LOGGER.info("BillPayment entity saved successfully for txnId={}", txnId); + helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus()); - // Create and save TransactionMaster for this initiate flow so everything is stored in the same table - try { - TransactionMaster transactionMaster = new TransactionMaster(); - // bankTxnId is BBPS txn id (primary key) - transactionMaster.setBankTxnId(billPaymentResponse.getTxnId()); - // use internal txnId generated earlier for tracing - transactionMaster.setTxnId(txnId); - transactionMaster.setReqId(requestId); - transactionMaster.setMid(billPaymentRequest.getMid()); - transactionMaster.setClientUserId(billPaymentRequest.getClientUserId()); - transactionMaster.setClientReqId(billPaymentRequest.getClientReqId()); - transactionMaster.setUserId(userId); - transactionMaster.setBillerId(billPaymentResponse.getBillerId()); - transactionMaster.setTxnAmount(billPaymentResponse.getTxnAmount()); - transactionMaster.setStatus(billPaymentResponse.getStatus()); - EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); - // ensure internal txnId uniqueness and save (handles collisions) - transactionMaster.setTxnId(makeUniqueTxnId(transactionMaster.getTxnId())); - saveTransactionMasterWithRetry(transactionMaster); - LOGGER.info("TransactionMaster created during initiatePayment for internalTxnId={} bankTxnId={}", transactionMaster.getTxnId(), transactionMaster.getBankTxnId()); - } catch (Exception e) { - LOGGER.warn("Failed to persist TransactionMaster during initiatePayment for txnId={}: {}", txnId, e.getMessage()); - } TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); - transactionStatusRequest.setTxnId(billPaymentResponse.getTxnId()); - transactionStatusRequest.setReqId(requestId); - transactionStatusRequest.setMid(billPaymentRequest.getMid()); - transactionStatusRequest.setClientUserId(billPaymentRequest.getClientUserId()); - - try { - Thread.sleep((long) BBPSConstant.PAYMENT_STATUS_DELAY_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOGGER.warn("Thread interrupted during payment delay: {}", e.getMessage()); + transactionStatusRequest.setReqId(billPaymentRequest.getReqId()); + transactionStatusRequest.setTxnId(entity.getTxnId()); + ResponseModel responseModel = checkTrasactionStatus(transactionStatusRequest, inboundLog); + return responseModel; + } catch (JsonProcessingException e) { + helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); + throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); } - LOGGER.info("Checking transaction status for txnId={}", billPaymentResponse.getTxnId()); - LOGGER.info("[{}] END | reqId={} | txnId={}", apiName, requestId, txnId); - return checkTrasactionStatus(transactionStatusRequest, inboundLog); - - } catch (JsonProcessingException e) { - LOGGER.error("JSON Parsing Error in initiatePayment: {}", e.getMessage(), e); - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - // Persist TransactionMaster placeholder so admin/scheduler can reconcile later - TransactionMaster tm = new TransactionMaster(); - tm.setBankTxnId("INVALID_JSON_" + txnId); - tm.setTxnId(txnId); - tm.setUserId(billPaymentRequest.getClientReqId()); - tm.setReqId(billPaymentRequest.getReqId()); - tm.setMid(billPaymentRequest.getMid()); - tm.setClientUserId(billPaymentRequest.getClientUserId()); - tm.setClientReqId(billPaymentRequest.getClientReqId()); - tm.setStatus("INVALID_JSON_FROM_UPSTREAM"); - transactionMasterRepository.save(tm); - LOGGER.info("TransactionMaster saved with status=INVALID_JSON_FROM_UPSTREAM for txnId={}", txnId); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM, BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); - } - } + + + + } public ResponseModel checkTrasactionStatus(TransactionStatusRequest transactionStatusRequest, InboundTraceLog inboundLog) { String apiName = BBPSConstant.API_CHECK_TRANSACTION_STATUS; - String txnId = helper.generateTxnId(15); + String checkerTxnId = helper.generateTxnId(15); String requestId = transactionStatusRequest.getReqId(); // START log for internal transaction status check - LOGGER.info("[{}] START (internal) | checkTxnId={} | reqId={} | bankTxnId={}", apiName, txnId, requestId, + LOGGER.info("[{}] START (internal) | checkTxnId={} | reqId={} | bankTxnId={}", apiName, checkerTxnId, requestId, transactionStatusRequest.getTxnId()); String url = paymentUrl + BBPSConstant.CHECK_TRANSACTION_URL + transactionStatusRequest.getTxnId(); @@ -282,21 +192,20 @@ public class PaymentServiceImpl implements PaymentService { BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); HttpHeaders headers = helper.buildHeaders(headersData); - OutboundTraceLog outboundLog = helper.createOutboundLog(txnId, requestId, apiName, url, BBPSConstant.HTTP_GET, + OutboundTraceLog outboundLog = helper.createOutboundLog(checkerTxnId, requestId, apiName, url, BBPSConstant.HTTP_GET, null); - LOGGER.debug("Outbound log created for internal status check | txnId={} | reqId={}", txnId, requestId); + LOGGER.debug("Outbound log created for internal status check | txnId={} | reqId={}", checkerTxnId, requestId); Map responseMap = httpService.getRequest(url, headers); String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); - LOGGER.info("Transaction status response: {}", responseData); helper.updateOutboundLogResponse(outboundLog, responseData); - LOGGER.debug("Outbound log response updated for internal status check | txnId={}", txnId); + LOGGER.debug("Outbound log response updated for internal status check | txnId={}", checkerTxnId); try { Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); + new TypeReference>() { + }); if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); LOGGER.warn("Invalid transaction status response for txnId={}", transactionStatusRequest.getTxnId()); @@ -306,38 +215,83 @@ public class PaymentServiceImpl implements PaymentService { } TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); + TransactionStatusResponse.class); GeneralResp generalResp = responseWrapper.getGeneralResp(); - LOGGER.info("Transaction status received: {}", generalResp.getStatus()); - - TransactionMaster transactionMaster = modelMapper.map(responseWrapper.getTransactionDAO(), + // Map DTO -> Entity, but explicitly copy bankTxnId from DTO to entity to avoid mapping gaps + TransactionDTO txDao = responseWrapper.getTransactionDAO(); + + TransactionMaster transactionMaster = modelMapper.map(txDao, TransactionMaster.class); - transactionMaster.setTxnId(txnId); + LOGGER.info("INBOPUND TRANSACTION ID:{}",inboundLog.getTxnId()); + TransactionMaster + existingTransaction=transactionMasterRepository.findByInitiatorTxnId( + inboundLog.getTxnId()); + LOGGER.info("existingTransaction :{}",existingTransaction); + + if(existingTransaction != null) { + LOGGER.info("Existing TransactionMaster found for bankTxnId"); + // Merge incoming non-null fields into the existing DB entity so we preserve DB values + mergeNonNullFields(transactionMaster, existingTransaction); + transactionMaster = existingTransaction; + } + + // Ensure bankTxnId from DTO is set if provided + if (txDao != null && txDao.getBankTxnId() != null && !txDao.getBankTxnId().isBlank()) { + transactionMaster.setBankTxnId(txDao.getBankTxnId()); + } + + // do not overwrite existing txnId if present; set checker txn id when missing + if (transactionMaster.getTxnId() == null || transactionMaster.getTxnId().isBlank()) { + transactionMaster.setTxnId(checkerTxnId); + } + transactionMaster.setCheckerTxnId(checkerTxnId); transactionMaster.setReqId(transactionStatusRequest.getReqId()); transactionMaster.setMid(transactionStatusRequest.getMid()); transactionMaster.setClientUserId(transactionStatusRequest.getClientUserId()); EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); - // Ensure internal txnId is unique; if collisions occur append suffix (_1, _2...) - transactionMaster.setTxnId(makeUniqueTxnId(transactionMaster.getTxnId())); - saveTransactionMasterWithRetry(transactionMaster); - LOGGER.info("TransactionMaster saved successfully for txnId={}", txnId); + // Ensure internal txnId exists + transactionMaster.setTxnId(ensureTxnId(transactionMaster.getTxnId())); - AgentFundTransfer agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), - AgentFundTransfer.class); - agentFundTransfer.setTxnId(txnId); - agentFundTransfer.setReqId(transactionStatusRequest.getReqId()); - agentFundTransfer.setTransactionMasterId(transactionMaster.getBankTxnId()); - agentFundTransferRepository.save(agentFundTransfer); - LOGGER.info("AgentFundTransfer entry saved successfully for txnId={}", txnId); + // If bankTxnId (primary key) is missing, assign a placeholder so the entity can be saved + if (transactionMaster.getBankTxnId() == null || transactionMaster.getBankTxnId().isBlank()) { + String placeholderBankId = "GEN_" + transactionMaster.getTxnId(); + transactionMaster.setBankTxnId(placeholderBankId); + LOGGER.warn("bankTxnId missing in response; assigning placeholder bankTxnId={} for txnId={}", placeholderBankId, transactionMaster.getTxnId()); + } + + // Map and persist AgentFundTransfer if present + AgentFundTransfer agentFundTransfer = null; + try { + if (responseWrapper.getAgentFundTransfer() != null) { + agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), AgentFundTransfer.class); + } else { + agentFundTransfer = new AgentFundTransfer(); + } + agentFundTransfer.setTxnId(transactionMaster.getTxnId()); + agentFundTransfer.setReqId(transactionStatusRequest.getReqId()); + agentFundTransfer.setTransactionMasterId(transactionMaster.getBankTxnId()); + } catch (Exception e) { + LOGGER.debug("No AgentFundTransfer in response or mapping failed: {}", e.getMessage()); + } + + // Persist updated transaction master and agent fund transfer so DB reflects merged non-null fields + try { + transactionMasterRepository.save(transactionMaster); + if (agentFundTransfer != null) { agentFundTransferRepository.save(agentFundTransfer); } + LOGGER.info("Persisted TransactionMaster and AgentFundTransfer for txnId={} bankTxnId={}" + , transactionMaster.getTxnId(), transactionMaster.getBankTxnId()); + } catch (Exception e) { + LOGGER.warn("Failed to persist TransactionMaster/AgentFundTransfer for txnId={}: {}", transactionMaster.getTxnId(), e.getMessage(), e); + } ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), Integer.parseInt(generalResp.getStatusCode()), responseWrapper); helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); - LOGGER.info("[{}] END (internal) | checkTxnId={} | reqId={} | status={}", apiName, txnId, requestId, - generalResp.getStatus()); + LOGGER.info("[{}] END (internal) | checkTxnId={} | reqId={} | status={}", apiName, checkerTxnId, requestId, generalResp.getStatus()); return responseModel; + } catch (JsonProcessingException e) { LOGGER.error("Error parsing transaction status JSON: {}", e.getMessage(), e); helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); @@ -348,68 +302,141 @@ public class PaymentServiceImpl implements PaymentService { @Override public ResponseModel checkTrasactionStatus(TransactionStatusRequest transactionStatusRequest) { helper.validateMerchant(transactionStatusRequest.getMid()); - String txnID = helper.generateTxnId(15); - String apiName = BBPSConstant.API_CHECK_TRANSACTION_PUBLIC; - String requestId = transactionStatusRequest.getReqId(); - LOGGER.info("[{}] START (public) | reqId={} | mid={} | checkBankTxnId={}", apiName, requestId, - transactionStatusRequest.getMid(), transactionStatusRequest.getTxnId()); - String requestJson = helper.convertObjectToJson(transactionStatusRequest); - String url = paymentUrl + BBPSConstant.CHECK_TRANSACTION_URL + transactionStatusRequest.getTxnId(); + String txnID = helper.generateTxnId(15); + String apiName = BBPSConstant.API_CHECK_TRANSACTION_PUBLIC; + String requestId = transactionStatusRequest.getReqId(); + LOGGER.info("[{}] START (public) | reqId={} | mid={} | checkBankTxnId={}", apiName, requestId, + transactionStatusRequest.getMid(), transactionStatusRequest.getTxnId()); + String requestJson = helper.convertObjectToJson(transactionStatusRequest); + String url = paymentUrl + BBPSConstant.CHECK_TRANSACTION_URL + transactionStatusRequest.getTxnId(); - LOGGER.info("Checking transaction status (public) for txnId={} and reqId={}", - transactionStatusRequest.getTxnId(), requestId); + LOGGER.info("Checking transaction status (public) for txnId={} and reqId{}", + transactionStatusRequest.getTxnId(), requestId); - BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); - HttpHeaders headers = helper.buildHeaders(headersData); + // First, attempt to find an existing TransactionMaster by initiatorTxnId and update it + try { + String initiatorTxnId = transactionStatusRequest.getTxnId(); + if (initiatorTxnId != null && !initiatorTxnId.isBlank()) { + + } + } + catch (Exception e) { + // Log but continue to fallback behavior + LOGGER.warn("Error while updating by initiatorTxnId: {}", e.getMessage()); + } - InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnID, apiName, - transactionStatusRequest.getMid(), BBPSConstant.HTTP_POST, requestJson); - LOGGER.debug("Inbound log created for public status check | reqId={} | txnId={}", requestId, txnID); - OutboundTraceLog outboundLog = helper.createOutboundLog(txnID, requestId, apiName, url, BBPSConstant.HTTP_GET, - null); - LOGGER.debug("Outbound log created for public status check | reqId={} | txnId={}", requestId, txnID); + LOGGER.info("Falling back to external BBPS status check URL={}", url); - Map responseMap = httpService.getRequest(url, headers); - String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); - LOGGER.info("Transaction status response data: {}", responseData); - helper.updateOutboundLogResponse(outboundLog, responseData); - LOGGER.debug("Outbound log response updated for public status check | reqId={} | txnId={}", requestId, txnID); + BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); + HttpHeaders headers = helper.buildHeaders(headersData); - try { - Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); - if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { - helper.updateInboundLogFailure(inboundLog, responseData, - String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); - LOGGER.warn("Invalid transaction status response for txnId={} from BBPS", - transactionStatusRequest.getTxnId()); - throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), - Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), - String.valueOf(dataMap.get("remarks"))); - } + InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnID, apiName, + transactionStatusRequest.getMid(), BBPSConstant.HTTP_POST, requestJson); + LOGGER.debug("Inbound log created for public status check | reqId={} | txnId={}", requestId, txnID); + OutboundTraceLog outboundLog = helper.createOutboundLog(txnID, requestId, apiName, url, BBPSConstant.HTTP_GET, + null); + LOGGER.debug("Outbound log created for public status check | reqId={} | txnId={}", requestId, txnID); - TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); - GeneralResp generalResp = responseWrapper.getGeneralResp(); + Map responseMap = httpService.getRequest(url, headers); + String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); + LOGGER.info("Transaction status response data: {}", responseData); + helper.updateOutboundLogResponse(outboundLog, responseData); + LOGGER.debug("Outbound log response updated for public status check | reqId={} | txnId={}", requestId, txnID); - String status = generalResp.getStatus(); - LOGGER.info("Current transaction status: {}", status); + try { + Map dataMap = objectMapper.readValue(responseData, + new TypeReference>() { + }); + if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { + helper.updateInboundLogFailure(inboundLog, responseData, + String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); + LOGGER.warn("Invalid transaction status response for txnId={} from BBPS", + transactionStatusRequest.getTxnId()); + throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), + Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), + String.valueOf(dataMap.get("remarks"))); + } - ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), - Integer.parseInt(generalResp.getStatusCode()), responseWrapper); + TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, + TransactionStatusResponse.class); + GeneralResp generalResp = responseWrapper.getGeneralResp(); - helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); - LOGGER.info("[{}] END (public) | reqId={} | txnId={} | status={}", apiName, requestId, txnID, - generalResp.getStatus()); - return responseModel; + String status = generalResp.getStatus(); + LOGGER.info("Current transaction status: {}", status); - } catch (JsonProcessingException e) { - LOGGER.error("Error parsing responseData JSON: {}", e.getMessage(), e); - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); - } - } + // --- NEW: attempt to persist/update TransactionMaster so DB reflects new status/bankTxnId --- + try { + TransactionDTO txDao = responseWrapper.getTransactionDAO(); + TransactionMaster updatedEntity = null; + + // 1) Try to find by initiatorTxnId (client may pass initiator txn id) + if (transactionStatusRequest.getTxnId() != null && !transactionStatusRequest.getTxnId().isBlank()) { + TransactionMaster byInitiator = transactionMasterRepository.findByInitiatorTxnId(transactionStatusRequest.getTxnId()); + if (byInitiator != null) { + updatedEntity = byInitiator; + } + } + + // 2) If not found, try find by internal txnId (txn_id column) + if (updatedEntity == null && txDao != null && txDao.getBankTxnId() != null && !txDao.getBankTxnId().isBlank()) { + try { + java.util.List byTxnIdList = transactionMasterRepository.findByTxnId(txDao.getBankTxnId()); + if (byTxnIdList != null && !byTxnIdList.isEmpty()) { + updatedEntity = byTxnIdList.get(0); + } + } catch (Exception ignore) { + LOGGER.debug("Error while searching by txnId: {}", ignore.getMessage()); + } + } + + // 3) As a last attempt, if request passed an id that matches internal txnId column + if (updatedEntity == null && transactionStatusRequest.getTxnId() != null && !transactionStatusRequest.getTxnId().isBlank()) { + try { + java.util.List byTxnIdList2 = transactionMasterRepository.findByTxnId(transactionStatusRequest.getTxnId()); + if (byTxnIdList2 != null && !byTxnIdList2.isEmpty()) { + updatedEntity = byTxnIdList2.get(0); + } + } catch (Exception ignore) { + LOGGER.debug("Error while searching by txnId (fallback): {}", ignore.getMessage()); + } + } + + // If we found an entity, merge non-null fields from DTO and save + if (updatedEntity != null) { + TransactionMaster mapped = modelMapper.map(txDao, TransactionMaster.class); + mergeNonNullFields(mapped, updatedEntity); + // ensure internal txnId exists + updatedEntity.setTxnId(ensureTxnId(updatedEntity.getTxnId())); + EntityRelationshipUtil.linkTransactionMasterRelationships(updatedEntity); + try { + transactionMasterRepository.save(updatedEntity); + LOGGER.info("Updated TransactionMaster in DB for initiatorTxnId={} (bankTxnId={})", + updatedEntity.getInitiatorTxnId(), updatedEntity.getBankTxnId()); + } catch (Exception e) { + LOGGER.warn("Failed to persist updated TransactionMaster: {}", e.getMessage(), e); + } + } else { + LOGGER.debug("No matching TransactionMaster found to update for txnId={}", transactionStatusRequest.getTxnId()); + } + } catch (Exception e) { + LOGGER.warn("Error while attempting to persist transaction status update: {}", e.getMessage()); + } + // --- END NEW PERSISTENCE BLOCK --- + + ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), + Integer.parseInt(generalResp.getStatusCode()), responseWrapper); + + helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); + LOGGER.info("[{}] END (public) | reqId={} | txnId={} | status={}", apiName, requestId, txnID, + generalResp.getStatus()); + return responseModel; + + } catch (JsonProcessingException e) { + LOGGER.error("Error parsing responseData JSON: {}", e.getMessage(), e); + helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); + throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); + } + } @Override public ResponseModel checkTrasactionStatusByClientReqId( @@ -471,14 +498,14 @@ public class PaymentServiceImpl implements PaymentService { LOGGER.debug("Outbound log response updated for clientReqId check | reqId={} | txnId={}", requestId, txnID); Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); + new TypeReference>() { + }); if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); LOGGER.warn("Invalid transaction status response for clientReqId={} | Response={}", clientReqId, - responseData); + responseData); throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), @@ -486,7 +513,7 @@ public class PaymentServiceImpl implements PaymentService { } TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); + TransactionStatusResponse.class); GeneralResp generalResp = responseWrapper.getGeneralResp(); @@ -494,7 +521,7 @@ public class PaymentServiceImpl implements PaymentService { helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); LOGGER.info("[{}] END | reqId={} | clientReqId={} | status={}", apiName, requestId, clientReqId, - generalResp.getStatus()); + generalResp.getStatus()); return new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), Integer.parseInt(generalResp.getStatusCode()), responseWrapper); @@ -506,51 +533,78 @@ public class PaymentServiceImpl implements PaymentService { } /** - * Ensure the provided internal txnId is unique in bbps_transaction_master.txn_id. - * If a record already exists with the same txnId, append _1, _2, ... until unique. + * Return the provided txnId or generate a new one when the provided value is null/blank. + * This intentionally does NOT try to ensure database-level uniqueness (no suffixing or retries). */ - private String makeUniqueTxnId(String baseTxnId) { - if (baseTxnId == null || baseTxnId.isBlank()) return baseTxnId; - List existing = transactionMasterRepository.findByTxnId(baseTxnId); - if (existing == null || existing.isEmpty()) { - return baseTxnId; - } - int suffix = 1; - String candidate; - while (true) { - candidate = baseTxnId + "_" + suffix; - List coll = transactionMasterRepository.findByTxnId(candidate); - if (coll == null || coll.isEmpty()) { - return candidate; - } - suffix++; + private String ensureTxnId(String txnId) { + if (txnId == null || txnId.isBlank()) { + return helper.generateTxnId(15); } + return txnId; } /** - * Try to save TransactionMaster and handle unique txnId collisions that may occur under concurrency. - * This method will attempt to save up to maxAttempts; on duplicate collision it will append a numeric suffix. + * Merge non-null fields from src into dest. This keeps existing DB fields when the incoming + * payload doesn't provide them. Only non-null (and for Strings, non-blank) fields from src + * will overwrite dest fields. */ - private void saveTransactionMasterWithRetry(TransactionMaster transactionMaster) { - final int maxAttempts = 5; - String baseTxnId = transactionMaster.getTxnId(); - for (int attempt = 0; attempt < maxAttempts; attempt++) { - try { - if (attempt == 0) { - transactionMaster.setTxnId(makeUniqueTxnId(baseTxnId)); - } else { - transactionMaster.setTxnId(baseTxnId + "_" + attempt); - } - transactionMasterRepository.save(transactionMaster); - return; // success - } catch (org.springframework.dao.DataIntegrityViolationException dex) { - LOGGER.warn("Duplicate txnId detected when saving TransactionMaster (attempt {}) for baseTxnId={}: {}", attempt, baseTxnId, dex.getMessage()); - // retry with next suffix - } - } - // if we reach here, all attempts failed - LOGGER.error("Failed to save TransactionMaster after {} attempts for baseTxnId={}", maxAttempts, transactionMaster.getTxnId()); - throw new CustomException("Unable to save TransactionMaster due to txnId collisions", BBPSConstant.FAILED_STATUS, 500, "Duplicate txnId"); + private void mergeNonNullFields(TransactionMaster src, TransactionMaster dest) { + /* + * LOGGER.info("TransactionMaster src: {}", src); + */ // Only copy values that are non-null (and non-blank for Strings) + + if (src.getTxnId() != null && !src.getTxnId().isBlank()) dest.setTxnId(src.getTxnId()); + if (src.getInitiatorTxnId() != null && !src.getInitiatorTxnId().isBlank()) dest.setInitiatorTxnId(src.getInitiatorTxnId()); + if (src.getCheckerTxnId() != null && !src.getCheckerTxnId().isBlank()) dest.setCheckerTxnId(src.getCheckerTxnId()); + if (src.getReqId() != null && !src.getReqId().isBlank()) dest.setReqId(src.getReqId()); + if (src.getMid() != null && !src.getMid().isBlank()) dest.setMid(src.getMid()); + if (src.getClientUserId() != null && !src.getClientUserId().isBlank()) dest.setClientUserId(src.getClientUserId()); + if (src.getClientReqId() != null && !src.getClientReqId().isBlank()) dest.setClientReqId(src.getClientReqId()); + if (src.getUserId() != null && !src.getUserId().isBlank()) dest.setUserId(src.getUserId()); + if (src.getBillerId() != null && !src.getBillerId().isBlank()) dest.setBillerId(src.getBillerId()); + if (src.getTxnAmount() != null) dest.setTxnAmount(src.getTxnAmount()); + if (src.getStatus() != null && !src.getStatus().isBlank()) dest.setStatus(src.getStatus()); + if (src.getPaymentInformation() != null && !src.getPaymentInformation().isBlank()) dest.setPaymentInformation(src.getPaymentInformation()); + if (src.getCreationDateBank() != null) dest.setCreationDateBank(src.getCreationDateBank()); + if (src.getModifiedDateBank() != null) dest.setModifiedDateBank(src.getModifiedDateBank()); + if (src.getCreatedEpochDateBank() != null) dest.setCreatedEpochDateBank(src.getCreatedEpochDateBank()); + if (src.getModifiedEpochDateBank() != null) dest.setModifiedEpochDateBank(src.getModifiedEpochDateBank()); + + /* ---- additional fields ---- */ + if (src.getRefId() != null && !src.getRefId().isBlank()) dest.setRefId(src.getRefId()); + if (src.getPaymentRefId() != null && !src.getPaymentRefId().isBlank()) dest.setPaymentRefId(src.getPaymentRefId()); + if (src.getRemitterName() != null && !src.getRemitterName().isBlank()) dest.setRemitterName(src.getRemitterName()); + if (src.getOrderId() != null && !src.getOrderId().isBlank()) dest.setOrderId(src.getOrderId()); + if (src.getBillId() != null && !src.getBillId().isBlank()) dest.setBillId(src.getBillId()); + if (src.getUserType() != null && !src.getUserType().isBlank()) dest.setUserType(src.getUserType()); + if (src.getTotalTxnAmount() != null) dest.setTotalTxnAmount(src.getTotalTxnAmount()); + if (src.getBillerCategory() != null && !src.getBillerCategory().isBlank()) dest.setBillerCategory(src.getBillerCategory()); + if (src.getBillerName() != null && !src.getBillerName().isBlank()) dest.setBillerName(src.getBillerName()); + if (src.getOffUsPay() != null) dest.setOffUsPay(src.getOffUsPay()); + if (src.getSplitPay() != null) dest.setSplitPay(src.getSplitPay()); + if (src.getQuickPay() != null) dest.setQuickPay(src.getQuickPay()); + if (src.getPaymentMode() != null && !src.getPaymentMode().isBlank()) dest.setPaymentMode(src.getPaymentMode()); + if (src.getAgentPaymentInformation() != null && !src.getAgentPaymentInformation().isBlank()) dest.setAgentPaymentInformation(src.getAgentPaymentInformation()); + if (src.getCustConvFee() != null) dest.setCustConvFee(src.getCustConvFee()); + if (src.getCurrency() != null && !src.getCurrency().isBlank()) dest.setCurrency(src.getCurrency()); + if (src.getRequestedIp() != null && !src.getRequestedIp().isBlank()) dest.setRequestedIp(src.getRequestedIp()); + if (src.getCustomerMobNo() != null && !src.getCustomerMobNo().isBlank()) dest.setCustomerMobNo(src.getCustomerMobNo()); + if (src.getMacAddress() != null && !src.getMacAddress().isBlank()) dest.setMacAddress(src.getMacAddress()); + if (src.getSplitPayAmount() != null) dest.setSplitPayAmount(src.getSplitPayAmount()); + if (src.getCouCustConvFee() != null) dest.setCouCustConvFee(src.getCouCustConvFee()); + if (src.getSettlementVerification() != null) dest.setSettlementVerification(src.getSettlementVerification()); + if (src.getFundTxnType() != null && !src.getFundTxnType().isBlank()) dest.setFundTxnType(src.getFundTxnType()); + if (src.getReason() != null && !src.getReason().isBlank()) dest.setReason(src.getReason()); + if (src.getAmountTags() != null && !src.getAmountTags().isBlank()) dest.setAmountTags(src.getAmountTags()); + if (src.getPlanId() != null && !src.getPlanId().isBlank()) dest.setPlanId(src.getPlanId()); + if (src.getPlanType() != null && !src.getPlanType().isBlank()) dest.setPlanType(src.getPlanType()); + if (src.getTxnNpciId() != null && !src.getTxnNpciId().isBlank()) dest.setTxnNpciId(src.getTxnNpciId()); + + /* ---- lists (direct replace when provided) ---- */ + if (src.getDeviceBlockTags() != null && !src.getDeviceBlockTags().isEmpty()) dest.setDeviceBlockTags(src.getDeviceBlockTags()); + if (src.getCustomerParams() != null && !src.getCustomerParams().isEmpty()) dest.setCustomerParams(src.getCustomerParams()); + + LOGGER.info("TransactionMaster dest after merge: {}", dest); } } diff --git a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java index c151e3d..d8b5e09 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java @@ -51,8 +51,8 @@ public class BBPSConstant extends BBPS_RESPONSE_COD_CONSTANT { public static final String BILL_FETCH_BY_ID = "/couapp/bills/fetch/byId?billId="; public static final String BILL_FETCH_BY_CLIENT_REQ_ID = "/couapp/bills/fetch/byClientId?clientReqId="; public static final String BILL_VALIDATE_BY_CLIENT_REQ_ID = "/couapp/bills/validate/byClientId?clientReqId="; - public static final String PAYMENT_URL = "/agent/bills/paymhent"; - public static final String CHECK_TRANSACTION_URL = "/transactions/payment/byTxnId?txnId=j"; + public static final String PAYMENT_URL = "/agent/bills/payment"; + public static final String CHECK_TRANSACTION_URL = "/transactions/payment/byTxnId?txnId="; public static final String CHECK_TRANSACTION_BY_CLIENT_URL = "/bills/fetch/byClientId?clientReqId="; public static final String CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL = "/transactions/payment/byClientId?clientReqId="; From f752ed269f8a22ec46273253205f37b5426dec09 Mon Sep 17 00:00:00 2001 From: Lincpay Tech Date: Thu, 18 Dec 2025 21:57:56 +0530 Subject: [PATCH 3/4] improved the logging and corrected payement start record --- .../bbps/config/RateLimitingFilter.java | 116 ---- .../bbps/entites/PaymentStartRecord.java | 4 +- .../bbps/entites/TransactionMaster.java | 16 +- .../TransactionMasterRepository.java | 5 +- .../bbps/serviceImpl/AgentServiceImpl.java | 7 - .../bbps/serviceImpl/ComplainServiceImpl.java | 2 +- .../bbps/serviceImpl/HttpService.java | 5 - .../bbps/serviceImpl/PaymentServiceImpl.java | 540 ++++++------------ .../serviceImpl/PaymentStartServiceImpl.java | 172 +++--- .../bbps/serviceImpl/PollBackServiceImpl.java | 1 - .../serviceImpl/TxnHistoryServiceImpl.java | 1 - .../bbps/util/BBPSConstant.java | 1 - .../bbps/util/BillerMapperHelper.java | 96 +++- .../bbps/util/EncryptionUtil.java | 1 - .../billpaymentservice/bbps/util/Helper.java | 59 ++ 15 files changed, 417 insertions(+), 609 deletions(-) delete mode 100644 src/main/java/com/billpaymentservice/bbps/config/RateLimitingFilter.java diff --git a/src/main/java/com/billpaymentservice/bbps/config/RateLimitingFilter.java b/src/main/java/com/billpaymentservice/bbps/config/RateLimitingFilter.java deleted file mode 100644 index 14ff370..0000000 --- a/src/main/java/com/billpaymentservice/bbps/config/RateLimitingFilter.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.billpaymentservice.bbps.config; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import com.billpaymentservice.bbps.util.BBPSConstant; -import com.billpaymentservice.bbps.dto.ResponseModel; - -import io.github.resilience4j.ratelimiter.RateLimiter; -import io.github.resilience4j.ratelimiter.RateLimiterRegistry; -import com.fasterxml.jackson.databind.ObjectMapper; - -@Component -@Order(1) -public class RateLimitingFilter extends OncePerRequestFilter { - - private final RateLimiterRegistry rateLimiterRegistry; - private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitingFilter.class); - - // Har MID ke liye alag limiter - private final Map limiterCache = new ConcurrentHashMap<>(); - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - public RateLimitingFilter(RateLimiterRegistry rateLimiterRegistry) { - this.rateLimiterRegistry = rateLimiterRegistry; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) - throws ServletException, IOException { - String mid = request.getHeader("mid"); - if(mid == null) { - mid = "GLOBAL"; - } - - // Validate MID - if (mid == null || mid.isBlank()) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.setContentType("application/json"); - - ResponseModel apiResponse = new ResponseModel<>("mid cannot be null or blank", "FAILURE", HttpServletResponse.SC_BAD_REQUEST, null); - response.getWriter().write(OBJECT_MAPPER.writeValueAsString(apiResponse)); - return; - } - - try { - RateLimiter rateLimiter = limiterCache.computeIfAbsent( - mid, - key -> rateLimiterRegistry.rateLimiter("default") - ); - - // Rate limit check - if (!rateLimiter.acquirePermission()) { - - // Prefer existing constant if it maps to 429, otherwise fallback to literal 429 - int tooManyRequestsStatus =429; - - response.setStatus(tooManyRequestsStatus); - response.setContentType("application/json"); - - ResponseModel apiResponse = new ResponseModel<>("Rate limit exceeded for MID: " + mid, "FAILURE", tooManyRequestsStatus, null); - response.getWriter().write(OBJECT_MAPPER.writeValueAsString(apiResponse)); - return; - } - - // Proceed with the chain and catch downstream exceptions to return consistent model - try { - filterChain.doFilter(request, response); - } catch (Exception ex) { - LOGGER.error("Unhandled exception in filter chain for MID: {}", mid, ex); - if (!response.isCommitted()) { - response.reset(); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.setContentType("application/json"); - ResponseModel apiResponse = new ResponseModel<>("Internal server error", "FAILURE", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); - response.getWriter().write(OBJECT_MAPPER.writeValueAsString(apiResponse)); - } - } - - } catch (Exception e) { - LOGGER.error("Error while rate limiting request", e); - if (!response.isCommitted()) { - response.reset(); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.setContentType("application/json"); - ResponseModel apiResponse = new ResponseModel<>("Internal server error", "FAILURE", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); - response.getWriter().write(OBJECT_MAPPER.writeValueAsString(apiResponse)); - } - return; - } - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - - String path = request.getRequestURI(); - - return path.startsWith("/swagger") - || path.startsWith("/v3/api-docs") - || path.startsWith("/actuator"); - } -} \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java b/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java index a809a87..0e11856 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java @@ -26,7 +26,7 @@ import jakarta.persistence.FetchType; public class PaymentStartRecord extends Auditable { - @Id + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -47,7 +47,7 @@ public class PaymentStartRecord extends Auditable { @Column(name = "amount", nullable = false) private String amount; - @Column(name = "txn_id", unique = true, nullable = false) + @Column(name = "txn_id") private String txnId; @Column(name = "qr_string", columnDefinition = "TEXT") diff --git a/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java b/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java index 88124d4..77a3a22 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/TransactionMaster.java @@ -9,6 +9,8 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; @@ -28,23 +30,21 @@ import lombok.ToString; @NoArgsConstructor @AllArgsConstructor public class TransactionMaster extends Auditable { - - // 👇 JSON me "txn_id" aayega, ye DB me bank_txn_id column me save hoga - + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + @JsonProperty("txn_id") - @Column(name = "bank_txn_id", nullable = false, unique = true) + @Column(name = "bank_txn_id",unique = true) private String bankTxnId; - // 👇 Apna internal transaction id, manually generate or system ke through @Column(name = "txn_id") private String txnId; - // 👇 New: txn id generated at the time of initiatePayment - @Id @Column(name = "initiator_txn_id") private String initiatorTxnId; - // 👇 New: txn id generated during internal check transaction status @Column(name = "checker_txn_id") private String checkerTxnId; diff --git a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java index 00ae295..cc7f68d 100644 --- a/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java +++ b/src/main/java/com/billpaymentservice/bbps/repository/TransactionMasterRepository.java @@ -1,6 +1,7 @@ package com.billpaymentservice.bbps.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,7 +11,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import com.billpaymentservice.bbps.entites.TransactionMaster; -public interface TransactionMasterRepository extends JpaRepository { +public interface TransactionMasterRepository extends JpaRepository { // @Query("SELECT t FROM TransactionMaster t WHERE t.mid = :mid OR t.clientUserId = :clientUserId") // List findByMidOrClientUserId(@Param("mid") String mid, @@ -40,4 +41,6 @@ public interface TransactionMasterRepository extends JpaRepository findByBankTxnId(String txnId); + } \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/AgentServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/AgentServiceImpl.java index ee8e573..35ecdeb 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/AgentServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/AgentServiceImpl.java @@ -468,14 +468,7 @@ public class AgentServiceImpl implements AgentService { throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); } } - // ================================== Bill fetch and Bill Validate by Client Req - // Id==================================// - // ======= - // ... remaining methods unchanged except where headers/objectMapper were - // previously created (they now use buildHeaders() and the class-level - // objectMapper) - // >>>>>>>d0812656ff2aee80b1eb0c0ab8e29a01d7d66553 @Override public ResponseModel fetchBillValidateByClientReqId(BillValidateByClientIdRequest billValidateByClientIdRequest) { diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/ComplainServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/ComplainServiceImpl.java index b5d1926..c1a5037 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/ComplainServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/ComplainServiceImpl.java @@ -126,7 +126,7 @@ public class ComplainServiceImpl implements ComplainService { try { - Optional dataById = transactionMasterRepository.findById(complainRequestDTO.getTxnId()); + Optional dataById = transactionMasterRepository.findByBankTxnId(complainRequestDTO.getTxnId()); if (dataById == null || dataById.isEmpty()) { LOGGER.warn("Txn Data Not Found This Txn Id is null"); throw new CustomException(BBPSConstant.USER_Txn_IS_NOT_FOUND, "FAILED", BBPSConstant.BADREQUEST, null); diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/HttpService.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/HttpService.java index 32aa1a7..75dd8cb 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/HttpService.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/HttpService.java @@ -181,20 +181,15 @@ public class HttpService { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - log.info("🌐 Sending FORM POST to: {}", url); log.info("📦 Payload: {}", formBody); - ResponseEntity restResponse = restClient.post() .uri(url) .headers(h -> h.addAll(headers)) .body(formBody) .retrieve() .toEntity(String.class); - response.put("respCode", String.valueOf(restResponse.getStatusCode().value())); - - // HTML RESPONSE directly stored response.put("data", restResponse.getBody()); log.info("✅ FORM POST Response Status: {}", restResponse.getStatusCode()); diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java index 2443c0b..21efaf1 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentServiceImpl.java @@ -105,75 +105,68 @@ public class PaymentServiceImpl implements PaymentService { String requestJson = helper.convertObjectToJson(billPaymentRequest); InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnId, apiName, billPaymentRequest.getMid(), - BBPSConstant.HTTP_POST, requestJson); + BBPSConstant.HTTP_POST, requestJson); LOGGER.debug("Inbound log created for api={} | reqId={} | txnId={}", apiName, requestId, txnId); OutboundTraceLog outboundLog = helper.createOutboundLog(txnId, requestId, apiName, url, BBPSConstant.HTTP_POST, - forwardjson); + forwardjson); LOGGER.debug("Outbound log created for api={} | reqId={} | txnId={}", apiName, requestId, txnId); - // Persist a TransactionMaster BEFORE making the external call so we have a DB record - // even if the HTTP call fails later. This is intentionally tolerant: failure to - // save here should not prevent the outgoing request, so we catch and log errors. - try { - TransactionMaster preTransaction = new TransactionMaster(); - preTransaction.setBankTxnId(txnId+"bank"); - preTransaction.setTxnId(txnId); - preTransaction.setInitiatorTxnId(txnId); - preTransaction.setReqId(requestId); - preTransaction.setMid(billPaymentRequest.getMid()); - preTransaction.setClientUserId(billPaymentRequest.getClientUserId()); - preTransaction.setClientReqId(billPaymentRequest.getClientReqId()); - preTransaction.setUserId(userId); - preTransaction.setStatus("PENDING"); - EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction); - // ensure txnId exists (no uniqueness suffixing) - preTransaction.setTxnId(ensureTxnId(preTransaction.getTxnId())); - transactionMasterRepository.save(preTransaction); - LOGGER.info("Pre-request TransactionMaster saved with status=INITIATED for txnId={}", txnId); - } catch (Exception e) { - LOGGER.warn("Failed to persist pre-request TransactionMaster for txnId={}: {}", txnId, e.getMessage()); - } - LOGGER.info("Calling BBPS payment URL: {}", url); - Map responseMap = httpService.postRequest(url, forwardjson, headers); - String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); - LOGGER.info("Payment API Response: {}", responseData); + TransactionMaster preTransaction=null; - helper.updateOutboundLogResponse(outboundLog, responseData); - LOGGER.debug("Outbound log response updated for txnId={}", txnId); + preTransaction = new TransactionMaster(); + preTransaction.setTxnId(txnId); + preTransaction.setInitiatorTxnId(txnId); + preTransaction.setReqId(requestId); + preTransaction.setMid(billPaymentRequest.getMid()); + preTransaction.setClientUserId(billPaymentRequest.getClientUserId()); + preTransaction.setClientReqId(billPaymentRequest.getClientReqId()); + preTransaction.setUserId(userId); + preTransaction.setStatus("PENDING"); + EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction); + // ensure txnId exists (no uniqueness suffixing) + preTransaction.setTxnId(ensureTxnId(preTransaction.getTxnId())); + transactionMasterRepository.save(preTransaction); + LOGGER.info("Pre-request TransactionMaster saved with status=INITIATED for txnId={}", txnId); - - Map dataMap; - try { - dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); - + LOGGER.info("Calling BBPS payment URL: {}", url); + Map responseMap = httpService.postRequest(url, forwardjson, headers); - BillPaymentResponse billPaymentResponse = objectMapper.convertValue(dataMap, BillPaymentResponse.class); - - GeneralResp generalResponse = billPaymentResponse.getGenericResponse(); - LOGGER.info("genric Response" + generalResponse); - - - BillPayment entity = modelMapper.map(billPaymentResponse, BillPayment.class); - paymentRepository.save(entity); - LOGGER.info("BillPayment entity saved successfully for txnId={}", txnId); - - - helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus()); - TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); - transactionStatusRequest.setReqId(billPaymentRequest.getReqId()); - transactionStatusRequest.setTxnId(entity.getTxnId()); - ResponseModel responseModel = checkTrasactionStatus(transactionStatusRequest, inboundLog); - return responseModel; - } catch (JsonProcessingException e) { - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); - } - - + String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); + LOGGER.info("Payment API Response: {}", responseData); - } + + helper.updateOutboundLogResponse(outboundLog, responseData); + LOGGER.debug("Outbound log response updated for txnId={}", txnId); + + + Map dataMap = parseJsonToMap(responseData, inboundLog); + + BillPaymentResponse billPaymentResponse = objectMapper.convertValue(dataMap, BillPaymentResponse.class); + + GeneralResp generalResponse = billPaymentResponse.getGenericResponse(); + LOGGER.info("genric Response" + generalResponse); + + + BillPayment entity = modelMapper.map(billPaymentResponse, BillPayment.class); + paymentRepository.save(entity); + LOGGER.info("BillPayment entity saved successfully for txnId={}", txnId); + + + helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus()); + + preTransaction.setBankTxnId(entity.getTxnId()); + transactionMasterRepository.save(preTransaction); + LOGGER.info("updated TransactionMaster with bankTxnId for txnId={}", txnId); + + + TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); + transactionStatusRequest.setReqId(billPaymentRequest.getReqId()); + transactionStatusRequest.setTxnId(entity.getTxnId()); + ResponseModel responseModel = checkTrasactionStatus(transactionStatusRequest, inboundLog); + return responseModel; + + + } public ResponseModel checkTrasactionStatus(TransactionStatusRequest transactionStatusRequest, InboundTraceLog inboundLog) { @@ -202,101 +195,59 @@ public class PaymentServiceImpl implements PaymentService { helper.updateOutboundLogResponse(outboundLog, responseData); LOGGER.debug("Outbound log response updated for internal status check | txnId={}", checkerTxnId); - try { - Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); - if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { - helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); - LOGGER.warn("Invalid transaction status response for txnId={}", transactionStatusRequest.getTxnId()); - throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), - Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), - String.valueOf(dataMap.get("remarks"))); - } - - TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); - GeneralResp generalResp = responseWrapper.getGeneralResp(); - // Map DTO -> Entity, but explicitly copy bankTxnId from DTO to entity to avoid mapping gaps - TransactionDTO txDao = responseWrapper.getTransactionDAO(); - - TransactionMaster transactionMaster = modelMapper.map(txDao, - TransactionMaster.class); - LOGGER.info("INBOPUND TRANSACTION ID:{}",inboundLog.getTxnId()); - TransactionMaster - existingTransaction=transactionMasterRepository.findByInitiatorTxnId( - inboundLog.getTxnId()); - LOGGER.info("existingTransaction :{}",existingTransaction); - - if(existingTransaction != null) { - LOGGER.info("Existing TransactionMaster found for bankTxnId"); - // Merge incoming non-null fields into the existing DB entity so we preserve DB values - mergeNonNullFields(transactionMaster, existingTransaction); - transactionMaster = existingTransaction; - } - - // Ensure bankTxnId from DTO is set if provided - if (txDao != null && txDao.getBankTxnId() != null && !txDao.getBankTxnId().isBlank()) { - transactionMaster.setBankTxnId(txDao.getBankTxnId()); - } - - // do not overwrite existing txnId if present; set checker txn id when missing - if (transactionMaster.getTxnId() == null || transactionMaster.getTxnId().isBlank()) { - transactionMaster.setTxnId(checkerTxnId); - } - transactionMaster.setCheckerTxnId(checkerTxnId); - transactionMaster.setReqId(transactionStatusRequest.getReqId()); - transactionMaster.setMid(transactionStatusRequest.getMid()); - transactionMaster.setClientUserId(transactionStatusRequest.getClientUserId()); - EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); - // Ensure internal txnId exists - transactionMaster.setTxnId(ensureTxnId(transactionMaster.getTxnId())); - - // If bankTxnId (primary key) is missing, assign a placeholder so the entity can be saved - if (transactionMaster.getBankTxnId() == null || transactionMaster.getBankTxnId().isBlank()) { - String placeholderBankId = "GEN_" + transactionMaster.getTxnId(); - transactionMaster.setBankTxnId(placeholderBankId); - LOGGER.warn("bankTxnId missing in response; assigning placeholder bankTxnId={} for txnId={}", placeholderBankId, transactionMaster.getTxnId()); - } - - // Map and persist AgentFundTransfer if present - AgentFundTransfer agentFundTransfer = null; - try { - if (responseWrapper.getAgentFundTransfer() != null) { - agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), AgentFundTransfer.class); - } else { - agentFundTransfer = new AgentFundTransfer(); - } - agentFundTransfer.setTxnId(transactionMaster.getTxnId()); - agentFundTransfer.setReqId(transactionStatusRequest.getReqId()); - agentFundTransfer.setTransactionMasterId(transactionMaster.getBankTxnId()); - } catch (Exception e) { - LOGGER.debug("No AgentFundTransfer in response or mapping failed: {}", e.getMessage()); - } - - // Persist updated transaction master and agent fund transfer so DB reflects merged non-null fields - try { - transactionMasterRepository.save(transactionMaster); - if (agentFundTransfer != null) { agentFundTransferRepository.save(agentFundTransfer); } - LOGGER.info("Persisted TransactionMaster and AgentFundTransfer for txnId={} bankTxnId={}" - , transactionMaster.getTxnId(), transactionMaster.getBankTxnId()); - } catch (Exception e) { - LOGGER.warn("Failed to persist TransactionMaster/AgentFundTransfer for txnId={}: {}", transactionMaster.getTxnId(), e.getMessage(), e); - } - - ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), - Integer.parseInt(generalResp.getStatusCode()), responseWrapper); - - helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); - LOGGER.info("[{}] END (internal) | checkTxnId={} | reqId={} | status={}", apiName, checkerTxnId, requestId, generalResp.getStatus()); - return responseModel; - - - } catch (JsonProcessingException e) { - LOGGER.error("Error parsing transaction status JSON: {}", e.getMessage(), e); - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM, BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); + Map dataMap = parseJsonToMap(responseData, inboundLog); + if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { + helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); + LOGGER.warn("Invalid transaction status response for txnId={}", transactionStatusRequest.getTxnId()); + throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), + Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), + String.valueOf(dataMap.get("remarks"))); } + + TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, + TransactionStatusResponse.class); + GeneralResp generalResp = responseWrapper.getGeneralResp(); + TransactionDTO txDao = responseWrapper.getTransactionDAO(); + + TransactionMaster transactionMaster = modelMapper.map(txDao, + TransactionMaster.class); + + LOGGER.info("INBOUND TRANSACTION ID:{}",inboundLog.getTxnId()); + TransactionMaster + existingTransaction=transactionMasterRepository.findByInitiatorTxnId( + inboundLog.getTxnId()); + LOGGER.info("existingTransaction :{}",existingTransaction); + if(existingTransaction != null) { + LOGGER.info("Existing TransactionMaster found for bankTxnId"); + // Merge incoming non-null fields into the existing DB entity so we preserve DB values + helper.mergeNonNullFields(transactionMaster, existingTransaction); + transactionMaster = existingTransaction; + } + transactionMaster.setCheckerTxnId(checkerTxnId); + EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); + AgentFundTransfer agentFundTransfer = null; + if (responseWrapper.getAgentFundTransfer() != null) { + agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), AgentFundTransfer.class); + } else { + agentFundTransfer = new AgentFundTransfer(); + } + agentFundTransfer.setTxnId(transactionMaster.getTxnId()); + agentFundTransfer.setReqId(transactionStatusRequest.getReqId()); + agentFundTransfer.setTransactionMasterId(transactionMaster.getBankTxnId()); + + transactionMasterRepository.save(transactionMaster); + if (agentFundTransfer != null) { agentFundTransferRepository.save(agentFundTransfer); } + LOGGER.info("Persisted TransactionMaster and AgentFundTransfer for txnId={} bankTxnId={}" + , transactionMaster.getTxnId(), transactionMaster.getBankTxnId()); + + ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), + Integer.parseInt(generalResp.getStatusCode()), responseWrapper); + + helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); + LOGGER.info("[{}] END (internal) | checkTxnId={} | reqId={} | status={}", apiName, checkerTxnId, requestId, generalResp.getStatus()); + return responseModel; + + } @Override @@ -314,15 +265,10 @@ public class PaymentServiceImpl implements PaymentService { transactionStatusRequest.getTxnId(), requestId); // First, attempt to find an existing TransactionMaster by initiatorTxnId and update it - try { - String initiatorTxnId = transactionStatusRequest.getTxnId(); - if (initiatorTxnId != null && !initiatorTxnId.isBlank()) { - - } - } - catch (Exception e) { - // Log but continue to fallback behavior - LOGGER.warn("Error while updating by initiatorTxnId: {}", e.getMessage()); + String initiatorTxnId = transactionStatusRequest.getTxnId(); + if (initiatorTxnId != null && !initiatorTxnId.isBlank()) { + // logic intentionally left minimal here — any runtime exception should bubble + // to the global exception handler so failures are visible and handled centrally. } LOGGER.info("Falling back to external BBPS status check URL={}", url); @@ -343,99 +289,37 @@ public class PaymentServiceImpl implements PaymentService { helper.updateOutboundLogResponse(outboundLog, responseData); LOGGER.debug("Outbound log response updated for public status check | reqId={} | txnId={}", requestId, txnID); - try { - Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); - if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { - helper.updateInboundLogFailure(inboundLog, responseData, - String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); - LOGGER.warn("Invalid transaction status response for txnId={} from BBPS", - transactionStatusRequest.getTxnId()); - throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), - Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), - String.valueOf(dataMap.get("remarks"))); - } - - TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); - GeneralResp generalResp = responseWrapper.getGeneralResp(); - - String status = generalResp.getStatus(); - LOGGER.info("Current transaction status: {}", status); - - // --- NEW: attempt to persist/update TransactionMaster so DB reflects new status/bankTxnId --- - try { - TransactionDTO txDao = responseWrapper.getTransactionDAO(); - TransactionMaster updatedEntity = null; - - // 1) Try to find by initiatorTxnId (client may pass initiator txn id) - if (transactionStatusRequest.getTxnId() != null && !transactionStatusRequest.getTxnId().isBlank()) { - TransactionMaster byInitiator = transactionMasterRepository.findByInitiatorTxnId(transactionStatusRequest.getTxnId()); - if (byInitiator != null) { - updatedEntity = byInitiator; - } - } - - // 2) If not found, try find by internal txnId (txn_id column) - if (updatedEntity == null && txDao != null && txDao.getBankTxnId() != null && !txDao.getBankTxnId().isBlank()) { - try { - java.util.List byTxnIdList = transactionMasterRepository.findByTxnId(txDao.getBankTxnId()); - if (byTxnIdList != null && !byTxnIdList.isEmpty()) { - updatedEntity = byTxnIdList.get(0); - } - } catch (Exception ignore) { - LOGGER.debug("Error while searching by txnId: {}", ignore.getMessage()); - } - } - - // 3) As a last attempt, if request passed an id that matches internal txnId column - if (updatedEntity == null && transactionStatusRequest.getTxnId() != null && !transactionStatusRequest.getTxnId().isBlank()) { - try { - java.util.List byTxnIdList2 = transactionMasterRepository.findByTxnId(transactionStatusRequest.getTxnId()); - if (byTxnIdList2 != null && !byTxnIdList2.isEmpty()) { - updatedEntity = byTxnIdList2.get(0); - } - } catch (Exception ignore) { - LOGGER.debug("Error while searching by txnId (fallback): {}", ignore.getMessage()); - } - } - - // If we found an entity, merge non-null fields from DTO and save - if (updatedEntity != null) { - TransactionMaster mapped = modelMapper.map(txDao, TransactionMaster.class); - mergeNonNullFields(mapped, updatedEntity); - // ensure internal txnId exists - updatedEntity.setTxnId(ensureTxnId(updatedEntity.getTxnId())); - EntityRelationshipUtil.linkTransactionMasterRelationships(updatedEntity); - try { - transactionMasterRepository.save(updatedEntity); - LOGGER.info("Updated TransactionMaster in DB for initiatorTxnId={} (bankTxnId={})", - updatedEntity.getInitiatorTxnId(), updatedEntity.getBankTxnId()); - } catch (Exception e) { - LOGGER.warn("Failed to persist updated TransactionMaster: {}", e.getMessage(), e); - } - } else { - LOGGER.debug("No matching TransactionMaster found to update for txnId={}", transactionStatusRequest.getTxnId()); - } - } catch (Exception e) { - LOGGER.warn("Error while attempting to persist transaction status update: {}", e.getMessage()); - } - // --- END NEW PERSISTENCE BLOCK --- - - ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), - Integer.parseInt(generalResp.getStatusCode()), responseWrapper); - - helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); - LOGGER.info("[{}] END (public) | reqId={} | txnId={} | status={}", apiName, requestId, txnID, - generalResp.getStatus()); - return responseModel; - - } catch (JsonProcessingException e) { - LOGGER.error("Error parsing responseData JSON: {}", e.getMessage(), e); - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); + Map dataMap = parseJsonToMap(responseData, inboundLog); + if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { + helper.updateInboundLogFailure(inboundLog, responseData, + String.valueOf(dataMap.get(BBPSConstant.RESPONSE_STATUS_KEY))); + LOGGER.warn("Invalid transaction status response for txnId={} from BBPS", + transactionStatusRequest.getTxnId()); + throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), + Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), + String.valueOf(dataMap.get("remarks"))); } + + TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, + TransactionStatusResponse.class); + GeneralResp generalResp = responseWrapper.getGeneralResp(); + + String status = generalResp.getStatus(); + LOGGER.info("Current transaction status: {}", status); + + // --- NEW: attempt to persist/update TransactionMaster so DB reflects new status/bankTxnId --- + TransactionDTO txDao = responseWrapper.getTransactionDAO(); + TransactionMaster updatedEntity = null; + + + ResponseModel responseModel = new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), + Integer.parseInt(generalResp.getStatusCode()), responseWrapper); + + helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); + LOGGER.info("[{}] END (public) | reqId={} | txnId={} | status={}", apiName, requestId, txnID, + generalResp.getStatus()); + return responseModel; + } @Override @@ -480,62 +364,50 @@ public class PaymentServiceImpl implements PaymentService { HttpHeaders headers = helper.buildHeaders(headersData); InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnID, apiName, - transactionStatusRequest.getMid(), BBPSConstant.HTTP_POST, requestJson); + transactionStatusRequest.getMid(), BBPSConstant.HTTP_POST, requestJson); LOGGER.debug("Inbound log created for clientReqId check | reqId={} | txnId={}", requestId, txnID); OutboundTraceLog outboundLog = helper.createOutboundLog(txnID, requestId, apiName, url, BBPSConstant.HTTP_GET, - null); + null); LOGGER.debug("Outbound log created for clientReqId check | reqId={} | txnId={}", requestId, txnID); - try { - LOGGER.info("Calling BBPS Transaction Status API URL={}", url); - Map responseMap = httpService.getRequest(url, headers); + LOGGER.info("Calling BBPS Transaction Status API URL={}", url); + Map responseMap = httpService.getRequest(url, headers); - String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); - LOGGER.info("Transaction Status Response: {}", responseData); + String responseData = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); + LOGGER.info("Transaction Status Response: {}", responseData); - helper.updateOutboundLogResponse(outboundLog, responseData); - LOGGER.debug("Outbound log response updated for clientReqId check | reqId={} | txnId={}", requestId, txnID); + helper.updateOutboundLogResponse(outboundLog, responseData); + LOGGER.debug("Outbound log response updated for clientReqId check | reqId={} | txnId={}", requestId, txnID); - Map dataMap = objectMapper.readValue(responseData, - new TypeReference>() { - }); + Map dataMap = parseJsonToMap(responseData, inboundLog); - if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { - helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); + if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { + helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_RESPONSE); - LOGGER.warn("Invalid transaction status response for clientReqId={} | Response={}", clientReqId, - responseData); + LOGGER.warn("Invalid transaction status response for clientReqId={} | Response={}", clientReqId, + responseData); - throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), - Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), - String.valueOf(dataMap.get("remarks"))); - } - - TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, - TransactionStatusResponse.class); - - GeneralResp generalResp = responseWrapper.getGeneralResp(); - - LOGGER.info("Transaction Status for clientReqId={} => {}", clientReqId, generalResp.getStatus()); - - helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); - LOGGER.info("[{}] END | reqId={} | clientReqId={} | status={}", apiName, requestId, clientReqId, - generalResp.getStatus()); - return new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), - Integer.parseInt(generalResp.getStatusCode()), responseWrapper); - - } catch (JsonProcessingException e) { - LOGGER.error("Error parsing JSON in transaction status API: {}", e.getMessage(), e); - helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); - throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM,BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); + throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), + Integer.parseInt(String.valueOf(dataMap.get("statusCode"))), + String.valueOf(dataMap.get("remarks"))); } + + TransactionStatusResponse responseWrapper = objectMapper.convertValue(dataMap, + TransactionStatusResponse.class); + + GeneralResp generalResp = responseWrapper.getGeneralResp(); + + LOGGER.info("Transaction Status for clientReqId={} => {}", clientReqId, generalResp.getStatus()); + + helper.updateInboundLogSuccess(inboundLog, responseData, generalResp.getStatus()); + LOGGER.info("[{}] END | reqId={} | clientReqId={} | status={}", apiName, requestId, clientReqId, + generalResp.getStatus()); + + return new ResponseModel(generalResp.getMessage(), generalResp.getStatus(), + Integer.parseInt(generalResp.getStatusCode()), responseWrapper); } - /** - * Return the provided txnId or generate a new one when the provided value is null/blank. - * This intentionally does NOT try to ensure database-level uniqueness (no suffixing or retries). - */ private String ensureTxnId(String txnId) { if (txnId == null || txnId.isBlank()) { return helper.generateTxnId(15); @@ -543,68 +415,14 @@ public class PaymentServiceImpl implements PaymentService { return txnId; } - /** - * Merge non-null fields from src into dest. This keeps existing DB fields when the incoming - * payload doesn't provide them. Only non-null (and for Strings, non-blank) fields from src - * will overwrite dest fields. - */ - private void mergeNonNullFields(TransactionMaster src, TransactionMaster dest) { - /* - * LOGGER.info("TransactionMaster src: {}", src); - */ // Only copy values that are non-null (and non-blank for Strings) - - if (src.getTxnId() != null && !src.getTxnId().isBlank()) dest.setTxnId(src.getTxnId()); - if (src.getInitiatorTxnId() != null && !src.getInitiatorTxnId().isBlank()) dest.setInitiatorTxnId(src.getInitiatorTxnId()); - if (src.getCheckerTxnId() != null && !src.getCheckerTxnId().isBlank()) dest.setCheckerTxnId(src.getCheckerTxnId()); - if (src.getReqId() != null && !src.getReqId().isBlank()) dest.setReqId(src.getReqId()); - if (src.getMid() != null && !src.getMid().isBlank()) dest.setMid(src.getMid()); - if (src.getClientUserId() != null && !src.getClientUserId().isBlank()) dest.setClientUserId(src.getClientUserId()); - if (src.getClientReqId() != null && !src.getClientReqId().isBlank()) dest.setClientReqId(src.getClientReqId()); - if (src.getUserId() != null && !src.getUserId().isBlank()) dest.setUserId(src.getUserId()); - if (src.getBillerId() != null && !src.getBillerId().isBlank()) dest.setBillerId(src.getBillerId()); - if (src.getTxnAmount() != null) dest.setTxnAmount(src.getTxnAmount()); - if (src.getStatus() != null && !src.getStatus().isBlank()) dest.setStatus(src.getStatus()); - if (src.getPaymentInformation() != null && !src.getPaymentInformation().isBlank()) dest.setPaymentInformation(src.getPaymentInformation()); - if (src.getCreationDateBank() != null) dest.setCreationDateBank(src.getCreationDateBank()); - if (src.getModifiedDateBank() != null) dest.setModifiedDateBank(src.getModifiedDateBank()); - if (src.getCreatedEpochDateBank() != null) dest.setCreatedEpochDateBank(src.getCreatedEpochDateBank()); - if (src.getModifiedEpochDateBank() != null) dest.setModifiedEpochDateBank(src.getModifiedEpochDateBank()); - - /* ---- additional fields ---- */ - if (src.getRefId() != null && !src.getRefId().isBlank()) dest.setRefId(src.getRefId()); - if (src.getPaymentRefId() != null && !src.getPaymentRefId().isBlank()) dest.setPaymentRefId(src.getPaymentRefId()); - if (src.getRemitterName() != null && !src.getRemitterName().isBlank()) dest.setRemitterName(src.getRemitterName()); - if (src.getOrderId() != null && !src.getOrderId().isBlank()) dest.setOrderId(src.getOrderId()); - if (src.getBillId() != null && !src.getBillId().isBlank()) dest.setBillId(src.getBillId()); - if (src.getUserType() != null && !src.getUserType().isBlank()) dest.setUserType(src.getUserType()); - if (src.getTotalTxnAmount() != null) dest.setTotalTxnAmount(src.getTotalTxnAmount()); - if (src.getBillerCategory() != null && !src.getBillerCategory().isBlank()) dest.setBillerCategory(src.getBillerCategory()); - if (src.getBillerName() != null && !src.getBillerName().isBlank()) dest.setBillerName(src.getBillerName()); - if (src.getOffUsPay() != null) dest.setOffUsPay(src.getOffUsPay()); - if (src.getSplitPay() != null) dest.setSplitPay(src.getSplitPay()); - if (src.getQuickPay() != null) dest.setQuickPay(src.getQuickPay()); - if (src.getPaymentMode() != null && !src.getPaymentMode().isBlank()) dest.setPaymentMode(src.getPaymentMode()); - if (src.getAgentPaymentInformation() != null && !src.getAgentPaymentInformation().isBlank()) dest.setAgentPaymentInformation(src.getAgentPaymentInformation()); - if (src.getCustConvFee() != null) dest.setCustConvFee(src.getCustConvFee()); - if (src.getCurrency() != null && !src.getCurrency().isBlank()) dest.setCurrency(src.getCurrency()); - if (src.getRequestedIp() != null && !src.getRequestedIp().isBlank()) dest.setRequestedIp(src.getRequestedIp()); - if (src.getCustomerMobNo() != null && !src.getCustomerMobNo().isBlank()) dest.setCustomerMobNo(src.getCustomerMobNo()); - if (src.getMacAddress() != null && !src.getMacAddress().isBlank()) dest.setMacAddress(src.getMacAddress()); - if (src.getSplitPayAmount() != null) dest.setSplitPayAmount(src.getSplitPayAmount()); - if (src.getCouCustConvFee() != null) dest.setCouCustConvFee(src.getCouCustConvFee()); - if (src.getSettlementVerification() != null) dest.setSettlementVerification(src.getSettlementVerification()); - if (src.getFundTxnType() != null && !src.getFundTxnType().isBlank()) dest.setFundTxnType(src.getFundTxnType()); - if (src.getReason() != null && !src.getReason().isBlank()) dest.setReason(src.getReason()); - if (src.getAmountTags() != null && !src.getAmountTags().isBlank()) dest.setAmountTags(src.getAmountTags()); - if (src.getPlanId() != null && !src.getPlanId().isBlank()) dest.setPlanId(src.getPlanId()); - if (src.getPlanType() != null && !src.getPlanType().isBlank()) dest.setPlanType(src.getPlanType()); - if (src.getTxnNpciId() != null && !src.getTxnNpciId().isBlank()) dest.setTxnNpciId(src.getTxnNpciId()); - - /* ---- lists (direct replace when provided) ---- */ - if (src.getDeviceBlockTags() != null && !src.getDeviceBlockTags().isEmpty()) dest.setDeviceBlockTags(src.getDeviceBlockTags()); - if (src.getCustomerParams() != null && !src.getCustomerParams().isEmpty()) dest.setCustomerParams(src.getCustomerParams()); - - LOGGER.info("TransactionMaster dest after merge: {}", dest); + private Map parseJsonToMap(String responseData, InboundTraceLog inboundLog) { + try { + return objectMapper.readValue(responseData, new TypeReference>() {}); + } catch (JsonProcessingException e) { + LOGGER.error("Error parsing JSON response: {}", e.getMessage(), e); + helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); + throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_UPSTREAM, BBPSConstant.FAILED_STATUS, BBPSConstant.INVALID_JSON_STATUS_CODE, e.getMessage()); + } } -} +} \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java index 6ce0311..58a2c69 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java @@ -110,9 +110,13 @@ public class PaymentStartServiceImpl implements PaymentStartService { LOGGER.info("[startIntentPayment] outbound payload prepared txnId={} orderNo={}", txnId, orderNo); OutboundTraceLog outboundLog = helper.createOutboundLog(txnId, requestId, apiName, BBPSConstant.UAT_PAYMENT_URL_INTENT, BBPSConstant.HTTP_POST, forwardJson); + + PaymentStartRecord paymentRecord=bbpsMapperHelper.savePaymentStartRecord(request, null, txnId, orderNo, requestJson, null, null); + // call external API Map responseMap = httpService.postRequest(BBPSConstant.UAT_PAYMENT_URL_INTENT, forwardJson, null); + LOGGER.debug("[startIntentPayment] raw response map: {}", responseMap); int httpCode = Integer.parseInt(responseMap.getOrDefault(BBPSConstant.RESP_CODE_KEY, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR))); @@ -123,12 +127,15 @@ public class PaymentStartServiceImpl implements PaymentStartService { if (httpCode != BBPSConstant.HTTP_OK_INT) { helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(httpCode)); LOGGER.error("[startIntentPayment] upstream HTTP error txnId={} httpCode={} body={}", txnId, httpCode, respBody); + paymentRecord.setTxnStatus(BBPSConstant.FAILED_STR); + paymentStartRecordRepository.save(paymentRecord); throw new CustomException(BBPSConstant.MSG_UPSTREAM_HTTP_ERROR, BBPSConstant.FAILED_STR, httpCode, respBody); } if (respBody == null || respBody.isEmpty()) { helper.updateInboundLogFailure(inboundLog, BBPSConstant.MSG_EMPTY_RESPONSE_PROVIDER, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR)); LOGGER.error("[startIntentPayment] empty response txnId={}", txnId); + paymentRecord.setTxnStatus(BBPSConstant.FAILED_STR); throw new CustomException(BBPSConstant.MSG_EMPTY_RESPONSE_PROVIDER, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, null); } @@ -210,78 +217,83 @@ public class PaymentStartServiceImpl implements PaymentStartService { return sent; } + @Override public ResponseModel startWebPayment(PaymentStartUserRequest request) { - String requestId = request.getReqId(); - String apiName = BBPSConstant.API_START_PAYMENT; - String txnId = helper.generateTxnId(15); - LOGGER.info("[startWebPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); + String requestId = request.getReqId(); + String apiName = BBPSConstant.API_START_PAYMENT; + String txnId = helper.generateTxnId(15); + LOGGER.info("[startWebPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); - helper.validateMerchant(request.getMid()); + helper.validateMerchant(request.getMid()); - // inbound log - String requestJson = helper.convertObjectToJson(request); - InboundTraceLog inboundLog = + // inbound log + String requestJson = helper.convertObjectToJson(request); + InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnId, apiName, request.getMid(), BBPSConstant.HTTP_POST, requestJson); - LOGGER.debug("[startWebPayment] inbound log created txnId={} requestId={}", txnId, requestId); + LOGGER.debug("[startWebPayment] inbound log created txnId={} requestId={}", txnId, requestId); - // Prepare request payload - PaymentDetailSentRequest sent = createPaymentSentRequest(request); - String orderNo = "ORD" + helper.generateTxnId(12); - sent.setOrderNo(orderNo); + // Prepare request payload + PaymentDetailSentRequest sent = createPaymentSentRequest(request); + String orderNo = "ORD" + helper.generateTxnId(12); + sent.setOrderNo(orderNo); - String plaintext = helper.convertObjectToJson(sent); - String encKey = paymentEncryKey; - sent.setEnckey(encKey); + String plaintext = helper.convertObjectToJson(sent); + String encKey = paymentEncryKey; + sent.setEnckey(encKey); - LOGGER.debug("[startWebPayment] plaintext prepared txnId={} orderNo={}", txnId, orderNo); + LOGGER.debug("[startWebPayment] plaintext prepared txnId={} orderNo={}", txnId, orderNo); - String encrypted = encryptionUtil.encryptMData(plaintext, encKey); - LOGGER.info("[startWebPayment] payload encrypted txnId={} orderNo={}", txnId, orderNo); + String encrypted = encryptionUtil.encryptMData(plaintext, encKey); + LOGGER.info("[startWebPayment] payload encrypted txnId={} orderNo={}", txnId, orderNo); - // ----------- BUILD x-www-form-urlencoded PAYLOAD ----------- - String formPayload = - "mid=" + URLEncoder.encode(paymentMid, StandardCharsets.UTF_8) + - "&payload=" + URLEncoder.encode(encrypted, StandardCharsets.UTF_8); + // ----------- BUILD x-www-form-urlencoded PAYLOAD ----------- + String formPayload = + "mid=" + URLEncoder.encode(paymentMid, StandardCharsets.UTF_8) + + "&payload=" + URLEncoder.encode(encrypted, StandardCharsets.UTF_8); LOGGER.info("[startWebPayment] Form Payload: {}", formPayload); - OutboundTraceLog outboundLog = - helper.createOutboundLog(txnId, requestId, apiName, BBPSConstant.UAT_PAYMENT_URL_WEB, - BBPSConstant.HTTP_POST, formPayload); - LOGGER.debug("[startWebPayment] outbound log created txnId={} url={}", txnId, BBPSConstant.UAT_PAYMENT_URL_WEB); + OutboundTraceLog outboundLog = + helper.createOutboundLog(txnId, requestId, apiName, BBPSConstant.UAT_PAYMENT_URL_WEB, + BBPSConstant.HTTP_POST, formPayload); + LOGGER.debug("[startWebPayment] outbound log created txnId={} url={}", txnId, BBPSConstant.UAT_PAYMENT_URL_WEB); + PaymentStartRecord paymentRecord= bbpsMapperHelper.savePaymentStartRecord(request, null, txnId, orderNo, requestJson, null, null); - // ----------- CALL API (FORM URL-ENCODED) ----------- - Map responseMap = - httpService.postFormRequest(BBPSConstant.UAT_PAYMENT_URL_WEB, formPayload); + // ----------- CALL API (FORM URL-ENCODED) ----------- + Map responseMap = httpService.postFormRequest(BBPSConstant.UAT_PAYMENT_URL_WEB, formPayload); - LOGGER.info("[startWebPayment] Payment Start Response: {}", responseMap); + LOGGER.info("[startWebPayment] Payment Start Response: {}", responseMap); - // parse http code and response body using centralized keys - int httpCode = Integer.parseInt(responseMap.getOrDefault(BBPSConstant.RESP_CODE_KEY, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR))); - String respBody = responseMap.getOrDefault(BBPSConstant.RESPONSE_DATA_KEY, ""); - helper.updateOutboundLogResponse(outboundLog, respBody); - LOGGER.debug("[startWebPayment] updated outbound log response txnId={} httpCode={}", txnId, httpCode); + // parse http code and response body using centralized keys + int httpCode = Integer.parseInt(responseMap.getOrDefault(BBPSConstant.RESP_CODE_KEY, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR))); + String respBody = responseMap.getOrDefault(BBPSConstant.RESPONSE_DATA_KEY, ""); + helper.updateOutboundLogResponse(outboundLog, respBody); + LOGGER.debug("[startWebPayment] updated outbound log response txnId={} httpCode={}", txnId, httpCode); - // ---- HTTP FAILURE ---- - if (httpCode != BBPSConstant.HTTP_OK_INT) { - helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(httpCode)); - LOGGER.error("[startWebPayment] upstream HTTP error txnId={} httpCode={} body={}", txnId, httpCode, respBody); - throw new CustomException("Upstream HTTP Error", BBPSConstant.FAILED_STR, httpCode, respBody); - } + // ---- HTTP FAILURE ---- + if (httpCode != BBPSConstant.HTTP_OK_INT) { + helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(httpCode)); + LOGGER.error("[startWebPayment] upstream HTTP error txnId={} httpCode={} body={}", txnId, httpCode, respBody); + paymentRecord.setTxnStatus(BBPSConstant.FAILED_STR); + paymentStartRecordRepository.save(paymentRecord); + throw new CustomException("Upstream HTTP Error", BBPSConstant.FAILED_STR, httpCode, respBody); + } - // ---- EMPTY RESPONSE ---- - if (respBody == null || respBody.isEmpty()) { - helper.updateInboundLogFailure(inboundLog, BBPSConstant.MSG_EMPTY_RESPONSE_PG, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR)); - LOGGER.error("[startWebPayment] empty response from PG txnId={}", txnId); - throw new CustomException(BBPSConstant.MSG_EMPTY_RESPONSE_PG, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, null); - } + // ---- EMPTY RESPONSE ---- + if (respBody == null || respBody.isEmpty()) { + helper.updateInboundLogFailure(inboundLog, BBPSConstant.MSG_EMPTY_RESPONSE_PG, String.valueOf(BBPSConstant.HTTP_INTERNAL_SERVER_ERROR)); + LOGGER.error("[startWebPayment] empty response from PG txnId={}", txnId); + paymentRecord.setTxnStatus(BBPSConstant.FAILED_STR); + paymentStartRecordRepository.save(paymentRecord); + throw new CustomException(BBPSConstant.MSG_EMPTY_RESPONSE_PG, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, null); + } - // PG RETURNS HTML → NO JSON PARSING REQUIRED - helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(BBPSConstant.HTTP_OK_INT)); - LOGGER.info("[startWebPayment] received HTML response txnId={} length={}", txnId, respBody.length()); + // PG RETURNS HTML → NO JSON PARSING REQUIRED + helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(BBPSConstant.HTTP_OK_INT)); + LOGGER.info("[startWebPayment] received HTML response txnId={} length={}", txnId, respBody.length()); - // Save record with HTML (persist original request and HTML response) + // Save record with HTML (persist original request and HTML response) String responseTypeWeb = BBPSConstant.RESPONSE_TYPE_HTML; String rawResponseWeb = respBody == null ? "" : respBody; @@ -289,8 +301,8 @@ public class PaymentStartServiceImpl implements PaymentStartService { Map parsed = parseHtmlResponse(rawResponseWeb); // Build a minimal PaymentStartApiResponse so savePaymentStartRecord() can safely read fields - com.billpaymentservice.bbps.dto.response.PaymentStartApiResponse apiResp = new com.billpaymentservice.bbps.dto.response.PaymentStartApiResponse(); - com.billpaymentservice.bbps.dto.response.PaymentStartApiResponse.ResponseData rd = new com.billpaymentservice.bbps.dto.response.PaymentStartApiResponse.ResponseData(); + PaymentStartApiResponse apiResp = new PaymentStartApiResponse(); + PaymentStartApiResponse.ResponseData rd = new PaymentStartApiResponse.ResponseData(); String parsedTxn = parsed.getOrDefault("txnId", ""); String parsedAmount = parsed.getOrDefault("amount", "0.00"); @@ -351,14 +363,8 @@ public class PaymentStartServiceImpl implements PaymentStartService { LOGGER.info("[{}] Decrypted data for mid={}", apiName, mid); - // Parse decrypted JSON - Map payloadMap; - try { - payloadMap = objectMapper.readValue(decrypted, new TypeReference>(){}); - } catch (JsonProcessingException e) { - LOGGER.error("[{}] Failed to parse decrypted callback JSON: {}", apiName, e.getMessage(), e); - return new ResponseModel(BBPSConstant.MSG_INVALID_DECRYPTED_JSON, String.valueOf(BBPSConstant.STATUS_CODE_207), BBPSConstant.HTTP_OK_INT, null); - } + // Parse decrypted JSON (convert checked Jackson exception into unchecked CustomException so global handler can catch it) + Map payloadMap = parseJsonToMap(decrypted, apiName); String txnId = String.valueOf(payloadMap.getOrDefault("txnId", payloadMap.getOrDefault("txn_id", ""))); String orderNo = String.valueOf(payloadMap.getOrDefault("orderNo", payloadMap.getOrDefault("order_no", ""))); @@ -435,30 +441,44 @@ public class PaymentStartServiceImpl implements PaymentStartService { return new ResponseModel(BBPSConstant.MSG_CALLBACK_PROCESSED, merchantStatus, BBPSConstant.HTTP_OK_INT, null); } - public Map parseHtmlResponse(String html) { Map map = new HashMap<>(); - try { - Document doc = Jsoup.parse(html); + Document doc = Jsoup.parse(html); - Element txnId = doc.selectFirst("input[name=txnId]"); - Element amount = doc.selectFirst("input[name=amount]"); - Element mid = doc.selectFirst("input[name=mid]"); + Element txnId = doc.selectFirst("input[name=txnId]"); + Element amount = doc.selectFirst("input[name=amount]"); + Element mid = doc.selectFirst("input[name=mid]"); - if (txnId != null) map.put("txnId", txnId.attr("value")); - if (amount != null) map.put("amount", amount.attr("value")); - if (mid != null) map.put("mid", mid.attr("value")); + if (txnId != null) map.put("txnId", txnId.attr("value")); + if (amount != null) map.put("amount", amount.attr("value")); + if (mid != null) map.put("mid", mid.attr("value")); - LOGGER.debug("[parseHtmlResponse] parsed txnId={} amount={} mid={}", map.get("txnId"), map.get("amount"), map.get("mid")); - - } catch (Exception e) { - LOGGER.error("[parseHtmlResponse] error parsing HTML: {}", e.getMessage(), e); - } + LOGGER.debug("[parseHtmlResponse] parsed txnId={} amount={} mid={}", map.get("txnId"), map.get("amount"), map.get("mid")); return map; } + // Helper: parse JSON string into Map and rethrow Jackson checked exceptions as CustomException (unchecked) + private Map parseJsonToMap(String json, String apiName) { + if (json == null || json.trim().isEmpty()) { + LOGGER.error("[{}] Decrypted callback JSON is empty or null (length={})", apiName, json == null ? 0 : json.length()); + throw new CustomException(BBPSConstant.MSG_INVALID_DECRYPTED_JSON, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, json); + } + + // Log a short preview (avoid full sensitive data in logs) + int previewLen = Math.min(120, json.length()); + String preview = json.substring(0, previewLen) + (json.length() > previewLen ? "..." : ""); + LOGGER.debug("[{}] parsing decrypted JSON preview (len={}): {}", apiName, json.length(), preview); + + try { + return objectMapper.readValue(json, new TypeReference>(){}); + } catch (Exception e) { + // Jackson throws JsonProcessingException / IOException; catch any parsing error and rethrow as unchecked + LOGGER.error("[{}] Failed to parse decrypted callback JSON (len={}) error={}", apiName, json.length(), e.getMessage(), e); + throw new CustomException(BBPSConstant.MSG_INVALID_DECRYPTED_JSON, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, json); + } + } } \ No newline at end of file diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PollBackServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PollBackServiceImpl.java index 92eb583..b1a95d5 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PollBackServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PollBackServiceImpl.java @@ -23,7 +23,6 @@ public class PollBackServiceImpl implements PollBackService { this.helper = helper; } - @Override public ResponseModel managePollBack(PollBackRequest request) { diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/TxnHistoryServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/TxnHistoryServiceImpl.java index 93ecd54..c1b3714 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/TxnHistoryServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/TxnHistoryServiceImpl.java @@ -109,7 +109,6 @@ public class TxnHistoryServiceImpl implements TxnHistoryService { String mid = request.getMid(); String clientUserId = request.getClientUserId(); log.info("[{}] START | reqId={} | mid={} | userId={}", apiName, requestId, mid, clientUserId); - String txnId = helper.generateTxnId(15); String fullUrl = "this api fetch data from DB not BBPS url !"; log.info("[{}] Full URL={}", apiName, fullUrl); diff --git a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java index d8b5e09..db0486c 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java @@ -147,7 +147,6 @@ public class BBPSConstant extends BBPS_RESPONSE_COD_CONSTANT { public static final String UAT_PAYMENT_URL_WEB = "https://uat.godemo.in/api/UATIntegration/uat/payin/paymentrequest"; public static final String UAT_PAYMENT_URL_INTENT = "https://uat.godemo.in/api/UATIntegration/paymentrequest/seamless"; public static final String RESP_CODE_KEY = "respCode"; // key used by HttpService responses - public static final String RESPONSE_TYPE_HTML = "HTML"; public static final String RESPONSE_TYPE_QR = "QR"; public static final String RESPONSE_TYPE_JSON = "JSON"; diff --git a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java index 4581928..7d829cd 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java @@ -1078,38 +1078,78 @@ public class BillerMapperHelper { // Map PaymentStartUserRequest + API response into PaymentStartRecord and // persist - public void savePaymentStartRecord(PaymentStartUserRequest request, PaymentStartApiResponse apiResponse, - String txnId, String orderNo, String requestJson, String rawResponse, String responseType) { - PaymentStartRecord record = new PaymentStartRecord(); - record.setOrderNo(orderNo); - record.setRequestId(request.getReqId()); - record.setClientMid(request.getMid()); - record.setClientUserId(request.getClientUserId()); - record.setInternalTxnId(txnId); + public PaymentStartRecord savePaymentStartRecord( + PaymentStartUserRequest request, + PaymentStartApiResponse apiResponse, + String txnId, + String orderNo, + String requestJson, + String rawResponse, + String responseType) { - if (apiResponse != null && - apiResponse.getResponseData() != null) { - record.setAmount(Optional.ofNullable(apiResponse.getResponseData().getAmount( - )).orElse("0.00")); - - record.setTxnId(Optional.ofNullable(apiResponse.getResponseData().getTxnId()) - .orElse("DUMMY_TXN")); - record.setQrString(Optional.ofNullable(apiResponse.getResponseData(). - getQrString()).orElse("NO_QR")); // parse createdOn string into LocalDateTime and set on Auditable - Date createdOn =parseToDate(apiResponse.getResponseData().getCreatedOn()); - - record.setTxnStatus("PENDING"); } else { record.setAmount("0.00"); - record.setTxnStatus("PENDING"); } - - - // store the original request body and raw response - record.setRequestBody(requestJson); - record.setResponseBody(rawResponse); - record.setResponseType(responseType); + PaymentStartRecord record; - paymentStartRecordRepository.save(record); + boolean createNewRecord = + apiResponse == null && + rawResponse == null && + responseType == null; + + if (createNewRecord) { + // 🔒 Duplicate check + if (paymentStartRecordRepository.findByOrderNo(orderNo).isPresent()) { + throw new CustomException( + "Duplicate OrderNo found", + "DUPLICATE_ORDERNO", + 400, + "OrderNo already exists" + ); + } + record = new PaymentStartRecord(); + } else { + record = paymentStartRecordRepository.findByOrderNo(orderNo) + .orElseThrow(() -> new CustomException( + "OrderNo not found", + "ORDERNO_NOT_FOUND", + 404, + "No record exists for given orderNo" + )); + } + + if (orderNo != null) { + record.setOrderNo(orderNo); + record.setRequestId(request.getReqId()); + record.setClientMid(request.getMid()); + record.setClientUserId(request.getClientUserId()); + record.setInternalTxnId(txnId); + } + + if (apiResponse != null && apiResponse.getResponseData() != null) { + record.setAmount(Optional.ofNullable( + apiResponse.getResponseData().getAmount()).orElse("0.00")); + + record.setTxnId(Optional.ofNullable( + apiResponse.getResponseData().getTxnId()).orElse("DUMMY_TXN")); + + record.setQrString(Optional.ofNullable( + apiResponse.getResponseData().getQrString()).orElse("NO_QR")); + + Date createdOn = parseToDate(apiResponse.getResponseData().getCreatedOn()); + + record.setTxnStatus("PENDING"); + } else { + record.setAmount("0.00"); + record.setTxnStatus("PENDING"); + } + + // Raw request/response + record.setRequestBody(requestJson); + record.setResponseBody(rawResponse); + record.setResponseType(responseType); + + return paymentStartRecordRepository.save(record); } + // Map ComplaintRequestDTO to forward request for complaint API public FetchComplaintForwardRequest fetchComplaintForwardRequestMap(ComplaintRequestDTO complainRequestDTO) { FetchComplaintForwardRequest forward = new FetchComplaintForwardRequest(); diff --git a/src/main/java/com/billpaymentservice/bbps/util/EncryptionUtil.java b/src/main/java/com/billpaymentservice/bbps/util/EncryptionUtil.java index 96a47d4..f7703d5 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/EncryptionUtil.java +++ b/src/main/java/com/billpaymentservice/bbps/util/EncryptionUtil.java @@ -29,7 +29,6 @@ public class EncryptionUtil { SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); - byte[] ivBytes = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV(); byte[] encryptedData = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); diff --git a/src/main/java/com/billpaymentservice/bbps/util/Helper.java b/src/main/java/com/billpaymentservice/bbps/util/Helper.java index 6c62baf..827aff5 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/Helper.java +++ b/src/main/java/com/billpaymentservice/bbps/util/Helper.java @@ -18,6 +18,7 @@ import com.billpaymentservice.bbps.entites.BBPSMastersData; import com.billpaymentservice.bbps.entites.BharatConnectMaster; import com.billpaymentservice.bbps.entites.InboundTraceLog; import com.billpaymentservice.bbps.entites.OutboundTraceLog; +import com.billpaymentservice.bbps.entites.TransactionMaster; import com.billpaymentservice.bbps.entites.UserEntity; import com.billpaymentservice.bbps.exception.CustomException; import com.billpaymentservice.bbps.repository.BBPSMasterDataRepo; @@ -225,5 +226,63 @@ public class Helper { return userId; } + public void mergeNonNullFields(TransactionMaster src, TransactionMaster dest) { + /* + * LOGGER.info("TransactionMaster src: {}", src); + */ // Only copy values that are non-null (and non-blank for Strings) + if (src.getBankTxnId() != null && !src.getBankTxnId().isBlank()) dest.setBankTxnId(src.getBankTxnId()); + if (src.getTxnId() != null && !src.getTxnId().isBlank()) dest.setTxnId(src.getTxnId()); + if (src.getInitiatorTxnId() != null && !src.getInitiatorTxnId().isBlank()) dest.setInitiatorTxnId(src.getInitiatorTxnId()); + if (src.getCheckerTxnId() != null && !src.getCheckerTxnId().isBlank()) dest.setCheckerTxnId(src.getCheckerTxnId()); + if (src.getReqId() != null && !src.getReqId().isBlank()) dest.setReqId(src.getReqId()); + if (src.getMid() != null && !src.getMid().isBlank()) dest.setMid(src.getMid()); + if (src.getClientUserId() != null && !src.getClientUserId().isBlank()) dest.setClientUserId(src.getClientUserId()); + if (src.getClientReqId() != null && !src.getClientReqId().isBlank()) dest.setClientReqId(src.getClientReqId()); + if (src.getUserId() != null && !src.getUserId().isBlank()) dest.setUserId(src.getUserId()); + if (src.getBillerId() != null && !src.getBillerId().isBlank()) dest.setBillerId(src.getBillerId()); + if (src.getTxnAmount() != null) dest.setTxnAmount(src.getTxnAmount()); + if (src.getStatus() != null && !src.getStatus().isBlank()) dest.setStatus(src.getStatus()); + if (src.getPaymentInformation() != null && !src.getPaymentInformation().isBlank()) dest.setPaymentInformation(src.getPaymentInformation()); + if (src.getCreationDateBank() != null) dest.setCreationDateBank(src.getCreationDateBank()); + if (src.getModifiedDateBank() != null) dest.setModifiedDateBank(src.getModifiedDateBank()); + if (src.getCreatedEpochDateBank() != null) dest.setCreatedEpochDateBank(src.getCreatedEpochDateBank()); + if (src.getModifiedEpochDateBank() != null) dest.setModifiedEpochDateBank(src.getModifiedEpochDateBank()); + /* ---- additional fields ---- */ + if (src.getRefId() != null && !src.getRefId().isBlank()) dest.setRefId(src.getRefId()); + if (src.getPaymentRefId() != null && !src.getPaymentRefId().isBlank()) dest.setPaymentRefId(src.getPaymentRefId()); + if (src.getRemitterName() != null && !src.getRemitterName().isBlank()) dest.setRemitterName(src.getRemitterName()); + if (src.getOrderId() != null && !src.getOrderId().isBlank()) dest.setOrderId(src.getOrderId()); + if (src.getBillId() != null && !src.getBillId().isBlank()) dest.setBillId(src.getBillId()); + if (src.getUserType() != null && !src.getUserType().isBlank()) dest.setUserType(src.getUserType()); + if (src.getTotalTxnAmount() != null) dest.setTotalTxnAmount(src.getTotalTxnAmount()); + if (src.getBillerCategory() != null && !src.getBillerCategory().isBlank()) dest.setBillerCategory(src.getBillerCategory()); + if (src.getBillerName() != null && !src.getBillerName().isBlank()) dest.setBillerName(src.getBillerName()); + if (src.getOffUsPay() != null) dest.setOffUsPay(src.getOffUsPay()); + if (src.getSplitPay() != null) dest.setSplitPay(src.getSplitPay()); + if (src.getQuickPay() != null) dest.setQuickPay(src.getQuickPay()); + if (src.getPaymentMode() != null && !src.getPaymentMode().isBlank()) dest.setPaymentMode(src.getPaymentMode()); + if (src.getAgentPaymentInformation() != null && !src.getAgentPaymentInformation().isBlank()) dest.setAgentPaymentInformation(src.getAgentPaymentInformation()); + if (src.getCustConvFee() != null) dest.setCustConvFee(src.getCustConvFee()); + if (src.getCurrency() != null && !src.getCurrency().isBlank()) dest.setCurrency(src.getCurrency()); + if (src.getRequestedIp() != null && !src.getRequestedIp().isBlank()) dest.setRequestedIp(src.getRequestedIp()); + if (src.getCustomerMobNo() != null && !src.getCustomerMobNo().isBlank()) dest.setCustomerMobNo(src.getCustomerMobNo()); + if (src.getMacAddress() != null && !src.getMacAddress().isBlank()) dest.setMacAddress(src.getMacAddress()); + if (src.getSplitPayAmount() != null) dest.setSplitPayAmount(src.getSplitPayAmount()); + if (src.getCouCustConvFee() != null) dest.setCouCustConvFee(src.getCouCustConvFee()); + if (src.getSettlementVerification() != null) dest.setSettlementVerification(src.getSettlementVerification()); + if (src.getFundTxnType() != null && !src.getFundTxnType().isBlank()) dest.setFundTxnType(src.getFundTxnType()); + if (src.getReason() != null && !src.getReason().isBlank()) dest.setReason(src.getReason()); + if (src.getAmountTags() != null && !src.getAmountTags().isBlank()) dest.setAmountTags(src.getAmountTags()); + if (src.getPlanId() != null && !src.getPlanId().isBlank()) dest.setPlanId(src.getPlanId()); + if (src.getPlanType() != null && !src.getPlanType().isBlank()) dest.setPlanType(src.getPlanType()); + if (src.getTxnNpciId() != null && !src.getTxnNpciId().isBlank()) dest.setTxnNpciId(src.getTxnNpciId()); + + /* ---- lists (direct replace when provided) ---- */ + if (src.getDeviceBlockTags() != null && !src.getDeviceBlockTags().isEmpty()) dest.setDeviceBlockTags(src.getDeviceBlockTags()); + if (src.getCustomerParams() != null && !src.getCustomerParams().isEmpty()) dest.setCustomerParams(src.getCustomerParams()); + + LOGGER.info("TransactionMaster dest after merge: {}", dest); + } + } \ No newline at end of file From 596f519d1e83b992b8db1093f3180366c7c608e0 Mon Sep 17 00:00:00 2001 From: Lincpay Tech Date: Fri, 19 Dec 2025 11:48:17 +0530 Subject: [PATCH 4/4] ready to go code and improved --- .../PaymentStartWithTransactionDto.java | 17 +++++++++++++++++ .../bbps/entites/PaymentStartRecord.java | 11 ----------- .../serviceImpl/PaymentStartServiceImpl.java | 2 +- .../bbps/util/BillerMapperHelper.java | 4 +--- 4 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/billpaymentservice/bbps/dto/response/PaymentStartWithTransactionDto.java diff --git a/src/main/java/com/billpaymentservice/bbps/dto/response/PaymentStartWithTransactionDto.java b/src/main/java/com/billpaymentservice/bbps/dto/response/PaymentStartWithTransactionDto.java new file mode 100644 index 0000000..ec3b97c --- /dev/null +++ b/src/main/java/com/billpaymentservice/bbps/dto/response/PaymentStartWithTransactionDto.java @@ -0,0 +1,17 @@ +package com.billpaymentservice.bbps.dto.response; + +import com.billpaymentservice.bbps.entites.PaymentStartRecord; +import com.billpaymentservice.bbps.entites.TransactionMaster; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PaymentStartWithTransactionDto { + private PaymentStartRecord paymentStartRecord; + private TransactionMaster transactionMaster; +} diff --git a/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java b/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java index 0e11856..4cfb463 100644 --- a/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java +++ b/src/main/java/com/billpaymentservice/bbps/entites/PaymentStartRecord.java @@ -53,10 +53,6 @@ public class PaymentStartRecord extends Auditable { @Column(name = "qr_string", columnDefinition = "TEXT") private String qrString; - // `createdOn` is provided by the base `Auditable` class as a LocalDateTime field, - // so we must not redeclare it here (it caused an incompatible return-type error). - // The Auditable base class already defines `createdOn` of type LocalDateTime. - // New callback-related fields @Column(name = "callback_status") private String callbackStatus; @@ -67,13 +63,6 @@ public class PaymentStartRecord extends Auditable { @Column(name = "signature") private String signature; - // Store original request body and full response body - @Column(name = "request_body", columnDefinition = "TEXT") - private String requestBody; - - @Column(name = "response_body", columnDefinition = "TEXT") - private String responseBody; - @Column(name = "response_type") private String responseType; // e.g., "QR", "HTML", "JSON" diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java index 58a2c69..f91c3bd 100644 --- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java +++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java @@ -406,7 +406,7 @@ public class PaymentStartServiceImpl implements PaymentStartService { record.setSignature(signature); // update txn status and callback timestamp record.setTxnStatus(txnStatus); - record.setCallbackReceivedOn(java.time.LocalDateTime.now().toString()); + record.setCallbackReceivedOn(LocalDateTime.now().toString()); String merchantStatus; if (!signatureMatches) { diff --git a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java index 7d829cd..38ada18 100644 --- a/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java +++ b/src/main/java/com/billpaymentservice/bbps/util/BillerMapperHelper.java @@ -1141,9 +1141,7 @@ public class BillerMapperHelper { record.setTxnStatus("PENDING"); } - // Raw request/response - record.setRequestBody(requestJson); - record.setResponseBody(rawResponse); + record.setResponseType(responseType); return paymentStartRecordRepository.save(record);