Updates and payments additions
This commit is contained in:
138
src/main/java/com/test/payment/service/AirtelService.java
Normal file
138
src/main/java/com/test/payment/service/AirtelService.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.test.payment.service;
|
||||
|
||||
import com.test.payment.client.AirtelClient;
|
||||
import com.test.payment.dto.AirtelCallbackPayload;
|
||||
import com.test.payment.dto.AirtelPaymentRequestDto;
|
||||
import com.test.payment.dto.AirtelResponseDto;
|
||||
import com.test.payment.dto.CallbackAckDto;
|
||||
import com.test.payment.dto.PaymentRequest;
|
||||
import com.test.payment.dto.PaymentResultDto;
|
||||
import com.test.payment.dto.TransactionStatusDto;
|
||||
import com.test.payment.exceptions.ProviderBusyException;
|
||||
import com.test.payment.models.PaymentProviderType;
|
||||
import com.test.payment.models.PaymentResponse;
|
||||
import com.test.payment.models.TransactionStatus;
|
||||
import com.test.payment.service.PaymentLifecycleService.CallbackData;
|
||||
import com.test.payment.service.PaymentLifecycleService.ProviderResponseData;
|
||||
import com.test.payment.service.PaymentLifecycleService.QueryOutcome;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Airtel Money USSD-push collections. Transaction status codes:
|
||||
* TIP = in progress, TS = success, TF = failed.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AirtelService implements PaymentProviderService {
|
||||
|
||||
private final AirtelClient airtelClient;
|
||||
private final PaymentLifecycleService lifecycle;
|
||||
private final Environment environment;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public String provider() {
|
||||
return PaymentProviderType.AIRTEL.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PaymentResultDto> initiatePayment(PaymentRequest request) {
|
||||
return lifecycle.saveInitiation(provider(), request)
|
||||
.flatMap(initiation -> Mono.defer(() -> {
|
||||
String reference = "ATL" + UUID.randomUUID().toString().replace("-", "");
|
||||
return airtelClient.pay(buildRequest(request, reference))
|
||||
.map(response -> toResponseData(response, reference));
|
||||
})
|
||||
.flatMap(data -> lifecycle.persistResponse(initiation, data))
|
||||
.onErrorResume(ex -> lifecycle.markFailed(initiation, ex)));
|
||||
}
|
||||
|
||||
public Mono<CallbackAckDto> handleCallback(AirtelCallbackPayload payload) {
|
||||
if (payload == null || payload.getTransaction() == null) {
|
||||
log.warn("Received malformed Airtel callback payload");
|
||||
return Mono.just(CallbackAckDto.accepted("Ignored: empty callback"));
|
||||
}
|
||||
AirtelCallbackPayload.Transaction transaction = payload.getTransaction();
|
||||
boolean success = "TS".equalsIgnoreCase(transaction.getStatusCode());
|
||||
CallbackData data = new CallbackData(
|
||||
transaction.getId(),
|
||||
transaction.getStatusCode(),
|
||||
transaction.getMessage(),
|
||||
transaction.getAirtelMoneyId(),
|
||||
null, null, null,
|
||||
success);
|
||||
return lifecycle.applyCallback(provider(), data, toJson(payload));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TransactionStatusDto> checkStatus(String providerReference) {
|
||||
return lifecycle.checkStatus(provider(), providerReference, this::queryProvider);
|
||||
}
|
||||
|
||||
private Mono<QueryOutcome> queryProvider(PaymentResponse response) {
|
||||
return airtelClient.status(response.getProviderReference())
|
||||
.map(result -> {
|
||||
AirtelResponseDto.Transaction tx = result.getData() != null ? result.getData().getTransaction() : null;
|
||||
String status = tx != null ? tx.getStatus() : null;
|
||||
String message = tx != null && tx.getMessage() != null
|
||||
? tx.getMessage()
|
||||
: (result.getStatus() != null ? result.getStatus().getMessage() : null);
|
||||
return new QueryOutcome(mapStatus(status), status, message,
|
||||
tx != null ? tx.getAirtelMoneyId() : null);
|
||||
})
|
||||
.onErrorResume(ProviderBusyException.class,
|
||||
e -> Mono.just(QueryOutcome.pending("Airtel status query rate-limited — showing last known state")));
|
||||
}
|
||||
|
||||
private AirtelPaymentRequestDto buildRequest(PaymentRequest request, String reference) {
|
||||
String country = environment.getProperty("airtel.country", "KE");
|
||||
String currency = environment.getProperty("airtel.currency", "KES");
|
||||
return new AirtelPaymentRequestDto(
|
||||
request.getAccountReference(),
|
||||
new AirtelPaymentRequestDto.Subscriber(country, currency, request.getPhoneNumber()),
|
||||
new AirtelPaymentRequestDto.Transaction(
|
||||
String.valueOf(request.getAmount()), country, currency, reference));
|
||||
}
|
||||
|
||||
private ProviderResponseData toResponseData(AirtelResponseDto response, String reference) {
|
||||
AirtelResponseDto.Status status = response.getStatus();
|
||||
boolean accepted = status != null && Boolean.TRUE.equals(status.getSuccess());
|
||||
String transactionStatus = response.getData() != null && response.getData().getTransaction() != null
|
||||
? response.getData().getTransaction().getStatus()
|
||||
: null;
|
||||
return new ProviderResponseData(
|
||||
reference,
|
||||
null,
|
||||
status != null ? status.getCode() : null,
|
||||
status != null ? status.getMessage() : null,
|
||||
transactionStatus,
|
||||
accepted);
|
||||
}
|
||||
|
||||
private TransactionStatus mapStatus(String airtelStatus) {
|
||||
if ("TS".equalsIgnoreCase(airtelStatus)) {
|
||||
return TransactionStatus.SUCCESS;
|
||||
}
|
||||
if ("TF".equalsIgnoreCase(airtelStatus)) {
|
||||
return TransactionStatus.FAILED;
|
||||
}
|
||||
return TransactionStatus.PENDING; // TIP or unknown — keep waiting
|
||||
}
|
||||
|
||||
private String toJson(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not serialize callback payload: {}", e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user