Merge: Products/Catalog module (agente eden-commercial)
This commit is contained in:
36
apps/api/src/products/dto/create-product.dto.ts
Normal file
36
apps/api/src/products/dto/create-product.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString, Min, MinLength } from "class-validator";
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
name!: string;
|
||||
|
||||
@IsOptional() @IsString() description?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
productType!: string;
|
||||
|
||||
@IsOptional() @IsString() productGroup?: string;
|
||||
@IsOptional() @IsString() productSubgroup?: string;
|
||||
@IsOptional() @IsString() brand?: string;
|
||||
@IsOptional() @IsString() unit?: string;
|
||||
|
||||
@IsOptional() @IsBoolean() active?: boolean;
|
||||
@IsOptional() @IsBoolean() discontinued?: boolean;
|
||||
@IsOptional() @IsBoolean() tracksStock?: boolean;
|
||||
@IsOptional() @IsBoolean() tracksSerial?: boolean;
|
||||
@IsOptional() @IsBoolean() tracksAssetTag?: boolean;
|
||||
@IsOptional() @IsBoolean() tracksMac?: boolean;
|
||||
|
||||
// Money fields: format-validated here only (non-negative number). The
|
||||
// backend never trusts these for anything beyond storage in this MVP —
|
||||
// no pricing/discount calculation happens yet, that's Quotes (a later
|
||||
// phase). Always persisted as NUMERIC, never float.
|
||||
@IsOptional() @IsNumber() @Min(0) price0?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) price12?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) price24?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) price36?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) price48?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) implUnitPrice?: number;
|
||||
}
|
||||
4
apps/api/src/products/dto/update-product.dto.ts
Normal file
4
apps/api/src/products/dto/update-product.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateProductDto } from "./create-product.dto.js";
|
||||
|
||||
export class UpdateProductDto extends PartialType(CreateProductDto) {}
|
||||
52
apps/api/src/products/products.controller.ts
Normal file
52
apps/api/src/products/products.controller.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { ProductsService } from "./products.service.js";
|
||||
import { CreateProductDto } from "./dto/create-product.dto.js";
|
||||
import { UpdateProductDto } from "./dto/update-product.dto.js";
|
||||
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
||||
import { PermissionGuard } from "../auth/permission.guard.js";
|
||||
import { RequirePermission } from "../auth/decorators/require-permission.decorator.js";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator.js";
|
||||
import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js";
|
||||
|
||||
/**
|
||||
* Read is open to any authenticated user (not gated by a resource
|
||||
* permission) — the catalog needs to be browsable by any seller building a
|
||||
* quote, mirroring the legacy (eden.md §2 §1.1) and the same choice made
|
||||
* for companies. Write requires products:edit. Backend always
|
||||
* recalculates/validates: money fields are format-checked (non-negative)
|
||||
* in the DTO but no pricing logic runs here — that's Quotes, a later phase.
|
||||
*/
|
||||
@ApiTags("products")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||
@Controller("v1/products")
|
||||
export class ProductsController {
|
||||
constructor(private readonly products: ProductsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query("includeInactive") includeInactive?: string) {
|
||||
return this.products.list(includeInactive === "true");
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
get(@Param("id") id: string) {
|
||||
return this.products.get(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission("products", "edit")
|
||||
create(@Body() dto: CreateProductDto, @CurrentUser() user: EdenAuthenticatedUser) {
|
||||
return this.products.create(dto, user.id);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@RequirePermission("products", "edit")
|
||||
update(
|
||||
@Param("id") id: string,
|
||||
@Body() dto: UpdateProductDto,
|
||||
@CurrentUser() user: EdenAuthenticatedUser,
|
||||
) {
|
||||
return this.products.update(id, dto, user.id);
|
||||
}
|
||||
}
|
||||
11
apps/api/src/products/products.module.ts
Normal file
11
apps/api/src/products/products.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AuthModule } from "../auth/auth.module.js";
|
||||
import { ProductsController } from "./products.controller.js";
|
||||
import { ProductsService } from "./products.service.js";
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [ProductsController],
|
||||
providers: [ProductsService],
|
||||
})
|
||||
export class ProductsModule {}
|
||||
102
apps/api/src/products/products.service.ts
Normal file
102
apps/api/src/products/products.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { auditRepo } from "@eden/database";
|
||||
// Deliberately deep-imported rather than added to the shared @eden/database
|
||||
// barrel (packages/database/src/index.ts) — that file is off limits for
|
||||
// this module per the integration boundary (avoids merge conflicts with
|
||||
// sibling agents editing the same barrel concurrently). Whoever wires
|
||||
// modules together should add
|
||||
// export * as productsRepo from "./repositories/products.js";
|
||||
// export type { Product } from "./repositories/products.js";
|
||||
// to the barrel for parity with how companiesRepo is consumed; until then
|
||||
// this deep path is functionally equivalent (requires @eden/database to be
|
||||
// built first, same as the barrel import would).
|
||||
import * as productsRepo from "@eden/database/dist/repositories/products.js";
|
||||
import type { Product } from "@eden/database/dist/repositories/products.js";
|
||||
import type { CreateProductDto } from "./dto/create-product.dto.js";
|
||||
import type { UpdateProductDto } from "./dto/update-product.dto.js";
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
list(includeInactive: boolean): Promise<Product[]> {
|
||||
return productsRepo.listProducts(includeInactive);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Product> {
|
||||
const product = await productsRepo.getProductById(id);
|
||||
if (!product) throw new NotFoundException("Produto não encontrado");
|
||||
return product;
|
||||
}
|
||||
|
||||
async create(dto: CreateProductDto, actorId: string): Promise<Product> {
|
||||
const product = await productsRepo.createProduct({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
productType: dto.productType,
|
||||
productGroup: dto.productGroup,
|
||||
productSubgroup: dto.productSubgroup,
|
||||
brand: dto.brand,
|
||||
unit: dto.unit,
|
||||
active: dto.active,
|
||||
discontinued: dto.discontinued,
|
||||
tracksStock: dto.tracksStock,
|
||||
tracksSerial: dto.tracksSerial,
|
||||
tracksAssetTag: dto.tracksAssetTag,
|
||||
tracksMac: dto.tracksMac,
|
||||
price0: dto.price0,
|
||||
price12: dto.price12,
|
||||
price24: dto.price24,
|
||||
price36: dto.price36,
|
||||
price48: dto.price48,
|
||||
implUnitPrice: dto.implUnitPrice,
|
||||
createdBy: actorId,
|
||||
});
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId,
|
||||
action: "product.created",
|
||||
resourceType: "product",
|
||||
resourceId: product.id,
|
||||
});
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateProductDto, actorId: string): Promise<Product> {
|
||||
await this.get(id); // 404 if missing
|
||||
|
||||
const product = await productsRepo.updateProduct(id, {
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
productType: dto.productType,
|
||||
productGroup: dto.productGroup,
|
||||
productSubgroup: dto.productSubgroup,
|
||||
brand: dto.brand,
|
||||
unit: dto.unit,
|
||||
active: dto.active,
|
||||
discontinued: dto.discontinued,
|
||||
tracksStock: dto.tracksStock,
|
||||
tracksSerial: dto.tracksSerial,
|
||||
tracksAssetTag: dto.tracksAssetTag,
|
||||
tracksMac: dto.tracksMac,
|
||||
price0: dto.price0,
|
||||
price12: dto.price12,
|
||||
price24: dto.price24,
|
||||
price36: dto.price36,
|
||||
price48: dto.price48,
|
||||
implUnitPrice: dto.implUnitPrice,
|
||||
});
|
||||
if (!product) throw new NotFoundException("Produto não encontrado");
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId,
|
||||
action: "product.updated",
|
||||
resourceType: "product",
|
||||
resourceId: id,
|
||||
metadata: { fields: Object.keys(dto) },
|
||||
});
|
||||
|
||||
return product;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user