Updates and payments additions
This commit is contained in:
124
src/main/java/com/test/payment/service/MtnService.java
Normal file
124
src/main/java/com/test/payment/service/MtnService.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.test.payment.service;
|
||||
|
||||
import com.test.payment.client.MtnClient;
|
||||
import com.test.payment.dto.CallbackAckDto;
|
||||
import com.test.payment.dto.MtnPayRequestDto;
|
||||
import com.test.payment.dto.MtnStatusResponseDto;
|
||||
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.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* MTN MoMo Collections (request-to-pay). We generate the X-Reference-Id UUID and use
|
||||
* it as the provider reference; externalId is set to the same value so callbacks can
|
||||
* be correlated. Status values: PENDING | SUCCESSFUL | FAILED.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MtnService implements PaymentProviderService {
|
||||
|
||||
private final MtnClient mtnClient;
|
||||
private final PaymentLifecycleService lifecycle;
|
||||
private final Environment environment;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public String provider() {
|
||||
return PaymentProviderType.MTN.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PaymentResultDto> initiatePayment(PaymentRequest request) {
|
||||
return lifecycle.saveInitiation(provider(), request)
|
||||
.flatMap(initiation -> Mono.defer(() -> {
|
||||
String reference = UUID.randomUUID().toString();
|
||||
// 202 Accepted, empty body — the reference is all we get back
|
||||
return mtnClient.requestToPay(reference, buildRequest(request, reference))
|
||||
.thenReturn(new ProviderResponseData(
|
||||
reference, null, "202", "Accepted", "Request to pay accepted", true));
|
||||
})
|
||||
.flatMap(data -> lifecycle.persistResponse(initiation, data))
|
||||
.onErrorResume(ex -> lifecycle.markFailed(initiation, ex)));
|
||||
}
|
||||
|
||||
public Mono<CallbackAckDto> handleCallback(MtnStatusResponseDto payload) {
|
||||
if (payload == null || (payload.getReferenceId() == null && payload.getExternalId() == null)) {
|
||||
log.warn("Received malformed MTN callback payload");
|
||||
return Mono.just(CallbackAckDto.accepted("Ignored: empty callback"));
|
||||
}
|
||||
String reference = payload.getReferenceId() != null ? payload.getReferenceId() : payload.getExternalId();
|
||||
boolean success = "SUCCESSFUL".equalsIgnoreCase(payload.getStatus());
|
||||
CallbackData data = new CallbackData(
|
||||
reference,
|
||||
payload.getStatus(),
|
||||
success ? "Payment successful" : payload.reasonText(),
|
||||
payload.getFinancialTransactionId(),
|
||||
payload.getAmount() != null ? new BigDecimal(payload.getAmount()) : null,
|
||||
payload.getPayer() != null ? payload.getPayer().getPartyId() : 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 mtnClient.status(response.getProviderReference())
|
||||
.map(result -> new QueryOutcome(
|
||||
mapStatus(result.getStatus()),
|
||||
result.getStatus(),
|
||||
"SUCCESSFUL".equalsIgnoreCase(result.getStatus()) ? "Payment successful" : result.reasonText(),
|
||||
result.getFinancialTransactionId()))
|
||||
.onErrorResume(ProviderBusyException.class,
|
||||
e -> Mono.just(QueryOutcome.pending("MTN status query rate-limited — showing last known state")));
|
||||
}
|
||||
|
||||
private MtnPayRequestDto buildRequest(PaymentRequest request, String reference) {
|
||||
return new MtnPayRequestDto(
|
||||
String.valueOf(request.getAmount()),
|
||||
environment.getProperty("mtn.currency", "EUR"),
|
||||
reference,
|
||||
new MtnPayRequestDto.Payer("MSISDN", request.getPhoneNumber()),
|
||||
request.getTransactionDesc(),
|
||||
request.getAccountReference());
|
||||
}
|
||||
|
||||
private TransactionStatus mapStatus(String mtnStatus) {
|
||||
if ("SUCCESSFUL".equalsIgnoreCase(mtnStatus)) {
|
||||
return TransactionStatus.SUCCESS;
|
||||
}
|
||||
if ("FAILED".equalsIgnoreCase(mtnStatus)) {
|
||||
return TransactionStatus.FAILED;
|
||||
}
|
||||
return TransactionStatus.PENDING;
|
||||
}
|
||||
|
||||
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