transaction master and payment start record new entity with update

This commit is contained in:
Lincpay Tech
2025-12-23 10:38:59 +05:30
parent 46e50acc88
commit 38979f0f94
9 changed files with 26 additions and 184 deletions
@@ -30,6 +30,7 @@ public class BillMaster extends Auditable {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "bill_id", unique = true) @Column(name = "bill_id", unique = true)
private String billId; // primary key private String billId; // primary key
private String reqId; // client se aaya hua private String reqId; // client se aaya hua
@@ -35,11 +35,11 @@ public class PaymentStartRecord extends Auditable {
@Column(name = "mid", nullable = false) @Column(name = "mid", nullable = false)
private String mid; private String mid;
@Column(name = "client_user_id") @Column(name = "user_id")
private String clientUserId; private String userId;
@Column(name = "internal_txn_id", unique = true, nullable = false) @Column(name = "txn_id", unique = true, nullable = false)
private String internalTxnId; private String txnId;
@Column(name = "order_no", unique = true, nullable = false) @Column(name = "order_no", unique = true, nullable = false)
private String orderNo; private String orderNo;
@@ -47,8 +47,8 @@ public class PaymentStartRecord extends Auditable {
@Column(name = "amount", nullable = false) @Column(name = "amount", nullable = false)
private String amount; private String amount;
@Column(name = "txn_id",unique = true) @Column(name = "payment_ref_id",unique = true)
private String txnId; private String paymentRefId;
@Column(name = "qr_string", columnDefinition = "TEXT") @Column(name = "qr_string", columnDefinition = "TEXT")
private String qrString; private String qrString;
@@ -30,6 +30,7 @@ import lombok.ToString;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TransactionMaster extends Auditable { public class TransactionMaster extends Auditable {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id") @Column(name = "id")
@@ -48,8 +49,8 @@ public class TransactionMaster extends Auditable {
@Column(name = "checker_txn_id") @Column(name = "checker_txn_id")
private String checkerTxnId; private String checkerTxnId;
@Column(name = "client_user_id") @Column(name = "user_id")
private String clientUserId; private String userId;
@Column(name = "mid") @Column(name = "mid")
private String mid; private String mid;
@@ -78,8 +79,8 @@ public class TransactionMaster extends Auditable {
@Column(name = "user_type") @Column(name = "user_type")
private String userType; private String userType;
@Column(name = "user_id") @Column(name = "bank_user_id")
private String userId; private String bankUserId;
@Column(name = "txn_amount") @Column(name = "txn_amount")
private String txnAmount; private String txnAmount;
@@ -163,7 +164,7 @@ public class TransactionMaster extends Auditable {
// ✅ Relation with PaymentStartRecord (one-to-one) // ✅ Relation with PaymentStartRecord (one-to-one)
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "payment_ref_id", referencedColumnName = "txn_id", insertable = false, updatable = false) @JoinColumn(name = "payment_ref_id", referencedColumnName = "payment_ref_id", insertable = false, updatable = false)
@JsonIgnore @JsonIgnore
private PaymentStartRecord paymentStartRecord; private PaymentStartRecord paymentStartRecord;
@@ -21,10 +21,13 @@ public interface TransactionMasterRepository extends JpaRepository<TransactionMa
@Query("SELECT t FROM TransactionMaster t WHERE t.mid = :mid") @Query("SELECT t FROM TransactionMaster t WHERE t.mid = :mid")
Page<TransactionMaster> findByMid(@Param("mid") String mid, Pageable pageable); Page<TransactionMaster> findByMid(@Param("mid") String mid, Pageable pageable);
@Query("SELECT t FROM TransactionMaster t WHERE t.mid = :mid AND t.clientUserId = :clientUserId") @Query("SELECT t FROM TransactionMaster t WHERE t.mid = :mid AND t.userId = :userId")
Page<TransactionMaster> findByMidOrClientUserId(@Param("mid") String mid, Page<TransactionMaster> findByMidAndUserId(
@Param("clientUserId") String clientUserId, Pageable pageable); @Param("mid") String mid,
@Param("userId") String userId,
Pageable pageable
);
@Query("SELECT t FROM TransactionMaster t WHERE t.txnId = :txnId") @Query("SELECT t FROM TransactionMaster t WHERE t.txnId = :txnId")
List<TransactionMaster> findByTxnId(@Param("txnId") String txnId); List<TransactionMaster> findByTxnId(@Param("txnId") String txnId);
@@ -1,162 +0,0 @@
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);
}
}
}
}
@@ -117,9 +117,8 @@ public class PaymentServiceImpl implements PaymentService {
preTransaction.setInitiatorTxnId(txnId); preTransaction.setInitiatorTxnId(txnId);
preTransaction.setReqId(requestId); preTransaction.setReqId(requestId);
preTransaction.setMid(billPaymentRequest.getMid()); preTransaction.setMid(billPaymentRequest.getMid());
preTransaction.setClientUserId(billPaymentRequest.getClientUserId()); preTransaction.setUserId(billPaymentRequest.getClientUserId());
preTransaction.setClientReqId(billPaymentRequest.getClientReqId()); preTransaction.setClientReqId(billPaymentRequest.getClientReqId());
preTransaction.setUserId(userId);
preTransaction.setStatus("PENDING"); preTransaction.setStatus("PENDING");
EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction); EntityRelationshipUtil.linkTransactionMasterRelationships(preTransaction);
// ensure txnId exists (no uniqueness suffixing) // ensure txnId exists (no uniqueness suffixing)
@@ -67,7 +67,7 @@ public class TxnHistoryServiceImpl implements TxnHistoryService {
} else { } else {
Pageable pageable = PageRequest.of(paymentHistoryRequest.getPageNo(), Pageable pageable = PageRequest.of(paymentHistoryRequest.getPageNo(),
paymentHistoryRequest.getPageSize()); paymentHistoryRequest.getPageSize());
dataResult = transactionMasterRepository.findByMidOrClientUserId(mid, clientUserId, pageable); dataResult = transactionMasterRepository.findByMidAndUserId(mid, clientUserId, pageable);
} }
// List<TransactionMaster> dataResult = // List<TransactionMaster> dataResult =
@@ -1034,15 +1034,15 @@ public class BillerMapperHelper {
record.setOrderNo(orderNo); record.setOrderNo(orderNo);
record.setRequestId(request.getReqId()); record.setRequestId(request.getReqId());
record.setMid(request.getMid()); record.setMid(request.getMid());
record.setClientUserId(request.getClientUserId()); record.setUserId(request.getClientUserId());
record.setInternalTxnId(txnId); record.setTxnId(txnId);
} }
if (apiResponse != null && apiResponse.getResponseData() != null) { if (apiResponse != null && apiResponse.getResponseData() != null) {
record.setAmount(Optional.ofNullable( record.setAmount(Optional.ofNullable(
apiResponse.getResponseData().getAmount()).orElse("0.00")); apiResponse.getResponseData().getAmount()).orElse("0.00"));
record.setTxnId(Optional.ofNullable( record.setPaymentRefId(Optional.ofNullable(
apiResponse.getResponseData().getTxnId()).orElse("DUMMY_TXN")); apiResponse.getResponseData().getTxnId()).orElse("DUMMY_TXN"));
Date createdOn = parseToDate(apiResponse.getResponseData().getCreatedOn()); Date createdOn = parseToDate(apiResponse.getResponseData().getCreatedOn());
@@ -238,7 +238,7 @@ public class Helper {
if (src.getCheckerTxnId() != null && !src.getCheckerTxnId().isBlank()) dest.setCheckerTxnId(src.getCheckerTxnId()); if (src.getCheckerTxnId() != null && !src.getCheckerTxnId().isBlank()) dest.setCheckerTxnId(src.getCheckerTxnId());
if (src.getReqId() != null && !src.getReqId().isBlank()) dest.setReqId(src.getReqId()); if (src.getReqId() != null && !src.getReqId().isBlank()) dest.setReqId(src.getReqId());
if (src.getMid() != null && !src.getMid().isBlank()) dest.setMid(src.getMid()); if (src.getMid() != null && !src.getMid().isBlank()) dest.setMid(src.getMid());
if (src.getClientUserId() != null && !src.getClientUserId().isBlank()) dest.setClientUserId(src.getClientUserId()); if (src.getBankUserId()!= null && !src.getBankUserId().isBlank()) dest.setBankUserId(src.getBankUserId());
if (src.getClientReqId() != null && !src.getClientReqId().isBlank()) dest.setClientReqId(src.getClientReqId()); if (src.getClientReqId() != null && !src.getClientReqId().isBlank()) dest.setClientReqId(src.getClientReqId());
if (src.getUserId() != null && !src.getUserId().isBlank()) dest.setUserId(src.getUserId()); if (src.getUserId() != null && !src.getUserId().isBlank()) dest.setUserId(src.getUserId());
if (src.getBillerId() != null && !src.getBillerId().isBlank()) dest.setBillerId(src.getBillerId()); if (src.getBillerId() != null && !src.getBillerId().isBlank()) dest.setBillerId(src.getBillerId());