AdminCatalogController.java
package com.ecommerce.catalog.interfaces.rest;
import com.ecommerce.catalog.application.model.CatalogModels.BrandView;
import com.ecommerce.catalog.application.model.CatalogModels.CategoryView;
import com.ecommerce.catalog.application.model.CatalogModels.CreateProductCommand;
import com.ecommerce.catalog.application.model.CatalogModels.CreateSkuCommand;
import com.ecommerce.catalog.application.model.CatalogModels.MediaView;
import com.ecommerce.catalog.application.model.CatalogModels.ProductDetail;
import com.ecommerce.catalog.application.model.CatalogModels.SkuView;
import com.ecommerce.catalog.application.model.CatalogModels.UpdateProductCommand;
import com.ecommerce.catalog.application.model.CatalogModels.UpdateSkuCommand;
import com.ecommerce.catalog.application.model.CatalogModels.UploadIntent;
import com.ecommerce.catalog.application.service.CatalogService;
import com.ecommerce.platform.common.api.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
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.PositiveOrZero;
import jakarta.validation.constraints.Size;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/api/v1/catalog/admin")
public class AdminCatalogController {
private static final String SLUG_PATTERN = "[a-z0-9]+(?:-[a-z0-9]+)*";
private final CatalogService catalogService;
public AdminCatalogController(CatalogService catalogService) {
this.catalogService = catalogService;
}
@PostMapping("/categories")
public ApiResponse<CategoryView> createCategory(@Valid @RequestBody CreateCategoryRequest request) {
return ApiResponse.success(catalogService.createCategory(
request.parentId(), request.name(), request.slug(), request.sortOrder()));
}
@PostMapping("/brands")
public ApiResponse<BrandView> createBrand(@Valid @RequestBody CreateBrandRequest request) {
return ApiResponse.success(catalogService.createBrand(request.name(), request.slug()));
}
@PostMapping("/products")
public ApiResponse<ProductDetail> createProduct(@Valid @RequestBody CreateProductRequest request) {
List<CreateSkuCommand> skus = request.skus().stream()
.map(sku -> new CreateSkuCommand(sku.skuCode(), sku.name(), sku.specJson(),
sku.salePrice(), sku.marketPrice()))
.toList();
return ApiResponse.success(catalogService.createProduct(new CreateProductCommand(
request.categoryId(), request.brandId(), request.title(), request.subtitle(),
request.description(), skus)));
}
@PutMapping("/products/{productId}")
public ApiResponse<ProductDetail> updateProduct(
@PathVariable @Positive Long productId,
@Valid @RequestBody UpdateProductRequest request) {
return ApiResponse.success(catalogService.updateProduct(productId, new UpdateProductCommand(
request.categoryId(), request.brandId(), request.title(), request.subtitle(),
request.description(), request.expectedVersion())));
}
@PostMapping("/products/{productId}/publish")
public ApiResponse<ProductDetail> publish(
@PathVariable @Positive Long productId,
@Valid @RequestBody VersionRequest request) {
return ApiResponse.success(catalogService.publishProduct(productId, request.expectedVersion()));
}
@PostMapping("/products/{productId}/unpublish")
public ApiResponse<ProductDetail> unpublish(
@PathVariable @Positive Long productId,
@Valid @RequestBody VersionRequest request) {
return ApiResponse.success(catalogService.unpublishProduct(productId, request.expectedVersion()));
}
@PutMapping("/products/{productId}/skus/{skuId}")
public ApiResponse<SkuView> updateSku(
@PathVariable @Positive Long productId,
@PathVariable @Positive Long skuId,
@Valid @RequestBody UpdateSkuRequest request) {
return ApiResponse.success(catalogService.updateSku(productId, skuId, new UpdateSkuCommand(
request.name(), request.specJson(), request.salePrice(), request.marketPrice(),
request.status(), request.expectedVersion())));
}
@PostMapping("/products/{productId}/media/upload-intents")
public ApiResponse<UploadIntent> createUploadIntent(
@PathVariable @Positive Long productId,
@Valid @RequestBody UploadIntentRequest request) {
return ApiResponse.success(catalogService.createUploadIntent(
productId, request.contentType(), request.sizeBytes()));
}
@PostMapping("/products/{productId}/media")
public ApiResponse<MediaView> confirmMedia(
@PathVariable @Positive Long productId,
@Valid @RequestBody ConfirmMediaRequest request) {
return ApiResponse.success(catalogService.confirmMedia(
productId, request.skuId(), request.objectKey(), request.sortOrder()));
}
public record CreateCategoryRequest(
@Positive Long parentId,
@NotBlank @Size(max = 80) String name,
@NotBlank @Size(max = 100) @Pattern(regexp = SLUG_PATTERN) String slug,
@Min(0) @Max(100000) int sortOrder
) {
}
public record CreateBrandRequest(
@NotBlank @Size(max = 80) String name,
@NotBlank @Size(max = 100) @Pattern(regexp = SLUG_PATTERN) String slug
) {
}
public record CreateProductRequest(
@NotNull @Positive Long categoryId,
@NotNull @Positive Long brandId,
@NotBlank @Size(max = 160) String title,
@Size(max = 240) String subtitle,
@Size(max = 10000) String description,
@NotEmpty @Size(max = 100) List<@Valid CreateSkuRequest> skus
) {
}
public record CreateSkuRequest(
@NotBlank @Size(max = 64) @Pattern(regexp = "[A-Za-z0-9._-]+") String skuCode,
@NotBlank @Size(max = 160) String name,
@NotBlank @Size(max = 2000) String specJson,
@NotNull @DecimalMin(value = "0.01") @Digits(integer = 16, fraction = 2) BigDecimal salePrice,
@DecimalMin(value = "0.01") @Digits(integer = 16, fraction = 2) BigDecimal marketPrice
) {
}
public record UpdateProductRequest(
@NotNull @Positive Long categoryId,
@NotNull @Positive Long brandId,
@NotBlank @Size(max = 160) String title,
@Size(max = 240) String subtitle,
@Size(max = 10000) String description,
@PositiveOrZero int expectedVersion
) {
}
public record UpdateSkuRequest(
@NotBlank @Size(max = 160) String name,
@NotBlank @Size(max = 2000) String specJson,
@NotNull @DecimalMin(value = "0.01") @Digits(integer = 16, fraction = 2) BigDecimal salePrice,
@DecimalMin(value = "0.01") @Digits(integer = 16, fraction = 2) BigDecimal marketPrice,
@NotBlank @Pattern(regexp = "ACTIVE|INACTIVE") String status,
@PositiveOrZero int expectedVersion
) {
}
public record VersionRequest(@PositiveOrZero int expectedVersion) {
}
public record UploadIntentRequest(
@NotBlank @Pattern(regexp = "image/(jpeg|png|webp)") String contentType,
@Positive long sizeBytes
) {
}
public record ConfirmMediaRequest(
@Positive Long skuId,
@NotBlank @Size(max = 500) String objectKey,
@Min(0) @Max(100000) int sortOrder
) {
}
}