Updates and payments additions

This commit is contained in:
spiro-alvin-nyasimi
2026-07-02 16:45:48 +03:00
parent 36c25a8fe6
commit e9d216be78
98 changed files with 2623 additions and 1445 deletions

View 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;
}
}
}