package com.billpaymentservice.bbps.serviceImpl; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Optional; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import com.billpaymentservice.bbps.dto.PaymentStartReturnDto; import com.billpaymentservice.bbps.dto.ResponseModel; import com.billpaymentservice.bbps.dto.request.PaymentStartUserRequest; import com.billpaymentservice.bbps.dto.request.CallbackRequest; import com.billpaymentservice.bbps.dto.response.PaymentStartApiResponse; import com.billpaymentservice.bbps.dto.response.PaymentStatusResponse; import com.billpaymentservice.bbps.dto.sentRequest.PaymentDetailSentRequest; import com.billpaymentservice.bbps.dto.sentRequest.PaymentRequestPayload; import com.billpaymentservice.bbps.entites.InboundTraceLog; import com.billpaymentservice.bbps.exception.CustomException; import com.billpaymentservice.bbps.repository.PaymentStartRecordRepository; import com.billpaymentservice.bbps.repository.BharatConnectMasterRepository; import com.billpaymentservice.bbps.repository.BBPSMasterDataRepo; import com.billpaymentservice.bbps.service.PaymentStartService; import com.billpaymentservice.bbps.util.EncryptionUtil; import com.billpaymentservice.bbps.util.Helper; import com.billpaymentservice.bbps.util.BBPSConstant; import com.billpaymentservice.bbps.util.BillerMapperHelper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.billpaymentservice.bbps.entites.OutboundTraceLog; import com.billpaymentservice.bbps.entites.BharatConnectMaster; import com.billpaymentservice.bbps.entites.PaymentStartRecord; @Service public class PaymentStartServiceImpl implements PaymentStartService { private static final Logger LOGGER = LoggerFactory.getLogger(PaymentStartServiceImpl.class); @Value("${payment.enckey}") private String paymentEncryKey; @Value("${payment.mid}") private String paymentMid; @Value("${payment.callbackurl}") private String paymentcallbackUrl; @Value("${payment.companyName}") private String paymentcompanyName; private final PaymentStartRecordRepository paymentStartRecordRepository; private final HttpService httpService; private final EncryptionUtil encryptionUtil; private final Helper helper; private final BharatConnectMasterRepository bharatConnectMasterRepository; private final BBPSMasterDataRepo bbpsMasterDataRepo; private final BillerMapperHelper bbpsMapperHelper ; private final ObjectMapper objectMapper = new ObjectMapper(); public PaymentStartServiceImpl(PaymentStartRecordRepository paymentStartRecordRepository, HttpService httpService, EncryptionUtil encryptionUtil, Helper helper, BharatConnectMasterRepository bharatConnectMasterRepository, BBPSMasterDataRepo bbpsMasterDataRepo, BillerMapperHelper bbpsMapperHelper) { this.paymentStartRecordRepository = paymentStartRecordRepository; this.httpService = httpService; this.encryptionUtil = encryptionUtil; this.helper = helper; this.bharatConnectMasterRepository = bharatConnectMasterRepository; this.bbpsMasterDataRepo = bbpsMasterDataRepo; this.bbpsMapperHelper = bbpsMapperHelper; } @Override public ResponseModel startIntentPayment(PaymentStartUserRequest request) { String requestId = request.getReqId(); String apiName = BBPSConstant.API_START_PAYMENT; 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 String requestJson = helper.convertObjectToJson(request); InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnId, apiName, request.getMid(), BBPSConstant.HTTP_POST, requestJson); LOGGER.debug("[startIntentPayment] inbound log created txnId={} requestId={}", txnId, requestId); // prepare and encrypt payload PaymentDetailSentRequest sent = createPaymentSentRequest(request); String orderNo = BBPSConstant.ORDER_PREFIX + helper.generateTxnId(12); sent.setOrderNo(orderNo); sent.setEnckey(paymentEncryKey); String plaintext = helper.convertObjectToJson(sent); String encrypted = encryptionUtil.encryptMData(plaintext, paymentEncryKey); PaymentRequestPayload finalPayload = new PaymentRequestPayload(); finalPayload.setMid(paymentMid); finalPayload.setPayload(encrypted); String forwardJson = helper.convertObjectToJson(finalPayload); LOGGER.info("FINAL PAYLODA :{}",forwardJson); 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); // 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))); String respBody = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); helper.updateOutboundLogResponse(outboundLog, respBody); 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); 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); throw new CustomException(BBPSConstant.MSG_EMPTY_RESPONSE_PROVIDER, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, null); } // assume response has fixed format: { statusCode, message, responseData: { amount, orderNo, createdOn, qrString, mid, txnId } } PaymentStartApiResponse apiResponse; try { apiResponse = objectMapper.readValue(respBody, PaymentStartApiResponse.class); } catch (JsonProcessingException e) { LOGGER.error("[startIntentPayment] invalid JSON for expected response shape txnId={} err={}", txnId, e.getMessage(), e); helper.updateInboundLogFailure(inboundLog, respBody, BBPSConstant.JSON_ERROR); throw new CustomException(BBPSConstant.MSG_INVALID_JSON_FROM_API, BBPSConstant.FAILED_STR, BBPSConstant.HTTP_INTERNAL_SERVER_ERROR, respBody); } // interpret numeric status int outcomeCode = BBPSConstant.HTTP_OK_INT; try { outcomeCode = Integer.parseInt(String.valueOf(apiResponse.getStatusCode())); } catch (Exception ignored) { } if (!(outcomeCode == BBPSConstant.HTTP_OK_INT || outcomeCode == BBPSConstant.STATUS_CODE_205 || outcomeCode == BBPSConstant.STATUS_CODE_206)) { helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(outcomeCode)); LOGGER.error("[startIntentPayment] provider returned failure status txnId={} code={} message={}", txnId, outcomeCode, apiResponse.getMessage()); throw new CustomException(apiResponse.getMessage() == null ? "Upstream failure" : apiResponse.getMessage(), BBPSConstant.FAILED_STR, outcomeCode, respBody); } // success: persist and return helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(outcomeCode)); String responseType = (apiResponse.getResponseData() != null && apiResponse.getResponseData().getQrString() != null && !apiResponse.getResponseData().getQrString().isEmpty()) ? BBPSConstant.RESPONSE_TYPE_QR : BBPSConstant.RESPONSE_TYPE_JSON; bbpsMapperHelper.savePaymentStartRecord(request, apiResponse, txnId, orderNo, requestJson, respBody, responseType); LOGGER.info("[startIntentPayment] saved record txnId={} orderNo={} responseType={}", txnId, orderNo, responseType); PaymentStartReturnDto responseData = new PaymentStartReturnDto(); responseData.setAmount(apiResponse.getResponseData().getAmount()); responseData.setCreated_on(apiResponse.getResponseData().getCreatedOn()); responseData.setOrderNo(apiResponse.getResponseData().getOrderNo()); responseData.setQrString(apiResponse.getResponseData().getQrString()); responseData.setTxnId(apiResponse.getResponseData().getTxnId()); LOGGER.info("[startIntentPayment] complete txnId={} orderNo={} returning SUCCESS", txnId, orderNo); return ResponseModel.builder() .status(BBPSConstant.SUCCESS_RESPONSE) .statusCode(BBPSConstant.HTTP_OK_INT) .message(apiResponse.getMessage()) .data(responseData) .build(); } private PaymentDetailSentRequest createPaymentSentRequest(PaymentStartUserRequest request) { PaymentDetailSentRequest sent = new PaymentDetailSentRequest(); sent.setAmount(request.getAmount()); sent.setCurrency(request.getCurrency()); sent.setEmailId(request.getEmailId()); sent.setUserId(request.getMid()); sent.setCustomerVpa(request.getCustomerVpa()); sent.setDateOfReg(BBPSConstant.DEFAULT_DATE_OF_REG); // String effectiveMid = (request.getMid() != null && !request.getMid().isEmpty()) ? request.getMid() : paymentMid; sent.setMid(paymentMid); sent.setMobileNo(request.getMobileNo()); sent.setRespUrl(paymentcallbackUrl); sent.setTxnReqType(BBPSConstant.TXN_REQ_TYPE); sent.setName(request.getName()); sent.setUdf1(request.getUdf1()); sent.setUdf2(request.getUdf2()); sent.setUdf3(request.getUdf3()); sent.setUdf4(request.getUdf4()); sent.setUdf5(request.getUdf5()); sent.setUdf6(request.getUdf6()); sent.setUdf7(request.getUdf7()); sent.setUdf8(request.getUdf8()); sent.setUdf9(request.getUdf9()); sent.setUdf10(request.getUdf10()); sent.setUdf11(request.getUdf11()); sent.setUdf12(request.getUdf12()); sent.setUdf13(request.getUdf13()); sent.setUdf14(request.getUdf14()); LOGGER.debug("[createPaymentSentRequest] built sent request mid={} amount={}", sent.getMid(), sent.getAmount()); 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()); helper.validateMerchant(request.getMid()); // 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); // 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); LOGGER.debug("[startWebPayment] plaintext prepared 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); 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); // ----------- CALL API (FORM URL-ENCODED) ----------- Map responseMap = httpService.postFormRequest(BBPSConstant.UAT_PAYMENT_URL_WEB, formPayload); 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); // ---- 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); } // ---- 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); } // 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) String responseTypeWeb = BBPSConstant.RESPONSE_TYPE_HTML; String rawResponseWeb = respBody == null ? "" : respBody; // Try to extract useful fields from returned HTML (txnId, amount, mid) 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(); String parsedTxn = parsed.getOrDefault("txnId", ""); String parsedAmount = parsed.getOrDefault("amount", "0.00"); String parsedMid = parsed.getOrDefault("mid", paymentMid == null ? "DUMMY_MID" : paymentMid); // prefer HTML-provided txnId if present, otherwise reuse generated txnId rd.setTxnId(parsedTxn != null && !parsedTxn.isEmpty() ? parsedTxn : txnId); rd.setAmount(parsedAmount != null && !parsedAmount.isEmpty() ? parsedAmount : "0.00"); rd.setMid(parsedMid); rd.setOrderNo(orderNo); rd.setQrString("HTML_RESPONSE"); rd.setCreatedOn(LocalDateTime.now().toString()); apiResp.setResponseData(rd); apiResp.setMessage("HTML_RESPONSE"); apiResp.setStatusCode(String.valueOf(BBPSConstant.HTTP_OK_INT)); bbpsMapperHelper.savePaymentStartRecord(request, apiResp, txnId, orderNo, requestJson, rawResponseWeb, responseTypeWeb); LOGGER.info("[startWebPayment] saved payment start record txnId={} orderNo={} responseType={}", txnId, orderNo, responseTypeWeb); // Return HTML directly in API response Map result = new HashMap<>(); result.put("OrderId", orderNo); result.put("html", respBody); result.put("txnId", parsedTxn); LOGGER.info("[startWebPayment] complete txnId={} orderNo={} returning HTML", txnId, orderNo); return ResponseModel.builder() .status(BBPSConstant.SUCCESS_RESPONSE) .statusCode(BBPSConstant.HTTP_OK_INT) .message(BBPSConstant.HTML_RESPONSE_MESSAGE) .data(result) .build(); } @Override public ResponseModel handleStartCallback(CallbackRequest callbackRequest) { String apiName = "HANDLE_START_CALLBACK"; LOGGER.info("[{}] START", apiName); if (callbackRequest == null) { LOGGER.warn("[{}] Callback payload is null", apiName); return new ResponseModel("Invalid callback", BBPSConstant.MERCHANT_ACK_NO_MATCH, BBPSConstant.HTTP_OK_INT, null); } String mid = callbackRequest.getMid(); String encryptedData = callbackRequest.getData(); String signature = callbackRequest.getSignature(); LOGGER.debug("[{}] received callback mid={} signaturePresent={}", apiName, mid, signature != null); // Decrypt data String decrypted = encryptionUtil.decryptMData(encryptedData,paymentEncryKey); if (decrypted == null) { LOGGER.error("[{}] Failed to decrypt callback payload for mid={}", apiName, mid); return new ResponseModel(BBPSConstant.MSG_DECRYPTION_FAILED, String.valueOf(BBPSConstant.STATUS_CODE_207), BBPSConstant.HTTP_OK_INT, null); } 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); } String txnId = String.valueOf(payloadMap.getOrDefault("txnId", payloadMap.getOrDefault("txn_id", ""))); String orderNo = String.valueOf(payloadMap.getOrDefault("orderNo", payloadMap.getOrDefault("order_no", ""))); String txnStatus = String.valueOf(payloadMap.getOrDefault("txnStatus", payloadMap.getOrDefault("txn_status", payloadMap.getOrDefault("status", "")))); LOGGER.debug("[{}] parsed callback txnId={} orderNo={} txnStatus={}", apiName, txnId, orderNo, txnStatus); // Compute checksum: Mid + txnId + orderNo + txnStatus String checksumStr = mid + txnId + orderNo + txnStatus; String computedChecksum = encryptionUtil.getChecksumCRC32(checksumStr); boolean signatureMatches = false; if (computedChecksum != null && signature != null) { signatureMatches = computedChecksum.equals(signature) || computedChecksum.equals(String.valueOf(Long.parseLong(signature))); } LOGGER.debug("[{}] checksum computed={} provided={} matches={}", apiName, computedChecksum, signature, signatureMatches); // Find DB record Optional recordOpt = Optional.empty(); if (orderNo != null && !orderNo.isEmpty()) { recordOpt = paymentStartRecordRepository.findByOrderNo(orderNo); LOGGER.debug("[{}] looked up by orderNo={} found={}", apiName, orderNo, recordOpt.isPresent()); } if (recordOpt.isEmpty() && txnId != null && !txnId.isEmpty()) { recordOpt = paymentStartRecordRepository.findByTxnId(txnId); LOGGER.debug("[{}] looked up by txnId={} found={}", apiName, txnId, recordOpt.isPresent()); } PaymentStartRecord record = recordOpt.orElse(null); if (record == null) { LOGGER.warn("[{}] No PaymentStartRecord found for orderNo={} txnId={}", apiName, orderNo, txnId); // still return 205 to acknowledge receipt return new ResponseModel(BBPSConstant.MSG_NO_MATCHING_RECORD, BBPSConstant.MERCHANT_ACK_NO_MATCH, BBPSConstant.HTTP_OK_INT, null); } // Update record with bank response and signature record.setBankResponse(decrypted); record.setSignature(signature); // update txn status and callback timestamp record.setTxnStatus(txnStatus); record.setCallbackReceivedOn(java.time.LocalDateTime.now().toString()); String merchantStatus; if (!signatureMatches) { LOGGER.error("[{}] Signature mismatch. computed={} provided={}", apiName, computedChecksum, signature); record.setCallbackStatus(BBPSConstant.SIGNATURE_MISMATCH); paymentStartRecordRepository.save(record); merchantStatus = BBPSConstant.MERCHANT_ACK_REJECT; // treat as rejected/failure LOGGER.info("[{}] END | returning status={}", apiName, merchantStatus); return new ResponseModel(BBPSConstant.MSG_SIGNATURE_MISMATCH, merchantStatus, BBPSConstant.HTTP_OK_INT, null); } // signature ok - decide response based on txnStatus if (txnStatus.equalsIgnoreCase("SUCCESS") || txnStatus.equalsIgnoreCase("SUCCESSFUL") || txnStatus.equalsIgnoreCase("SUCCESSFULL")) { record.setCallbackStatus(BBPSConstant.CALLBACK_STATUS_SUCCESS); paymentStartRecordRepository.save(record); merchantStatus = BBPSConstant.MERCHANT_ACK_OK; // accepted by merchant LOGGER.info("[{}] Callback success persisted orderNo={} txnId={}", apiName, orderNo, txnId); } else if (txnStatus.equalsIgnoreCase("FAILED") || txnStatus.equalsIgnoreCase("FAIL")) { record.setCallbackStatus(BBPSConstant.CALLBACK_STATUS_FAILED); paymentStartRecordRepository.save(record); merchantStatus = BBPSConstant.MERCHANT_ACK_REJECT; // rejected LOGGER.info("[{}] Callback failure persisted orderNo={} txnId={}", apiName, orderNo, txnId); } else { record.setCallbackStatus(BBPSConstant.CALLBACK_STATUS_RECEIVED); paymentStartRecordRepository.save(record); merchantStatus = BBPSConstant.MERCHANT_ACK_NO_MATCH; // generic ack LOGGER.info("[{}] Callback received persisted orderNo={} txnId={} status={}", apiName, orderNo, txnId, txnStatus); } LOGGER.info("[{}] END", apiName); 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); 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")); 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); } return map; } }