ProductCatalogController.java

package com.taxonomy.portfolio.controller;

import com.taxonomy.portfolio.dto.PortfolioDtos.CreateProductRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.ProductView;
import com.taxonomy.portfolio.dto.PortfolioDtos.SolutionProductCandidateView;
import com.taxonomy.portfolio.dto.PortfolioDtos.UpdateProductRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.UpsertProductCandidateRequest;
import com.taxonomy.portfolio.dto.PortfolioDtos.UpsertTaxonomyCoverageRequest;
import com.taxonomy.portfolio.service.ProductCatalogService;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/api")
@Tag(name = "Product Catalogue")
public class ProductCatalogController {

    private final ProductCatalogService productService;
    private final WorkspaceResolver workspaceResolver;

    public ProductCatalogController(ProductCatalogService productService,
                                    WorkspaceResolver workspaceResolver) {
        this.productService = productService;
        this.workspaceResolver = workspaceResolver;
    }

    @PostMapping("/products")
    @Operation(summary = "Create a sourced and dated product catalogue entry")
    public ResponseEntity<ProductView> create(@RequestBody CreateProductRequest request) {
        RequestScope scope = scope();
        ProductView product = productService.createProduct(
                request, scope.username(), scope.context());
        return ResponseEntity.created(URI.create("/api/products/" + product.id())).body(product);
    }

    @GetMapping("/products")
    @Operation(summary = "List sourced products in the current workspace")
    public List<ProductView> list() {
        RequestScope scope = scope();
        return productService.listProducts(scope.username(), scope.context());
    }

    @GetMapping("/products/{productId}")
    @Operation(summary = "Read one product including claim provenance")
    public ProductView get(@PathVariable Long productId) {
        RequestScope scope = scope();
        return productService.getProduct(productId, scope.username(), scope.context());
    }

    @PatchMapping("/products/{productId}")
    @Operation(summary = "Update a product claim and verification timestamp")
    public ProductView update(@PathVariable Long productId,
                              @RequestBody UpdateProductRequest request) {
        RequestScope scope = scope();
        return productService.updateProduct(
                productId, request, scope.username(), scope.context());
    }

    @PostMapping("/products/{productId}/taxonomy-coverage")
    @Operation(summary = "Create or update evidence-backed product taxonomy coverage")
    public ProductView upsertCoverage(
            @PathVariable Long productId,
            @RequestBody UpsertTaxonomyCoverageRequest request) {
        RequestScope scope = scope();
        return productService.upsertTaxonomyCoverage(
                productId, request, scope.username(), scope.context());
    }

    @PostMapping("/projects/{projectId}/solutions/{projectSolutionId}/products")
    @Operation(summary = "Add or review a product candidate for a project solution")
    public SolutionProductCandidateView upsertCandidate(
            @PathVariable Long projectId,
            @PathVariable Long projectSolutionId,
            @RequestBody UpsertProductCandidateRequest request) {
        RequestScope scope = scope();
        return productService.upsertCandidate(
                projectId, projectSolutionId, request, scope.username(), scope.context());
    }

    @GetMapping("/projects/{projectId}/solutions/{projectSolutionId}/products")
    @Operation(summary = "List product candidates for a project solution")
    public List<SolutionProductCandidateView> listCandidates(
            @PathVariable Long projectId,
            @PathVariable Long projectSolutionId) {
        RequestScope scope = scope();
        return productService.listCandidates(
                projectId, projectSolutionId, scope.username(), scope.context());
    }

    private RequestScope scope() {
        return new RequestScope(
                workspaceResolver.resolveCurrentUsername(),
                workspaceResolver.resolveCurrentContext());
    }

    private record RequestScope(String username, WorkspaceContext context) {
    }
}