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 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 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 checkStatus(String providerReference) { return lifecycle.checkStatus(provider(), providerReference, this::queryProvider); } private Mono 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; } } }