OrderController.java

package com.dmasone.identity.orders.interfaces.rest;

import com.dmasone.identity.orders.application.OrderQueryService;
import com.dmasone.identity.orders.application.OrderResponse;
import com.dmasone.identity.orders.application.PlaceOrderService;
import com.dmasone.identity.orders.application.PlaceOrderResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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;

/**
 * Order API exposing placement and lookup workflows. The controller delegates
 * to use-case services and does not reach into catalog or payment internals.
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private static final String IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";

    private final PlaceOrderService placeOrderService;
    private final OrderQueryService orderQueryService;
    private final OrderRestMapper orderRestMapper;

    public OrderController(
            PlaceOrderService placeOrderService,
            OrderQueryService orderQueryService,
            OrderRestMapper orderRestMapper
    ) {
        this.placeOrderService = placeOrderService;
        this.orderQueryService = orderQueryService;
        this.orderRestMapper = orderRestMapper;
    }

    @Operation(
            summary = "Place an order",
            description = "Reserves catalog stock, persists the order, and publishes an internal order placed event. "
                    + "When Idempotency-Key is provided, safe retries return the original order."
    )
    @ApiResponses({
            @ApiResponse(
                    responseCode = "200",
                    description = "Idempotent replay returned the original order",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON_VALUE,
                            schema = @Schema(implementation = OrderDto.class)
                    )
            ),
            @ApiResponse(
                    responseCode = "201",
                    description = "Order placed",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON_VALUE,
                            schema = @Schema(implementation = OrderDto.class)
                    )
            ),
            @ApiResponse(responseCode = "400", description = "Invalid request", content = @Content),
            @ApiResponse(responseCode = "404", description = "Product not found", content = @Content),
            @ApiResponse(responseCode = "409", description = "Insufficient stock or idempotency conflict", content = @Content)
    })
    @PostMapping
    public ResponseEntity<OrderDto> placeOrder(
            @Parameter(
                    name = IDEMPOTENCY_KEY_HEADER,
                    in = ParameterIn.HEADER,
                    description = "Optional retry key. Reusing the same key with the same request returns the original order."
            )
            @RequestHeader(value = IDEMPOTENCY_KEY_HEADER, required = false) String idempotencyKey,
            @Valid @RequestBody PlaceOrderRequest request
    ) {
        PlaceOrderResult result = placeOrderService.placeOrder(
                orderRestMapper.toCommand(request, idempotencyKey)
        );
        OrderResponse response = result.order();
        OrderDto body = orderRestMapper.toDto(response);
        return ResponseEntity
                .status(result.replayed() ? HttpStatus.OK : HttpStatus.CREATED)
                .location(URI.create("/api/orders/" + response.id()))
                .body(body);
    }

    @Operation(
            summary = "Find an order",
            description = "Returns a persisted order without exposing payment implementation details."
    )
    @ApiResponses({
            @ApiResponse(
                    responseCode = "200",
                    description = "Order found",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON_VALUE,
                            schema = @Schema(implementation = OrderDto.class)
                    )
            ),
            @ApiResponse(responseCode = "404", description = "Order not found", content = @Content)
    })
    @GetMapping("/{id}")
    public OrderDto findOrder(@PathVariable UUID id) {
        return orderRestMapper.toDto(orderQueryService.findById(id));
    }
}