125 lines
5.4 KiB
Java
125 lines
5.4 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|