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 { CreateCustomerCommandDTO } from "../../../../../common/dto";
import { CreateCustomerUseCase } from "../../../../application";
import {
ExpressController,
authGuard,
errorMapper,
forbidQueryFieldGuard,
tenantGuard,
} from "@erp/core/api";
import { CreateCustomerRequestDTO } from "../../../../common/dto";
import { CreateCustomerUseCase } from "../../../application";
export class CreateCustomerController extends ExpressController {
public constructor(private readonly createCustomer: CreateCustomerUseCase) {
public constructor(private readonly useCase: CreateCustomerUseCase) {
super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
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)
dto.customerCompanyId = user.companyId;
*/
const result = await this.createCustomer.execute(dto);
const result = await this.useCase.execute(dto);
if (result.isFailure) {
console.log(result.error);
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}
return this.created(result.data);
return result.match(
(data) => this.created(data),
(err) => this.handleApiError(errorMapper.toApiError(err))
);
}
}

View File

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

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

View File

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

View File

@ -3,13 +3,30 @@ import * as z from "zod/v4";
export const CustomerCreationResponseSchema = z.object({
id: z.uuid(),
invoice_status: z.string(),
invoice_number: z.string(),
invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(),
currency: z.string(),
reference: z.string(),
is_freelancer: z.boolean(),
name: z.string(),
trade_name: z.string(),
tin: 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(),
});

View File

@ -3,13 +3,30 @@ import * as z from "zod/v4";
export const GetCustomerByIdResponseSchema = z.object({
id: z.uuid(),
invoice_status: z.string(),
invoice_number: z.string(),
invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(),
currency: z.string(),
reference: z.string(),
is_freelancer: z.boolean(),
name: z.string(),
trade_name: z.string(),
tin: 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(),
});

View File

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