AdminFulfillmentController.java
package com.ecommerce.fulfillment.interfaces.rest;
import com.ecommerce.fulfillment.application.model.FulfillmentModels.AddTraceCommand;
import com.ecommerce.fulfillment.application.model.FulfillmentModels.FulfillmentView;
import com.ecommerce.fulfillment.application.model.FulfillmentModels.ShipCommand;
import com.ecommerce.fulfillment.application.service.FulfillmentService;
import com.ecommerce.platform.common.api.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
@Validated
@RestController
@RequestMapping("/api/v1/fulfillment/admin/orders")
public class AdminFulfillmentController {
private static final String BUSINESS_NO_PATTERN = "[A-Za-z0-9._:-]+";
private final FulfillmentService fulfillmentService;
public AdminFulfillmentController(FulfillmentService fulfillmentService) {
this.fulfillmentService = fulfillmentService;
}
@GetMapping
public ApiResponse<List<FulfillmentView>> orders(@RequestParam(required = false) String status) {
return ApiResponse.success(fulfillmentService.list(status));
}
@GetMapping("/{fulfillmentNo}")
public ApiResponse<FulfillmentView> order(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo) {
return ApiResponse.success(fulfillmentService.get(fulfillmentNo));
}
@PostMapping("/{fulfillmentNo}/picking")
public ApiResponse<FulfillmentView> startPicking(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.startPicking(fulfillmentNo, jwt.getSubject()));
}
@PostMapping("/{fulfillmentNo}/packed")
public ApiResponse<FulfillmentView> packed(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.markPacked(fulfillmentNo, jwt.getSubject()));
}
@PostMapping("/{fulfillmentNo}/ship")
public ApiResponse<FulfillmentView> ship(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@Valid @RequestBody ShipRequest request,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.ship(fulfillmentNo,
new ShipCommand(request.carrier(), request.trackingNo(), jwt.getSubject())));
}
@PostMapping("/{fulfillmentNo}/traces")
public ApiResponse<FulfillmentView> addTrace(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@Valid @RequestBody AddTraceRequest request,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.addTrace(fulfillmentNo, new AddTraceCommand(
request.externalEventId(), request.nodeType(), request.description(), request.locationName(),
request.longitude(), request.latitude(), request.occurredAt(), jwt.getSubject())));
}
@PostMapping("/{fulfillmentNo}/exception")
public ApiResponse<FulfillmentView> markException(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@Valid @RequestBody ExceptionRequest request,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.markException(
fulfillmentNo, request.reason(), jwt.getSubject()));
}
@PostMapping("/{fulfillmentNo}/exception/resolve")
public ApiResponse<FulfillmentView> resolveException(
@PathVariable @NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String fulfillmentNo,
@RequestHeader("Idempotency-Key")
@NotBlank @Size(max = 64) @Pattern(regexp = BUSINESS_NO_PATTERN)
String commandId,
@Valid @RequestBody ExceptionRequest request,
@AuthenticationPrincipal Jwt jwt) {
return ApiResponse.success(fulfillmentService.resolveException(
fulfillmentNo, commandId, request.reason(), jwt.getSubject()));
}
public record ShipRequest(
@NotBlank @Size(max = 40) @Pattern(regexp = "[A-Z0-9_-]+") String carrier,
@NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String trackingNo
) {
}
public record AddTraceRequest(
@NotBlank @Size(max = 100) @Pattern(regexp = BUSINESS_NO_PATTERN) String externalEventId,
@NotBlank @Pattern(regexp = "TRANSIT|DELIVERING|SIGNED|EXCEPTION") String nodeType,
@NotBlank @Size(max = 240) String description,
@Size(max = 120) String locationName,
@DecimalMin("-180") @DecimalMax("180") BigDecimal longitude,
@DecimalMin("-90") @DecimalMax("90") BigDecimal latitude,
@NotNull Instant occurredAt
) {
}
public record ExceptionRequest(@NotBlank @Size(max = 500) String reason) {
}
}