Files
bbps-backend/src/main/java/com/billpaymentservice/bbps/serviceImpl/PaymentStartServiceImpl.java
T

474 lines
22 KiB
Java

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<String, String> 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<String, Object> top = objectMapper.readValue(respBody, new TypeReference<Map<String, Object>>(){});
// Pick inner payload if present
Map<String, Object> payload = top;
if (top.containsKey("data") && top.get("data") instanceof Map) {
//noinspection unchecked
payload = (Map<String, Object>) 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<String, Object> 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<String, String> 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<String, String> 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<String, Object> payloadMap;
try {
payloadMap = objectMapper.readValue(decrypted, new TypeReference<Map<String, Object>>(){});
} 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<PaymentStartRecord> 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);
}
}