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.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); private String uatPaymentUrlweb="https://uat.godemo.in/api/UATIntegration/uat/payin/paymentrequest"; private String uatPaymentUrlintent="https://uat.godemo.in/api/UATIntegration/paymentrequest/seamless"; 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 = "StartPayment"; String txnId = helper.generateTxnId(15); helper.validateMerchant(request.getMid()); // inbound log String requestJson = helper.convertObjectToJson(request); InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnId, apiName, request.getMid(), BBPSConstant.HTTP_POST, requestJson); // Prepare request payload PaymentDetailSentRequest sent = createPaymentSentRequest(request); String orderNo = "ORD" + helper.generateTxnId(12); sent.setOrderNo(orderNo); String plaintext = helper.convertObjectToJson(sent); String encKey = BBPSConstant.enckey; sent.setEnckey(encKey); String encrypted = encryptionUtil.encryptMData(plaintext, encKey); PaymentRequestPayload finalPayload = new PaymentRequestPayload(); finalPayload.setMid(BBPSConstant.mid); finalPayload.setPayload(encrypted); String forwardJson = helper.convertObjectToJson(finalPayload); LOGGER.info("Payment Start Request Payload: " + forwardJson); OutboundTraceLog outboundLog = helper.createOutboundLog(txnId, requestId, apiName, uatPaymentUrlintent, BBPSConstant.HTTP_POST, forwardJson); // --- CALL EXTERNAL API --- Map responseMap = httpService.postRequest(uatPaymentUrlintent, forwardJson, null); LOGGER.info("Payment Start Response: " + responseMap); int httpCode = Integer.parseInt(responseMap.getOrDefault("respCode", "500")); String respBody = responseMap.get(BBPSConstant.RESPONSE_DATA_KEY); helper.updateOutboundLogResponse(outboundLog, respBody); // --- HTTP FAILURE --- if (httpCode != 200) { helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(httpCode)); throw new CustomException( "Upstream HTTP error", "FAILED", httpCode, respBody ); } if (respBody == null || respBody.isEmpty()) { helper.updateInboundLogFailure(inboundLog, "Empty response from payment provider", "500"); throw new CustomException( "Empty response from payment provider", "FAILED", 500, null ); } // --- Convert JSON to object --- PaymentStartApiResponse apiResponse = null; try { Map top = objectMapper.readValue(respBody, new TypeReference>(){}); // Pick inner payload if present Map payload = top; if (top.containsKey("data") && top.get("data") instanceof Map) { //noinspection unchecked payload = (Map) top.get("data"); } // Simple failure detection: status == Failure OR statusCode != 200 String status = String.valueOf(payload.getOrDefault("status", top.getOrDefault("status", ""))).trim(); int outcomeCode = 0; Object scObj = payload.getOrDefault("statusCode", top.get("statusCode")); // prefer numeric status field if present if (status.matches("\\d+")) { try { outcomeCode = Integer.parseInt(status); } catch (NumberFormatException ignore) {} } // if not present, fall back to statusCode if (outcomeCode == 0 && scObj != null) { try { outcomeCode = Integer.parseInt(String.valueOf(scObj)); } catch (NumberFormatException ignore) {} } // default to 200 when nothing provided if (outcomeCode == 0) outcomeCode = 200; String message = String.valueOf(payload.getOrDefault("message", top.getOrDefault("message", ""))); // Handle special cases if (outcomeCode == 200 || outcomeCode == 205 || outcomeCode == 206) { // success - build apiResponse from wrapper or direct payload if (top.containsKey("message") && (top.containsKey("responseData") || top.containsKey("data"))) { apiResponse = objectMapper.convertValue(top, PaymentStartApiResponse.class); } else { Map dataMap = payload; PaymentStartApiResponse.ResponseData rd = new PaymentStartApiResponse.ResponseData(); rd.setAmount(String.valueOf(dataMap.getOrDefault("amount", ""))); rd.setOrderNo(String.valueOf(dataMap.getOrDefault("orderNo", dataMap.getOrDefault("order_no", "")))); rd.setCreatedOn(String.valueOf(dataMap.getOrDefault("created_on", dataMap.getOrDefault("createdOn", "")))); rd.setQrString(String.valueOf(dataMap.getOrDefault("qrString", dataMap.getOrDefault("qr_string", "")))); rd.setMid(String.valueOf(dataMap.getOrDefault("mid", ""))); rd.setTxnId(String.valueOf(dataMap.getOrDefault("txnId", dataMap.getOrDefault("txn_id", "")))); apiResponse = new PaymentStartApiResponse(); apiResponse.setMessage(String.valueOf(top.getOrDefault("message", "SUCCESS"))); apiResponse.setStatusCode(String.valueOf(outcomeCode)); apiResponse.setResponseData(rd); } helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(outcomeCode)); // proceed normally } else if (outcomeCode == 198) { // transaction processing - treat as non-final success/pending helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(outcomeCode)); return ResponseModel.builder() .status(BBPSConstant.SENT_TO_PROCESSOR_STATUS) .statusCode(outcomeCode) .message(message == null || message.isEmpty() ? "Transaction processing" : message) .data(null) .build(); } else if (outcomeCode == 1003) { // warning - return a WARNING response; don't throw immediately helper.updateInboundLogSuccess(inboundLog, respBody, String.valueOf(outcomeCode)); return ResponseModel.builder() .status("WARNING") .statusCode(outcomeCode) .message(message == null || message.isEmpty() ? "Warning from merchant" : message) .data(null) .build(); } else if (outcomeCode == 207) { // Fraud rejected - explicit failure, handle exception helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(outcomeCode)); throw new CustomException(message == null || message.isEmpty() ? "Fraud Rejected" : message, "FAILED", outcomeCode, respBody); } else { // Other failure codes (417,99,197,194,193 etc.) helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(outcomeCode)); throw new CustomException(message == null || message.isEmpty() ? "Upstream failure" : message, "FAILED", outcomeCode, respBody); } } catch (JsonProcessingException e) { LOGGER.error("Error parsing payment start API response JSON: {}", e.getMessage(), e); helper.updateInboundLogFailure(inboundLog, respBody, BBPSConstant.JSON_ERROR); throw new CustomException("Invalid JSON received from payment API"); } // Save record // Determine response type: QR if qrString present, else JSON String responseType = "JSON"; String rawResponse = respBody == null ? "" : respBody; if (apiResponse != null && apiResponse.getResponseData() != null && apiResponse.getResponseData().getQrString() != null && !apiResponse.getResponseData().getQrString().isEmpty()) { responseType = "QR"; } bbpsMapperHelper.savePaymentStartRecord(request, apiResponse, txnId, orderNo, requestJson, rawResponse, responseType); // Prepare DTO 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()); helper.updateInboundLogSuccess(inboundLog, respBody, apiResponse.getStatusCode() == null ? "200" : apiResponse.getStatusCode().toString()); return ResponseModel.builder() .status("SUCCESS") .statusCode(httpCode) .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("2025-12-31"); String effectiveMid = (request.getMid() != null && !request.getMid().isEmpty()) ? request.getMid() : BBPSConstant.mid; sent.setMid(BBPSConstant.mid); sent.setMobileNo(request.getMobileNo()); sent.setRespUrl(BBPSConstant.callbackurl); sent.setTxnReqType("S"); 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()); return sent; } @Override public ResponseModel startWebPayment(PaymentStartUserRequest request) { String requestId = request.getReqId(); String apiName = "StartPayment"; String txnId = helper.generateTxnId(15); helper.validateMerchant(request.getMid()); // inbound log String requestJson = helper.convertObjectToJson(request); InboundTraceLog inboundLog = helper.createInboundLog(requestId, txnId, apiName, request.getMid(), BBPSConstant.HTTP_POST, requestJson); // Prepare request payload PaymentDetailSentRequest sent = createPaymentSentRequest(request); String orderNo = "ORD" + helper.generateTxnId(12); sent.setOrderNo(orderNo); String plaintext = helper.convertObjectToJson(sent); String encKey = BBPSConstant.enckey; sent.setEnckey(encKey); String encrypted = encryptionUtil.encryptMData(plaintext, encKey); // ----------- BUILD x-www-form-urlencoded PAYLOAD ----------- String formPayload = "mid=" + URLEncoder.encode(BBPSConstant.mid, StandardCharsets.UTF_8) + "&payload=" + URLEncoder.encode(encrypted, StandardCharsets.UTF_8); LOGGER.info("Form Payload: {}", formPayload); OutboundTraceLog outboundLog = helper.createOutboundLog(txnId, requestId, apiName, uatPaymentUrlweb, BBPSConstant.HTTP_POST, formPayload); // ----------- CALL API (FORM URL-ENCODED) ----------- Map responseMap = httpService.postFormRequest(uatPaymentUrlweb, formPayload); LOGGER.info("Payment Start Response: {}", responseMap); int httpCode = Integer.parseInt(responseMap.getOrDefault("respCode", "500")); String respBody = responseMap.getOrDefault("data", ""); helper.updateOutboundLogResponse(outboundLog, respBody); // ---- HTTP FAILURE ---- if (httpCode != 200) { helper.updateInboundLogFailure(inboundLog, respBody, String.valueOf(httpCode)); throw new CustomException("Upstream HTTP Error", "FAILED", httpCode, respBody); } // ---- EMPTY RESPONSE ---- if (respBody.isEmpty()) { helper.updateInboundLogFailure(inboundLog, "Empty response from PG", "500"); throw new CustomException("Empty response from PG", "FAILED", 500, null); } // ❗❗ PG RETURNS HTML → NO JSON PARSING REQUIRED helper.updateInboundLogSuccess(inboundLog, respBody, "200"); // Save record with HTML (persist original request and HTML response) String responseTypeWeb = "HTML"; String rawResponseWeb = respBody == null ? "" : respBody; // bbpsMapperHelper.savePaymentStartRecord(request, null, txnId, orderNo, requestJson, rawResponseWeb, responseTypeWeb); // Return HTML directly in API response Map result = new HashMap<>(); result.put("html", respBody); return ResponseModel.builder() .status("SUCCESS") .statusCode(200) .message("HTML Response Received") .data(result) .build(); } @Override public ResponseModel handleStartCallback(CallbackRequest callbackRequest) { String apiName = "HANDLE_START_CALLBACK"; LOGGER.info("[{}] START | payload={}", apiName, callbackRequest); if (callbackRequest == null) { LOGGER.warn("Callback payload is null"); return new ResponseModel("Invalid callback", "205", 200, null); } String mid = callbackRequest.getMid(); String encryptedData = callbackRequest.getData(); String signature = callbackRequest.getSignature(); // Decrypt data String decrypted = encryptionUtil.decryptMData(encryptedData, BBPSConstant.enckey); if (decrypted == null) { LOGGER.error("Failed to decrypt callback payload for mid={}", mid); return new ResponseModel("Decryption failed", "207", 200, null); } LOGGER.info("Decrypted callback data: {}", decrypted); // Parse decrypted JSON Map payloadMap; try { payloadMap = objectMapper.readValue(decrypted, new TypeReference>(){}); } catch (JsonProcessingException e) { LOGGER.error("Failed to parse decrypted callback JSON: {}", e.getMessage(), e); return new ResponseModel("Invalid decrypted JSON", "207", 200, 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", "")))); // 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))); } // Find DB record Optional recordOpt = Optional.empty(); if (orderNo != null && !orderNo.isEmpty()) { recordOpt = paymentStartRecordRepository.findByOrderNo(orderNo); } if (recordOpt.isEmpty() && txnId != null && !txnId.isEmpty()) { recordOpt = paymentStartRecordRepository.findByTxnId(txnId); } PaymentStartRecord record = recordOpt.orElse(null); if (record == null) { LOGGER.warn("No PaymentStartRecord found for orderNo={} txnId={}", orderNo, txnId); // still return 205 to acknowledge receipt return new ResponseModel("No matching record", "205", 200, 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={}", computedChecksum, signature); record.setCallbackStatus("SIGNATURE_MISMATCH"); paymentStartRecordRepository.save(record); merchantStatus = "207"; // treat as rejected/failure LOGGER.info("[{}] END | returning status={}", apiName, merchantStatus); return new ResponseModel("Signature mismatch", merchantStatus, 200, null); } // signature ok - decide response based on txnStatus if (txnStatus.equalsIgnoreCase("SUCCESS") || txnStatus.equalsIgnoreCase("SUCCESSFUL") || txnStatus.equalsIgnoreCase("SUCCESSFULL")) { record.setCallbackStatus("SUCCESS"); paymentStartRecordRepository.save(record); merchantStatus = "206"; // accepted by merchant } else if (txnStatus.equalsIgnoreCase("FAILED") || txnStatus.equalsIgnoreCase("FAIL")) { record.setCallbackStatus("FAILED"); paymentStartRecordRepository.save(record); merchantStatus = "207"; // rejected } else { record.setCallbackStatus("RECEIVED"); paymentStartRecordRepository.save(record); merchantStatus = "205"; // generic ack } LOGGER.info("Callback processed for orderNo={} txnId={} status={} returning merchantStatus={}", orderNo, txnId, txnStatus, merchantStatus); LOGGER.info("[{}] END", apiName); return new ResponseModel("Callback processed", merchantStatus, 200, null); } }