half commited

This commit is contained in:
Lincpay Tech
2025-12-17 15:30:33 +05:30
parent 4b5f81d0d5
commit c8bc6e7f12
12 changed files with 409 additions and 132 deletions
@@ -6,29 +6,37 @@ import java.time.LocalDateTime;
import jakarta.persistence.PrePersist; import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate; import jakarta.persistence.PreUpdate;
import java.lang.reflect.Field;
import java.util.Date;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
public class AuditListener { public class AuditListener {
@PrePersist @PrePersist
public void prePersist(Object entity) { 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, "createDateTime", now);
setIfNull(entity, "createdBy", null); // handle manually in service if needed setIfNull(entity, "createdBy", null);
setIfNull(entity, "createdEpochDate", System.currentTimeMillis()); setIfNull(entity, "createdEpochDate", now.getTime());
// Set isDeleted = false on creation // default soft delete flag
setAlways(entity, "isDeleted", Boolean.FALSE); setAlways(entity, "isDeleted", Boolean.FALSE);
} }
/* @PreUpdate
* @PreUpdate public void preUpdate(Object entity) { LocalDateTime now = public void preUpdate(Object entity) {
* LocalDateTime.now(); Date now = new Date();
*
* // Update fields setAlways(entity, "updatedDateTime", now); setIfNull(entity, // update fields
* "updatedBy", null); // you can override manually in service setAlways(entity, setAlways(entity, "updatedDateTime", now);
* "modifiedEpochDate", System.currentTimeMillis()); } setIfNull(entity, "updatedBy", null);
*/ setAlways(entity, "modifiedEpochDate", now.getTime());
}
private void setIfNull(Object entity, String fieldName, Object value) { private void setIfNull(Object entity, String fieldName, Object value) {
try { try {
Field f = findField(entity.getClass(), fieldName); Field f = findField(entity.getClass(), fieldName);
@@ -37,8 +45,7 @@ public class AuditListener {
boolean accessible = f.canAccess(entity); boolean accessible = f.canAccess(entity);
f.setAccessible(true); f.setAccessible(true);
Object currentValue = f.get(entity); if (f.get(entity) == null) {
if (currentValue == null) {
f.set(entity, value); f.set(entity, value);
} }
@@ -53,9 +60,7 @@ public class AuditListener {
boolean accessible = f.canAccess(entity); boolean accessible = f.canAccess(entity);
f.setAccessible(true); f.setAccessible(true);
f.set(entity, value); f.set(entity, value);
f.setAccessible(accessible); f.setAccessible(accessible);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
@@ -1,6 +1,8 @@
package com.billpaymentservice.bbps.entites; package com.billpaymentservice.bbps.entites;
import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners; import jakarta.persistence.EntityListeners;
@@ -16,18 +18,18 @@ import lombok.Setter;
@EntityListeners(AuditListener.class) @EntityListeners(AuditListener.class)
@Getter @Getter
@Setter @Setter
public abstract class Auditable { public abstract class Auditable implements Serializable {
@Column(name = "create_date_time") @Column(name = "create_date_time")
private LocalDateTime createDateTime; private Date createDateTime;
@Column(name = "updated_date_time") @Column(name = "updated_date_time")
private LocalDateTime updatedDateTime; private Date updatedDateTime;
@Column(name = "create_by") @Column(name = "create_by")
private LocalDateTime createdBy; private String createdBy;
@Column(name = "updated_by") @Column(name = "updated_by")
private LocalDateTime updatedBy; private String updatedBy;
@Column(name = "created_epoch_date") @Column(name = "created_epoch_date")
private Long createdEpochDate; private Long createdEpochDate;
@@ -1,9 +1,5 @@
package com.billpaymentservice.bbps.entites; 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.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -11,30 +7,15 @@ import lombok.NoArgsConstructor;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Entity public class PaymentInitiate {
@Table(name = "bbps_payment_initiate")
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;
} }
@@ -1,9 +1,16 @@
package com.billpaymentservice.bbps.repository; 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.JpaRepository;
import com.billpaymentservice.bbps.entites.OutboundTraceLog; import com.billpaymentservice.bbps.entites.OutboundTraceLog;
public interface OutboundTraceLogRepository extends JpaRepository<OutboundTraceLog, Long> { public interface OutboundTraceLogRepository extends JpaRepository<OutboundTraceLog, Long> {
} Optional<OutboundTraceLog> findTopByTxnIdAndApiNameOrderByIdDesc(String txnId, String apiName);
List<OutboundTraceLog> findByTxnId(String txnId);
}
@@ -1,17 +1,6 @@
package com.billpaymentservice.bbps.repository; package com.billpaymentservice.bbps.repository;
import java.util.Optional; // Legacy stub: PaymentInitiate table removed; keep a placeholder class to avoid compilation errors if referenced.
public class PaymentIntiateRepository {
import org.springframework.data.jpa.repository.JpaRepository; // Intentionally empty - PaymentInitiate is no longer a JPA entity and this repo is retired.
import org.springframework.data.jpa.repository.Query; }
import com.billpaymentservice.bbps.entites.PaymentInitiate;
public interface PaymentIntiateRepository extends JpaRepository<PaymentInitiate, String> {
// Optional<PaymentInitiate> findByReqId(String clientReqId);
@Query(value = "SELECT * FROM bbps_payment_initiate WHERE req_id = :clientReqId", nativeQuery = true)
Optional<PaymentInitiate> findByReqId(String clientReqId);
}
@@ -30,4 +30,11 @@ public interface TransactionMasterRepository extends JpaRepository<TransactionMa
// Find TransactionMaster(s) by the payment_ref_id column (references PaymentStartRecord.txn_id) // Find TransactionMaster(s) by the payment_ref_id column (references PaymentStartRecord.txn_id)
List<TransactionMaster> findByPaymentRefId(String paymentRefId); List<TransactionMaster> findByPaymentRefId(String paymentRefId);
// New finders to replace PaymentInitiate usage
List<TransactionMaster> findByClientReqId(String clientReqId);
List<TransactionMaster> findByStatus(String status);
List<TransactionMaster> findByStatusIn(List<String> statuses);
} }
@@ -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<String> statuses = Arrays.asList("PENDING_UPSTREAM", "INVALID_RESPONSE_FROM_BBPS",
"INVALID_JSON_FROM_UPSTREAM", "Failed", "FAILED");
List<TransactionMaster> 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<OutboundTraceLog> 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<String, String> 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<String, Object> dataMap = objectMapper.readValue(responseData, new TypeReference<Map<String, Object>>() {});
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);
}
}
}
}
@@ -1,5 +1,6 @@
package com.billpaymentservice.bbps.serviceImpl; package com.billpaymentservice.bbps.serviceImpl;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; 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.BillPayment;
import com.billpaymentservice.bbps.entites.InboundTraceLog; import com.billpaymentservice.bbps.entites.InboundTraceLog;
import com.billpaymentservice.bbps.entites.OutboundTraceLog; import com.billpaymentservice.bbps.entites.OutboundTraceLog;
import com.billpaymentservice.bbps.entites.PaymentInitiate;
import com.billpaymentservice.bbps.entites.TransactionMaster; import com.billpaymentservice.bbps.entites.TransactionMaster;
import com.billpaymentservice.bbps.exception.CustomException; import com.billpaymentservice.bbps.exception.CustomException;
import com.billpaymentservice.bbps.repository.AgentFundTransferRepository; import com.billpaymentservice.bbps.repository.AgentFundTransferRepository;
import com.billpaymentservice.bbps.repository.BharatConnectMasterRepository; import com.billpaymentservice.bbps.repository.BharatConnectMasterRepository;
import com.billpaymentservice.bbps.repository.InboundTraceLogRepository; import com.billpaymentservice.bbps.repository.InboundTraceLogRepository;
import com.billpaymentservice.bbps.repository.OutboundTraceLogRepository; import com.billpaymentservice.bbps.repository.OutboundTraceLogRepository;
import com.billpaymentservice.bbps.repository.PaymentIntiateRepository;
import com.billpaymentservice.bbps.repository.PaymentRepository; import com.billpaymentservice.bbps.repository.PaymentRepository;
import com.billpaymentservice.bbps.repository.TransactionMasterRepository; import com.billpaymentservice.bbps.repository.TransactionMasterRepository;
import com.billpaymentservice.bbps.service.PaymentService; import com.billpaymentservice.bbps.service.PaymentService;
@@ -54,7 +53,6 @@ public class PaymentServiceImpl implements PaymentService {
private final PaymentRepository paymentRepository; private final PaymentRepository paymentRepository;
private final InboundTraceLogRepository inboundTraceLogRepository; private final InboundTraceLogRepository inboundTraceLogRepository;
private final OutboundTraceLogRepository outboundTraceLogRepository; private final OutboundTraceLogRepository outboundTraceLogRepository;
private final PaymentIntiateRepository paymentIntiateRepository;
private final TransactionMasterRepository transactionMasterRepository; private final TransactionMasterRepository transactionMasterRepository;
private final AgentFundTransferRepository agentFundTransferRepository; private final AgentFundTransferRepository agentFundTransferRepository;
private final BillerMapperHelper mapperHelper; private final BillerMapperHelper mapperHelper;
@@ -66,8 +64,7 @@ public class PaymentServiceImpl implements PaymentService {
public PaymentServiceImpl(BharatConnectMasterRepository beneficiaryDetailsRepository, HttpService httpService, public PaymentServiceImpl(BharatConnectMasterRepository beneficiaryDetailsRepository, HttpService httpService,
Helper helper, PaymentRepository paymentRepository, InboundTraceLogRepository inboundTraceLogRepository, Helper helper, PaymentRepository paymentRepository, InboundTraceLogRepository inboundTraceLogRepository,
OutboundTraceLogRepository outboundTraceLogRepository, PaymentIntiateRepository paymentIntiateRepository, OutboundTraceLogRepository outboundTraceLogRepository, TransactionMasterRepository transactionMasterRepository,
TransactionMasterRepository transactionMasterRepository,
AgentFundTransferRepository agentFundTransferRepository, BillerMapperHelper mapperHelper) { AgentFundTransferRepository agentFundTransferRepository, BillerMapperHelper mapperHelper) {
this.beneficiaryDetailsRepository = beneficiaryDetailsRepository; this.beneficiaryDetailsRepository = beneficiaryDetailsRepository;
this.httpService = httpService; this.httpService = httpService;
@@ -75,7 +72,6 @@ public class PaymentServiceImpl implements PaymentService {
this.paymentRepository = paymentRepository; this.paymentRepository = paymentRepository;
this.inboundTraceLogRepository = inboundTraceLogRepository; this.inboundTraceLogRepository = inboundTraceLogRepository;
this.outboundTraceLogRepository = outboundTraceLogRepository; this.outboundTraceLogRepository = outboundTraceLogRepository;
this.paymentIntiateRepository = paymentIntiateRepository;
this.transactionMasterRepository = transactionMasterRepository; this.transactionMasterRepository = transactionMasterRepository;
this.agentFundTransferRepository = agentFundTransferRepository; this.agentFundTransferRepository = agentFundTransferRepository;
this.mapperHelper = mapperHelper; this.mapperHelper = mapperHelper;
@@ -114,6 +110,26 @@ public class PaymentServiceImpl implements PaymentService {
forwardjson); forwardjson);
LOGGER.debug("Outbound log created for api={} | reqId={} | txnId={}", apiName, requestId, txnId); 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 { try {
LOGGER.info("Calling BBPS payment URL: {}", url); LOGGER.info("Calling BBPS payment URL: {}", url);
Map<String, String> responseMap = httpService.postRequest(url, forwardjson, headers); Map<String, String> responseMap = httpService.postRequest(url, forwardjson, headers);
@@ -125,14 +141,21 @@ public class PaymentServiceImpl implements PaymentService {
if (responseData == null || responseData.isEmpty()) { if (responseData == null || responseData.isEmpty()) {
helper.updateInboundLogFailure(inboundLog, BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, helper.updateInboundLogFailure(inboundLog, BBPSConstant.EMPTY_RESPONSE_FROM_BBPS,
BBPSConstant.FAILED_STR); BBPSConstant.FAILED_STR);
LOGGER.warn("Empty response received from BBPS for txnId={}", txnId); LOGGER.warn("Empty response received from BBPS for txnId={}", txnId);
throw new CustomException( // Persist a TransactionMaster placeholder so scheduler can pick it up later
BBPSConstant.EMPTY_RESPONSE_FROM_BBPS, // message TransactionMaster tm = new TransactionMaster();
BBPSConstant.FAILED_STATUS, // status tm.setBankTxnId("PENDING_" + txnId);
502, // statusCode tm.setTxnId(txnId);
"BBPS upstream returned empty response" 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<String, Object> dataMap = objectMapper.readValue(responseData, Map<String, Object> dataMap = objectMapper.readValue(responseData,
@@ -141,16 +164,42 @@ public class PaymentServiceImpl implements PaymentService {
if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERIC_KEY)) { if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERIC_KEY)) {
helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_STRUCTURE); helper.updateInboundLogFailure(inboundLog, responseData, BBPSConstant.INVALID_STRUCTURE);
LOGGER.warn("Invalid response structure received for txnId={}", txnId); 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); BillPaymentResponse billPaymentResponse = objectMapper.convertValue(dataMap, BillPaymentResponse.class);
GeneralResp generalResponse = billPaymentResponse.getGenericResponse(); GeneralResp generalResponse = billPaymentResponse.getGenericResponse();
LOGGER.info("genric Response" + generalResponse); LOGGER.info("genric Response" + generalResponse);
if(generalResponse.getStatus().equalsIgnoreCase("Failed")) { if(generalResponse.getStatus().equalsIgnoreCase("Failed")) {
throw new CustomException(generalResponse.getMessage(), generalResponse.getStatus(), // Save TransactionMaster with failed status so it can be reconciled later
Integer.parseInt(generalResponse.getStatusCode()), generalResponse.getRemarks()); 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); 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); LOGGER.info("BillPayment entity saved successfully for txnId={}", txnId);
helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus()); helper.updateInboundLogSuccess(inboundLog, responseData, generalResponse.getStatus());
PaymentInitiate paymentInitiate = new PaymentInitiate(); // Create and save TransactionMaster for this initiate flow so everything is stored in the same table
paymentInitiate.setTxnId(txnId); try {
paymentInitiate.setUserId(userId); TransactionMaster transactionMaster = new TransactionMaster();
paymentInitiate.setReqId(requestId); // bankTxnId is BBPS txn id (primary key)
paymentInitiate.setMid(billPaymentRequest.getMid()); transactionMaster.setBankTxnId(billPaymentResponse.getTxnId());
paymentInitiate.setClientUserId(billPaymentRequest.getClientUserId()); // use internal txnId generated earlier for tracing
paymentInitiate.setBankTxnId(billPaymentResponse.getTxnId()); transactionMaster.setTxnId(txnId);
paymentInitiate.setStatus(billPaymentResponse.getStatus()); transactionMaster.setReqId(requestId);
paymentIntiateRepository.save(paymentInitiate); transactionMaster.setMid(billPaymentRequest.getMid());
LOGGER.info("PaymentInitiate entry saved successfully for txnId={}", txnId); 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 transactionStatusRequest = new TransactionStatusRequest();
transactionStatusRequest.setTxnId(billPaymentResponse.getTxnId()); transactionStatusRequest.setTxnId(billPaymentResponse.getTxnId());
transactionStatusRequest.setReqId(requestId); transactionStatusRequest.setReqId(requestId);
@@ -188,6 +249,18 @@ public class PaymentServiceImpl implements PaymentService {
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
LOGGER.error("JSON Parsing Error in initiatePayment: {}", e.getMessage(), e); LOGGER.error("JSON Parsing Error in initiatePayment: {}", e.getMessage(), e);
helper.updateInboundLogFailure(inboundLog, e.getMessage(), BBPSConstant.JSON_ERROR); 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()); 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.setMid(transactionStatusRequest.getMid());
transactionMaster.setClientUserId(transactionStatusRequest.getClientUserId()); transactionMaster.setClientUserId(transactionStatusRequest.getClientUserId());
EntityRelationshipUtil.linkTransactionMasterRelationships(transactionMaster); 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); LOGGER.info("TransactionMaster saved successfully for txnId={}", txnId);
AgentFundTransfer agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(), AgentFundTransfer agentFundTransfer = modelMapper.map(responseWrapper.getAgentFundTransfer(),
@@ -306,7 +381,7 @@ public class PaymentServiceImpl implements PaymentService {
}); });
if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) { if (!dataMap.containsKey(BBPSConstant.RESPONSE_GENERAL_RESP)) {
helper.updateInboundLogFailure(inboundLog, responseData, 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", LOGGER.warn("Invalid transaction status response for txnId={} from BBPS",
transactionStatusRequest.getTxnId()); transactionStatusRequest.getTxnId());
throw new CustomException(String.valueOf(dataMap.get("message")), String.valueOf(dataMap.get("status")), 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, LOGGER.info("[{}] START | reqId={} | mid={} | clientReqId={}", apiName, requestId,
transactionStatusRequest.getMid(), clientReqId); transactionStatusRequest.getMid(), clientReqId);
Optional<PaymentInitiate> paymentInitiate = paymentIntiateRepository.findByReqId(clientReqId); // look up TransactionMaster(s) by clientReqId instead of PaymentInitiate
if (paymentInitiate.isEmpty()) { List<TransactionMaster> transactionMasters = transactionMasterRepository.findByClientReqId(clientReqId);
if (transactionMasters == null || transactionMasters.isEmpty()) {
throw new CustomException( throw new CustomException(
BBPSConstant.NO_BILL_FOUND, // message // status BBPSConstant.NO_BILL_FOUND, // message // status
BBPSConstant.FAILED_STATUS, BBPSConstant.FAILED_STATUS,
BBPSConstant.HTTP_NOT_FOUND, // statusCode BBPSConstant.HTTP_NOT_FOUND, // statusCode
"Bill not found in database for given clientReqId" "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 String url = paymentUrl + BBPSConstant.CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL
+ paymentInitiate.get().getTxnId(); + chosen.getTxnId();
BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID); BharatConnectMaster headersData = beneficiaryDetailsRepository.findByOrgId(BBPSConstant.ORG_ID);
HttpHeaders headers = helper.buildHeaders(headersData); 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<TransactionMaster> existing = transactionMasterRepository.findByTxnId(baseTxnId);
if (existing == null || existing.isEmpty()) {
return baseTxnId;
}
int suffix = 1;
String candidate;
while (true) {
candidate = baseTxnId + "_" + suffix;
List<TransactionMaster> 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");
}
} }
@@ -87,7 +87,6 @@ public class PaymentStartServiceImpl implements PaymentStartService {
String txnId = helper.generateTxnId(15); String txnId = helper.generateTxnId(15);
LOGGER.info("MID IS:{}",paymentMid); LOGGER.info("MID IS:{}",paymentMid);
LOGGER.info("[startIntentPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); LOGGER.info("[startIntentPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid());
helper.validateMerchant(request.getMid()); helper.validateMerchant(request.getMid());
// inbound log // inbound log
@@ -102,7 +101,7 @@ public class PaymentStartServiceImpl implements PaymentStartService {
sent.setEnckey(paymentEncryKey); sent.setEnckey(paymentEncryKey);
String plaintext = helper.convertObjectToJson(sent); String plaintext = helper.convertObjectToJson(sent);
String encrypted = encryptionUtil.encryptMData(plaintext, paymentEncryKey); String encrypted = encryptionUtil.encryptMData(plaintext, paymentEncryKey);
PaymentRequestPayload finalPayload = new PaymentRequestPayload(); PaymentRequestPayload finalPayload = new PaymentRequestPayload();
finalPayload.setMid(paymentMid); finalPayload.setMid(paymentMid);
finalPayload.setPayload(encrypted); finalPayload.setPayload(encrypted);
@@ -217,7 +216,6 @@ public class PaymentStartServiceImpl implements PaymentStartService {
String requestId = request.getReqId(); String requestId = request.getReqId();
String apiName = BBPSConstant.API_START_PAYMENT; String apiName = BBPSConstant.API_START_PAYMENT;
String txnId = helper.generateTxnId(15); String txnId = helper.generateTxnId(15);
LOGGER.info("[startWebPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid()); LOGGER.info("[startWebPayment] enter requestId={} txnId={} mid={}", requestId, txnId, request.getMid());
helper.validateMerchant(request.getMid()); helper.validateMerchant(request.getMid());
@@ -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_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_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 BILL_VALIDATE_BY_CLIENT_REQ_ID = "/couapp/bills/validate/byClientId?clientReqId=";
public static final String PAYMENT_URL = "/agent/bills/payment"; public static final String PAYMENT_URL = "/agent/bills/paymhent";
public static final String CHECK_TRANSACTION_URL = "/transactions/payment/byTxnId?txnId="; 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_URL = "/bills/fetch/byClientId?clientReqId=";
public static final String CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL = "/transactions/payment/byClientId?clientReqId="; public static final String CHECK_TRANSACTION_BY_CLIENT_REQ_ID_URL = "/transactions/payment/byClientId?clientReqId=";
@@ -2,6 +2,7 @@ package com.billpaymentservice.bbps.util;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
@@ -719,9 +720,9 @@ public class BillerMapperHelper {
parent.setActionType(responseWrapper.getActionType()); parent.setActionType(responseWrapper.getActionType());
parent.setUserId(responseWrapper.getUserId()); parent.setUserId(responseWrapper.getUserId());
// Map incoming DTO date strings into Auditable LocalDateTime fields // 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.setCreatedEpochDate(responseWrapper.getCreatedEpochDate());
parent.setModifiedEpochDate(responseWrapper.getModifiedEpochDate()); parent.setModifiedEpochDate(responseWrapper.getModifiedEpochDate());
parent.setReqId(reqId); parent.setReqId(reqId);
@@ -963,9 +964,9 @@ public class BillerMapperHelper {
// DATES // DATES
// Map incoming DTO date strings into Auditable LocalDateTime fields // 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) { if (bmUpdatedOn != null) {
billMaster.setUpdatedDateTime(bmUpdatedOn); billMaster.setUpdatedDateTime(bmUpdatedOn);
} }
@@ -977,26 +978,17 @@ public class BillerMapperHelper {
// Helper to parse incoming ISO date/time strings (with optional offset) into // Helper to parse incoming ISO date/time strings (with optional offset) into
// LocalDateTime // LocalDateTime
private LocalDateTime parseToLocalDateTime(String dateStr) { private Date parseToDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) { try {
return null; // example format: 2025-12-17T14:30:45
} LocalDateTime ldt = LocalDateTime.parse(dateStr);
try { return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// Try parsing with offset first (e.g., 2023-04-19T17:28:05.508679Z or with } catch (Exception e) {
// +05:30) return null;
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;
}
}
} }
// Convert TransactionMaster to master DTO // Convert TransactionMaster to master DTO
public TransactionHistoryMasterDTO toDTO(TransactionMaster transactionMaster) { public TransactionHistoryMasterDTO toDTO(TransactionMaster transactionMaster) {
if (transactionMaster == null) if (transactionMaster == null)
@@ -1104,7 +1096,7 @@ public class BillerMapperHelper {
.orElse("DUMMY_TXN")); .orElse("DUMMY_TXN"));
record.setQrString(Optional.ofNullable(apiResponse.getResponseData(). record.setQrString(Optional.ofNullable(apiResponse.getResponseData().
getQrString()).orElse("NO_QR")); // parse createdOn string into LocalDateTime and set on Auditable 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"); } else { record.setAmount("0.00");
record.setTxnStatus("PENDING"); } record.setTxnStatus("PENDING"); }
+1 -1
View File
@@ -76,7 +76,7 @@ jwt.secret=6C756148487475787A414646736D4B6E444E585A766145396E6A4555594A595566657
# Resilience4j Rate Limiter # 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.limit-refresh-period=1m
resilience4j.ratelimiter.configs.default.timeout-duration=0 resilience4j.ratelimiter.configs.default.timeout-duration=0
# Resilience4j Retry # Resilience4j Retry