PaymentController.java

package com.ecommerce.payment.interfaces.rest;

import com.ecommerce.payment.application.model.PaymentModels.CallbackCommand;
import com.ecommerce.payment.application.model.PaymentModels.CreatePaymentCommand;
import com.ecommerce.payment.application.model.PaymentModels.PaymentView;
import com.ecommerce.payment.application.service.PaymentService;
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.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.RestController;

import java.math.BigDecimal;

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

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

    private final PaymentService paymentService;
    private final ObjectMapper objectMapper;

    public PaymentController(PaymentService paymentService, ObjectMapper objectMapper) {
        this.paymentService = paymentService;
        this.objectMapper = objectMapper;
    }

    @PostMapping("/payments")
    public ApiResponse<PaymentView> createPayment(
            @AuthenticationPrincipal Jwt jwt,
            @RequestHeader("Idempotency-Key")
            @Size(min = 8, max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String idempotencyKey,
            @Valid @RequestBody CreatePaymentRequest request) {
        return ApiResponse.success(paymentService.createPayment(new CreatePaymentCommand(
                Long.valueOf(jwt.getSubject()), idempotencyKey, request.orderNo(), request.channel())));
    }

    @GetMapping("/payments/{paymentNo}")
    public ApiResponse<PaymentView> payment(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String paymentNo) {
        return ApiResponse.success(paymentService.getPayment(Long.valueOf(jwt.getSubject()), paymentNo));
    }

    @GetMapping("/payments/by-idempotency-key/{key}")
    public ApiResponse<PaymentView> paymentByIdempotencyKey(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Size(min = 8, max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String key) {
        return ApiResponse.success(paymentService.getPaymentByIdempotencyKey(
                Long.valueOf(jwt.getSubject()), key));
    }

    @GetMapping("/payments/by-order/{orderNo}")
    public ApiResponse<PaymentView> paymentByOrder(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String orderNo) {
        return ApiResponse.success(paymentService.getPaymentByOrder(
                Long.valueOf(jwt.getSubject()), orderNo));
    }

    @PostMapping("/callbacks/mock")
    public ApiResponse<PaymentView> mockCallback(@Valid @RequestBody MockCallbackRequest request) {
        return ApiResponse.success(paymentService.processMockCallback(new CallbackCommand(
                request.paymentNo(), request.externalEventId(), request.externalTransactionNo(),
                request.status(), request.amount(), request.timestamp(), request.signature(), raw(request))));
    }

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

    public record CreatePaymentRequest(
            @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String orderNo,
            @NotBlank @Size(max = 32) @Pattern(regexp = "[A-Za-z]+") String channel
    ) {
    }

    public record MockCallbackRequest(
            @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String paymentNo,
            @NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String externalEventId,
            @NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String externalTransactionNo,
            @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
    ) {
    }
}