Facturas de cliente y clientes

This commit is contained in:
David Arranz 2025-08-21 10:14:12 +02:00
parent 3c1010adad
commit 335c2764b7
7 changed files with 119 additions and 101 deletions

View File

@ -1,36 +1,33 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import {
import { CreateCustomerCommandDTO } from "../../../../../common/dto"; ExpressController,
import { CreateCustomerUseCase } from "../../../../application"; authGuard,
errorMapper,
forbidQueryFieldGuard,
tenantGuard,
} from "@erp/core/api";
import { CreateCustomerRequestDTO } from "../../../../common/dto";
import { CreateCustomerUseCase } from "../../../application";
export class CreateCustomerController extends ExpressController { export class CreateCustomerController extends ExpressController {
public constructor(private readonly createCustomer: CreateCustomerUseCase) { public constructor(private readonly useCase: CreateCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {
const dto = this.req.body as CreateCustomerCommandDTO; const tenantId = this.getTenantId()!; // garantizado por tenantGuard
const dto = this.req.body as CreateCustomerRequestDTO;
/* /*
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
if (!user || !user.companyId) {
this.unauthorized(res, "Unauthorized: user or company not found");
return;
}
// Inyectar empresa del usuario autenticado (ownership) // Inyectar empresa del usuario autenticado (ownership)
dto.customerCompanyId = user.companyId; dto.customerCompanyId = user.companyId;
*/ */
const result = await this.createCustomer.execute(dto); const result = await this.useCase.execute(dto);
if (result.isFailure) { return result.match(
console.log(result.error); (data) => this.created(data),
const apiError = errorMapper.toApiError(result.error); (err) => this.handleApiError(errorMapper.toApiError(err))
return this.handleApiError(apiError); );
}
return this.created(result.data);
} }
} }

View File

@ -1,32 +1,28 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import {
import { DeleteCustomerUseCase } from "../../../../application"; ExpressController,
authGuard,
errorMapper,
forbidQueryFieldGuard,
tenantGuard,
} from "@erp/core/api";
import { DeleteCustomerUseCase } from "../../../application";
export class DeleteCustomerController extends ExpressController { export class DeleteCustomerController extends ExpressController {
public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) { public constructor(private readonly useCase: DeleteCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
async executeImpl(): Promise<any> { async executeImpl(): Promise<any> {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params; const { id } = this.req.params;
/* const result = await this.useCase.execute({ id, tenantId });
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
if (!user || !user.companyId) { return result.match(
this.unauthorized(res, "Unauthorized: user or company not found"); (data) => this.ok(data),
return; (error) => this.handleApiError(errorMapper.toApiError(error))
} );
*/
const result = await this.deleteCustomer.execute({ id });
if (result.isFailure) {
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}
return this.ok(result.data);
} }
} }

View File

@ -1,32 +1,28 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import {
ExpressController,
authGuard,
errorMapper,
forbidQueryFieldGuard,
tenantGuard,
} from "@erp/core/api";
import { GetCustomerUseCase } from "../../../application"; import { GetCustomerUseCase } from "../../../application";
export class GetCustomerController extends ExpressController { export class GetCustomerController extends ExpressController {
public constructor(private readonly getCustomer: GetCustomerUseCase) { public constructor(private readonly useCase: GetCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params; const { id } = this.req.params;
/* const result = await this.useCase.execute({ id, tenantId });
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
if (!user || !user.companyId) { return result.match(
this.unauthorized(res, "Unauthorized: user or company not found"); (data) => this.ok(data),
return; (error) => this.handleApiError(errorMapper.toApiError(error))
} );
*/
const result = await this.getCustomer.execute({ id });
if (result.isFailure) {
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}
return this.ok(result.data);
} }
} }

View File

@ -2,35 +2,30 @@ import * as z from "zod/v4";
export const CreateCustomerRequestSchema = z.object({ export const CreateCustomerRequestSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), reference: z.string(),
invoice_number: z.string().min(1, "Customer invoice number is required"),
invoice_series: z.string().min(1, "Customer invoice series is required"), is_freelancer: z.boolean(),
issue_date: z.string().datetime({ offset: true, message: "Invalid issue date format" }), name: z.string(),
operation_date: z.string().datetime({ offset: true, message: "Invalid operation date format" }), trade_name: z.string(),
description: z.string(), tin: z.string(),
language_code: z.string().min(2, "Language code must be at least 2 characters long"),
currency_code: z.string().min(3, "Currency code must be at least 3 characters long"), street: z.string(),
notes: z.string().optional(), city: z.string(),
items: z.array( state: z.string(),
z.object({ postal_code: z.string(),
description: z.string().min(1, "Item description is required"), country: z.string(),
quantity: z.object({
amount: z.number().positive("Quantity amount must be positive"), email: z.string(),
scale: z.number().int().nonnegative("Quantity scale must be a non-negative integer"), phone: z.string(),
}), fax: z.string(),
unit_price: z.object({ website: z.string(),
amount: z.number().positive("Unit price amount must be positive"),
scale: z.number().int().nonnegative("Unit price scale must be a non-negative integer"), legal_record: z.string(),
currency_code: z
.string() default_tax: z.number(),
.min(3, "Unit price currency code must be at least 3 characters long"), status: z.string(),
}), lang_code: z.string(),
discount: z.object({ currency_code: z.string(),
amount: z.number().nonnegative("Discount amount cannot be negative"),
scale: z.number().int().nonnegative("Discount scale must be a non-negative integer"),
}),
})
),
}); });
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>; export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;

View File

@ -3,13 +3,30 @@ import * as z from "zod/v4";
export const CustomerCreationResponseSchema = z.object({ export const CustomerCreationResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), reference: z.string(),
invoice_number: z.string(),
invoice_series: z.string(), is_freelancer: z.boolean(),
issue_date: z.iso.datetime({ offset: true }), name: z.string(),
operation_date: z.iso.datetime({ offset: true }), trade_name: z.string(),
language_code: z.string(), tin: z.string(),
currency: z.string(),
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
email: z.string(),
phone: z.string(),
fax: z.string(),
website: z.string(),
legal_record: z.string(),
default_tax: z.number(),
status: z.string(),
lang_code: z.string(),
currency_code: z.string(),
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}); });

View File

@ -3,13 +3,30 @@ import * as z from "zod/v4";
export const GetCustomerByIdResponseSchema = z.object({ export const GetCustomerByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), reference: z.string(),
invoice_number: z.string(),
invoice_series: z.string(), is_freelancer: z.boolean(),
issue_date: z.iso.datetime({ offset: true }), name: z.string(),
operation_date: z.iso.datetime({ offset: true }), trade_name: z.string(),
language_code: z.string(), tin: z.string(),
currency: z.string(),
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
email: z.string(),
phone: z.string(),
fax: z.string(),
website: z.string(),
legal_record: z.string(),
default_tax: z.number(),
status: z.string(),
lang_code: z.string(),
currency_code: z.string(),
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}); });

View File

@ -31,5 +31,5 @@
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"packageManager": "pnpm@10.14.0" "packageManager": "pnpm@10.15.0"
} }