RefundController.java

package com.ecommerce.payment.interfaces.rest;

import com.ecommerce.payment.application.model.PaymentModels.RefundCallbackCommand;
import com.ecommerce.payment.application.model.PaymentModels.CreatePaymentExceptionRefundCommand;
import com.ecommerce.payment.application.model.PaymentModels.PaymentExceptionRefundAuditView;
import com.ecommerce.payment.application.model.PaymentModels.RefundDispatchRetryAuditView;
import com.ecommerce.payment.application.model.PaymentModels.RefundView;
import com.ecommerce.payment.application.model.PaymentModels.RetryRefundDispatchCommand;
import com.ecommerce.payment.application.service.RefundService;
import com.ecommerce.platform.common.api.ApiResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.util.List;

@Validated
@RestController
@RequestMapping("/api/v1/payment")
public class RefundController {

    private static final String BUSINESS_NO_PATTERN = "[A-Za-z0-9._:-]+";

    private final RefundService refundService;
    private final ObjectMapper objectMapper;

    public RefundController(RefundService refundService, ObjectMapper objectMapper) {
        this.refundService = refundService;
        this.objectMapper = objectMapper;
    }

    @GetMapping("/refunds/{refundNo}")
    public ApiResponse<RefundView> refund(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String refundNo) {
        return ApiResponse.success(refundService.getForUser(Long.valueOf(jwt.getSubject()), refundNo));
    }

    @GetMapping("/refunds/by-after-sale/{afterSaleNo}")
    public ApiResponse<RefundView> refundByAfterSale(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String afterSaleNo) {
        return ApiResponse.success(
                refundService.getByAfterSaleNoForUser(Long.valueOf(jwt.getSubject()), afterSaleNo));
    }

    @PostMapping("/admin/refunds/{refundNo}/retry-dispatch")
    public ApiResponse<RefundView> retryDispatch(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String refundNo,
            @RequestHeader("Idempotency-Key")
            @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String commandId,
            @Valid @RequestBody RetryRefundDispatchRequest request) {
        return ApiResponse.success(refundService.retryDispatch(new RetryRefundDispatchCommand(
                refundNo, commandId, jwt.getSubject(), request.reason())));
    }

    @GetMapping("/admin/refunds/{refundNo}/retry-dispatch/audits")
    public ApiResponse<List<RefundDispatchRetryAuditView>> retryDispatchAudits(
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String refundNo,
            @RequestParam(defaultValue = "50") @Min(1) @Max(100) int limit) {
        return ApiResponse.success(refundService.listRetryAudits(refundNo, limit));
    }

    @PostMapping("/admin/payments/{paymentNo}/exception-refunds")
    public ApiResponse<RefundView> createPaymentExceptionRefund(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String paymentNo,
            @RequestHeader("Idempotency-Key")
            @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String commandId,
            @Valid @RequestBody CreatePaymentExceptionRefundRequest request) {
        return ApiResponse.success(refundService.createPaymentExceptionRefund(
                new CreatePaymentExceptionRefundCommand(
                        paymentNo,
                        commandId,
                        jwt.getSubject(),
                        request.reason())));
    }

    @GetMapping("/admin/payments/{paymentNo}/exception-refunds/audits")
    public ApiResponse<List<PaymentExceptionRefundAuditView>>
            paymentExceptionRefundAudits(
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String paymentNo,
            @RequestParam(defaultValue = "50") @Min(1) @Max(100) int limit) {
        return ApiResponse.success(
                refundService.listPaymentExceptionRefundAudits(paymentNo, limit));
    }

    @PostMapping("/callbacks/mock/refunds")
    public ApiResponse<RefundView> mockRefundCallback(@Valid @RequestBody MockRefundCallbackRequest request) {
        return ApiResponse.success(refundService.processMockCallback(new RefundCallbackCommand(
                request.refundNo(), request.externalEventId(), request.externalRefundNo(), request.status(),
                request.amount(), request.timestamp(), request.signature(), raw(request))));
    }

    private String raw(MockRefundCallbackRequest request) {
        try {
            return objectMapper.writeValueAsString(request);
        } catch (JsonProcessingException exception) {
            throw new IllegalArgumentException("Refund callback payload cannot be serialized", exception);
        }
    }

    public record MockRefundCallbackRequest(
            @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String refundNo,
            @NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String externalEventId,
            @NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String externalRefundNo,
            @NotBlank @Pattern(regexp = "SUCCESS|FAILED") String status,
            @NotNull @DecimalMin("0.00") BigDecimal amount,
            long timestamp,
            @NotBlank @Pattern(regexp = "[a-fA-F0-9]{64}") String signature
    ) {
    }

    public record RetryRefundDispatchRequest(
            @NotBlank @Size(max = 200) String reason
    ) {
    }

    public record CreatePaymentExceptionRefundRequest(
            @NotBlank @Size(max = 200) String reason
    ) {
    }
}