refactor(catalog): nest catalog routes under tenant
Move Catalog endpoints under /api/tenants/{tenant:codigo} and derive tenant
context from the route instead of request payload fields.
- scope marcas, categorias, productos, and product-attributes by tenant
- enforce tenant ownership in catalog controllers
- remove tenant_codigo and tenant_code from Catalog request validation
- update product attribute tests to use tenant-scoped routes
This commit is contained in:
@@ -6,40 +6,60 @@ use App\Domains\Catalog\Models\Brand;
|
||||
use App\Domains\Catalog\Requests\StoreBrandRequest;
|
||||
use App\Domains\Catalog\Requests\UpdateBrandRequest;
|
||||
use App\Domains\Catalog\Resources\BrandResource;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class BrandController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
public function index(Tenant $tenant): JsonResponse
|
||||
{
|
||||
return BrandResource::collection(Brand::query()->orderByDesc('id')->get())->response();
|
||||
return BrandResource::collection(
|
||||
Brand::query()->where('tenant_codigo', $tenant->codigo)->orderByDesc('id')->get()
|
||||
)->response();
|
||||
}
|
||||
|
||||
public function store(StoreBrandRequest $request): JsonResponse
|
||||
public function store(StoreBrandRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$brand = Brand::query()->create($request->validated());
|
||||
$brand = Brand::query()->create([
|
||||
...$request->validated(),
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
|
||||
return BrandResource::make($brand)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Brand $marca): BrandResource
|
||||
public function show(Tenant $tenant, Brand $marca): BrandResource
|
||||
{
|
||||
$marca = $this->resolveScopedBrand($tenant, $marca);
|
||||
|
||||
return BrandResource::make($marca);
|
||||
}
|
||||
|
||||
public function update(UpdateBrandRequest $request, Brand $marca): BrandResource
|
||||
public function update(UpdateBrandRequest $request, Tenant $tenant, Brand $marca): BrandResource
|
||||
{
|
||||
$marca = $this->resolveScopedBrand($tenant, $marca);
|
||||
$marca->update($request->validated());
|
||||
|
||||
return BrandResource::make($marca);
|
||||
}
|
||||
|
||||
public function destroy(Brand $marca): Response
|
||||
public function destroy(Tenant $tenant, Brand $marca): Response
|
||||
{
|
||||
$marca = $this->resolveScopedBrand($tenant, $marca);
|
||||
$marca->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
protected function resolveScopedBrand(Tenant $tenant, Brand $brand): Brand
|
||||
{
|
||||
if ($brand->tenant_codigo !== $tenant->codigo) {
|
||||
throw new NotFoundHttpException('Brand not found for tenant.');
|
||||
}
|
||||
|
||||
return $brand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,44 +6,66 @@ use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Catalog\Requests\StoreCategoryRequest;
|
||||
use App\Domains\Catalog\Requests\UpdateCategoryRequest;
|
||||
use App\Domains\Catalog\Resources\CategoryResource;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
public function index(Tenant $tenant): JsonResponse
|
||||
{
|
||||
return CategoryResource::collection(
|
||||
Category::query()->with(['parent', 'subCategories','tenant'])->orderByDesc('id')->get()
|
||||
Category::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->with(['parent', 'subCategories', 'tenant'])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
)->response();
|
||||
}
|
||||
|
||||
public function store(StoreCategoryRequest $request): JsonResponse
|
||||
public function store(StoreCategoryRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$category = Category::query()->create($request->validated());
|
||||
$category = Category::query()->create([
|
||||
...$request->validated(),
|
||||
'tenant_code' => $tenant->codigo,
|
||||
]);
|
||||
|
||||
return CategoryResource::make($category->load(['parent', 'subCategories', 'tenant']))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Category $categoria): CategoryResource
|
||||
public function show(Tenant $tenant, Category $categoria): CategoryResource
|
||||
{
|
||||
$categoria = $this->resolveScopedCategory($tenant, $categoria);
|
||||
|
||||
return CategoryResource::make($categoria->load(['parent', 'subCategories', 'tenant']));
|
||||
}
|
||||
|
||||
public function update(UpdateCategoryRequest $request, Category $categoria): CategoryResource
|
||||
public function update(UpdateCategoryRequest $request, Tenant $tenant, Category $categoria): CategoryResource
|
||||
{
|
||||
$categoria = $this->resolveScopedCategory($tenant, $categoria);
|
||||
$categoria->update($request->validated());
|
||||
|
||||
return CategoryResource::make($categoria->load(['parent', 'subCategories', 'tenant']));
|
||||
}
|
||||
|
||||
public function destroy(Category $categoria): Response
|
||||
public function destroy(Tenant $tenant, Category $categoria): Response
|
||||
{
|
||||
$categoria = $this->resolveScopedCategory($tenant, $categoria);
|
||||
$categoria->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
protected function resolveScopedCategory(Tenant $tenant, Category $category): Category
|
||||
{
|
||||
if ($category->tenant_code !== $tenant->codigo) {
|
||||
throw new NotFoundHttpException('Category not found for tenant.');
|
||||
}
|
||||
|
||||
return $category;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,28 @@ use App\Domains\Catalog\Requests\StoreProductAttributeRequest;
|
||||
use App\Domains\Catalog\Requests\UpdateProductAttributeRequest;
|
||||
use App\Domains\Catalog\Resources\ProductAttributeResource;
|
||||
use App\Domains\Catalog\Support\ProductAttributeType;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ProductAttributeController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
public function index(Tenant $tenant): JsonResponse
|
||||
{
|
||||
$query = ProductAttribute::query()->with('options')->latest();
|
||||
|
||||
if (request()->filled('tenant_codigo')) {
|
||||
$query->where('tenant_codigo', request('tenant_codigo'));
|
||||
}
|
||||
$query = ProductAttribute::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->with('options')
|
||||
->latest();
|
||||
|
||||
return ProductAttributeResource::collection($query->get())->response();
|
||||
}
|
||||
|
||||
public function store(StoreProductAttributeRequest $request): JsonResponse
|
||||
public function store(StoreProductAttributeRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$attribute = DB::transaction(function () use ($request): ProductAttribute {
|
||||
$attribute = DB::transaction(function () use ($request, $tenant): ProductAttribute {
|
||||
$validated = $request->validated();
|
||||
$options = $validated['options'] ?? [];
|
||||
unset($validated['options']);
|
||||
@@ -39,7 +40,10 @@ class ProductAttributeController extends Controller
|
||||
}
|
||||
|
||||
/** @var ProductAttribute $attribute */
|
||||
$attribute = ProductAttribute::query()->create($validated);
|
||||
$attribute = ProductAttribute::query()->create([
|
||||
...$validated,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
$attribute->options()->createMany($options);
|
||||
|
||||
return $attribute->load('options');
|
||||
@@ -48,13 +52,17 @@ class ProductAttributeController extends Controller
|
||||
return ProductAttributeResource::make($attribute)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(ProductAttribute $productAttribute): ProductAttributeResource
|
||||
public function show(Tenant $tenant, ProductAttribute $productAttribute): ProductAttributeResource
|
||||
{
|
||||
$productAttribute = $this->resolveScopedAttribute($tenant, $productAttribute);
|
||||
|
||||
return ProductAttributeResource::make($productAttribute->load('options'));
|
||||
}
|
||||
|
||||
public function update(UpdateProductAttributeRequest $request, ProductAttribute $productAttribute): ProductAttributeResource
|
||||
public function update(UpdateProductAttributeRequest $request, Tenant $tenant, ProductAttribute $productAttribute): ProductAttributeResource
|
||||
{
|
||||
$productAttribute = $this->resolveScopedAttribute($tenant, $productAttribute);
|
||||
|
||||
$productAttribute = DB::transaction(function () use ($request, $productAttribute): ProductAttribute {
|
||||
$validated = $request->validated();
|
||||
$options = $validated['options'] ?? [];
|
||||
@@ -76,10 +84,20 @@ class ProductAttributeController extends Controller
|
||||
return ProductAttributeResource::make($productAttribute);
|
||||
}
|
||||
|
||||
public function destroy(ProductAttribute $productAttribute): Response
|
||||
public function destroy(Tenant $tenant, ProductAttribute $productAttribute): Response
|
||||
{
|
||||
$productAttribute = $this->resolveScopedAttribute($tenant, $productAttribute);
|
||||
$productAttribute->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
protected function resolveScopedAttribute(Tenant $tenant, ProductAttribute $attribute): ProductAttribute
|
||||
{
|
||||
if ($attribute->tenant_codigo !== $tenant->codigo) {
|
||||
throw new NotFoundHttpException('Product attribute not found for tenant.');
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,40 +6,60 @@ use App\Domains\Catalog\Models\Product;
|
||||
use App\Domains\Catalog\Requests\StoreProductRequest;
|
||||
use App\Domains\Catalog\Requests\UpdateProductRequest;
|
||||
use App\Domains\Catalog\Resources\ProductResource;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
public function index(Tenant $tenant): JsonResponse
|
||||
{
|
||||
return ProductResource::collection(Product::query()->latest()->get())->response();
|
||||
return ProductResource::collection(
|
||||
Product::query()->where('tenant_codigo', $tenant->codigo)->latest()->get()
|
||||
)->response();
|
||||
}
|
||||
|
||||
public function store(StoreProductRequest $request): JsonResponse
|
||||
public function store(StoreProductRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$product = Product::query()->create($request->validated());
|
||||
$product = Product::query()->create([
|
||||
...$request->validated(),
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
]);
|
||||
|
||||
return ProductResource::make($product)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Product $producto): ProductResource
|
||||
public function show(Tenant $tenant, Product $producto): ProductResource
|
||||
{
|
||||
$producto = $this->resolveScopedProduct($tenant, $producto);
|
||||
|
||||
return ProductResource::make($producto);
|
||||
}
|
||||
|
||||
public function update(UpdateProductRequest $request, Product $producto): ProductResource
|
||||
public function update(UpdateProductRequest $request, Tenant $tenant, Product $producto): ProductResource
|
||||
{
|
||||
$producto = $this->resolveScopedProduct($tenant, $producto);
|
||||
$producto->update($request->validated());
|
||||
|
||||
return ProductResource::make($producto);
|
||||
}
|
||||
|
||||
public function destroy(Product $producto): Response
|
||||
public function destroy(Tenant $tenant, Product $producto): Response
|
||||
{
|
||||
$producto = $this->resolveScopedProduct($tenant, $producto);
|
||||
$producto->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
protected function resolveScopedProduct(Tenant $tenant, Product $product): Product
|
||||
{
|
||||
if ($product->tenant_codigo !== $tenant->codigo) {
|
||||
throw new NotFoundHttpException('Product not found for tenant.');
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ class StoreBrandRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
'descripcion' => ['nullable', 'string'],
|
||||
];
|
||||
|
||||
@@ -17,7 +17,6 @@ class StoreCategoryRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_code' => ['required_with:categoria_id', 'string', 'exists:tenants,codigo'],
|
||||
'categoria_id' => ['nullable', 'integer', 'exists:categorias,id'],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
@@ -20,13 +20,12 @@ class StoreProductAttributeRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'codigo' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('productos_attributes', 'codigo')->where(
|
||||
fn ($query) => $query->where('tenant_codigo', $this->input('tenant_codigo'))
|
||||
fn ($query) => $query->where('tenant_codigo', $this->route('tenant')?->codigo)
|
||||
),
|
||||
],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -18,7 +18,6 @@ class StoreProductRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'categoria_id' => ['required', 'integer'],
|
||||
'slug' => ['required', 'string', 'max:255', Rule::unique('productos', 'slug')],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -17,7 +17,6 @@ class UpdateBrandRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
'descripcion' => ['nullable', 'string'],
|
||||
];
|
||||
|
||||
@@ -22,7 +22,6 @@ class UpdateCategoryRequest extends FormRequest
|
||||
$category = $this->route('categoria');
|
||||
|
||||
return [
|
||||
'tenant_code' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'categoria_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
|
||||
@@ -22,7 +22,6 @@ class UpdateProductRequest extends FormRequest
|
||||
$product = $this->route('producto');
|
||||
|
||||
return [
|
||||
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||
'categoria_id' => ['required', 'integer'],
|
||||
'slug' => [
|
||||
'required',
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Domains\Catalog\Controllers\ProductController;
|
||||
use App\Domains\Catalog\Controllers\ProductAttributeController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::apiResource('marcas', BrandController::class)->parameters(['marcas' => 'marca']);
|
||||
Route::apiResource('categorias', CategoryController::class)->parameters(['categorias' => 'categoria']);
|
||||
Route::apiResource('productos', ProductController::class);
|
||||
Route::apiResource('product-attributes', ProductAttributeController::class)
|
||||
->parameters(['product-attributes' => 'productAttribute']);
|
||||
Route::prefix('tenants/{tenant:codigo}')->group(function (): void {
|
||||
Route::apiResource('marcas', BrandController::class)->parameters(['marcas' => 'marca']);
|
||||
Route::apiResource('categorias', CategoryController::class)->parameters(['categorias' => 'categoria']);
|
||||
Route::apiResource('productos', ProductController::class);
|
||||
Route::apiResource('product-attributes', ProductAttributeController::class)
|
||||
->parameters(['product-attributes' => 'productAttribute']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user