TradeController.java

package com.ecommerce.trade.interfaces.rest;

import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.platform.common.api.CursorPageResponse;
import com.ecommerce.platform.common.api.PageResponse;
import com.ecommerce.trade.application.model.TradeModels.CartItemView;
import com.ecommerce.trade.application.model.TradeModels.CreateOrderCommand;
import com.ecommerce.trade.application.model.TradeModels.GuestBagItemCommand;
import com.ecommerce.trade.application.model.TradeModels.OrderLineCommand;
import com.ecommerce.trade.application.model.TradeModels.OrderView;
import com.ecommerce.trade.application.service.CartService;
import com.ecommerce.trade.application.service.TradeOrderService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
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.util.List;

@Validated
@RestController
@RequestMapping("/api/v1/trade")
public class TradeController {

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

    private final CartService cartService;
    private final TradeOrderService orderService;

    public TradeController(CartService cartService, TradeOrderService orderService) {
        this.cartService = cartService;
        this.orderService = orderService;
    }

    @PutMapping("/cart/items/{skuId}")
    public ApiResponse<CartItemView> putCartItem(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive Long skuId,
            @Valid @RequestBody PutCartItemRequest request) {
        return ApiResponse.success(cartService.putItem(
                userId(jwt), request.productId(), skuId, request.quantity(), request.selected()));
    }

    @GetMapping("/cart/items")
    public ApiResponse<List<CartItemView>> cartItems(@AuthenticationPrincipal Jwt jwt) {
        return ApiResponse.success(cartService.listItems(userId(jwt)));
    }

    @DeleteMapping("/cart/items/{skuId}")
    public ApiResponse<Void> removeCartItem(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive Long skuId) {
        cartService.removeItem(userId(jwt), skuId);
        return ApiResponse.success(null);
    }

    @PostMapping("/cart/guest-merge")
    public ApiResponse<List<CartItemView>> mergeGuestBag(
            @AuthenticationPrincipal Jwt jwt,
            @RequestHeader("Idempotency-Key")
            @Size(min = 8, max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String idempotencyKey,
            @Valid @RequestBody GuestBagMergeRequest request) {
        return ApiResponse.success(cartService.mergeGuestBag(
                userId(jwt),
                idempotencyKey,
                request.items().stream()
                        .map(item -> new GuestBagItemCommand(
                                item.productId(), item.skuId(), item.quantity()))
                        .toList()));
    }

    @PostMapping("/orders")
    public ApiResponse<OrderView> createOrder(
            @AuthenticationPrincipal Jwt jwt,
            @RequestHeader("Idempotency-Key")
            @Size(min = 8, max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN) String idempotencyKey,
            @Valid @RequestBody CreateOrderRequest request) {
        List<OrderLineCommand> items = request.items().stream()
                .map(item -> new OrderLineCommand(item.productId(), item.skuId(), item.quantity()))
                .toList();
        return ApiResponse.success(orderService.createOrder(
                new CreateOrderCommand(userId(jwt), idempotencyKey, request.addressId(), items,
                        request.benefitNos())));
    }

    @GetMapping("/orders")
    public ApiResponse<List<OrderView>> orders(@AuthenticationPrincipal Jwt jwt) {
        return ApiResponse.success(orderService.listOrders(userId(jwt), 1, 100).items());
    }

    @GetMapping("/orders/page")
    public ApiResponse<PageResponse<OrderView>> orderPage(
            @AuthenticationPrincipal Jwt jwt,
            @RequestParam(defaultValue = "1") @Min(1) long page,
            @RequestParam(defaultValue = "20") @Min(1) @Max(100) long size) {
        return ApiResponse.success(orderService.listOrders(userId(jwt), page, size));
    }

    @GetMapping("/orders/cursor")
    public ApiResponse<CursorPageResponse<OrderView>> orderCursor(
            @AuthenticationPrincipal Jwt jwt,
            @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
            @RequestParam(required = false) @Size(max = 200) String cursor) {
        return ApiResponse.success(orderService.listOrdersByCursor(
                userId(jwt), size, cursor));
    }

    @GetMapping("/orders/by-idempotency-key/{idempotencyKey}")
    public ApiResponse<OrderView> orderByIdempotencyKey(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Size(min = 8, max = 64)
            @Pattern(regexp = BUSINESS_NO_PATTERN) String idempotencyKey) {
        return ApiResponse.success(orderService.getOrderByIdempotencyKey(
                userId(jwt), idempotencyKey));
    }

    @GetMapping("/orders/{orderNo}")
    public ApiResponse<OrderView> order(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String orderNo) {
        return ApiResponse.success(orderService.getOrder(userId(jwt), orderNo));
    }

    @PostMapping("/orders/{orderNo}/cancel")
    public ApiResponse<OrderView> cancel(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Pattern(regexp = BUSINESS_NO_PATTERN) String orderNo) {
        return ApiResponse.success(orderService.cancelOrder(userId(jwt), orderNo));
    }

    private Long userId(Jwt jwt) {
        return Long.valueOf(jwt.getSubject());
    }

    public record PutCartItemRequest(
            @NotNull @Positive Long productId,
            @Positive @Max(1000000000L) long quantity,
            boolean selected
    ) {
    }

    public record GuestBagMergeRequest(
            @NotEmpty @Size(max = 100) List<@Valid GuestBagItemRequest> items
    ) {
    }

    public record GuestBagItemRequest(
            @NotNull @Positive Long productId,
            @NotNull @Positive Long skuId,
            @Positive @Max(1000000000L) long quantity
    ) {
    }

    public record CreateOrderRequest(
            @NotNull @Positive Long addressId,
            @NotEmpty @Size(max = 20) List<@Valid OrderLineRequest> items,
            @Size(max = 3) List<@NotBlank @Size(max = 64)
                    @Pattern(regexp = BUSINESS_NO_PATTERN) String> benefitNos
    ) {
    }

    public record OrderLineRequest(
            @NotNull @Positive Long productId,
            @NotNull @Positive Long skuId,
            @Positive @Max(1000000000L) long quantity
    ) {
    }
}