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); } } } }