Compare commits
5 Commits
1f08645c3e
...
c8eff4e9fc
| Author | SHA1 | Date | |
|---|---|---|---|
| c8eff4e9fc | |||
| 2c84dc26bd | |||
| 55983f0295 | |||
| 5c51093b1d | |||
| d60b8276f6 |
@ -1,3 +1,4 @@
|
|||||||
|
import { globalErrorHandler } from "@erp/core/api";
|
||||||
import cors, { CorsOptions } from "cors";
|
import cors, { CorsOptions } from "cors";
|
||||||
import express, { Application } from "express";
|
import express, { Application } from "express";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
@ -86,5 +87,10 @@ export function createApp(): Application {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestión global de errores.
|
||||||
|
// Siempre al final de la cadena de middlewares
|
||||||
|
// y después de las rutas.
|
||||||
|
app.use(globalErrorHandler);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,3 @@ export const v1Routes = (): Router => {
|
|||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gestión global de errores.
|
|
||||||
// Siempre al final de la cadena de middlewares
|
|
||||||
// y después de las rutas.
|
|
||||||
//app.use(globalErrorHandler);
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { loggerSingleton } from "@repo/rdx-logger";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import { logger } from "../../helpers";
|
||||||
import { ITransactionManager } from "./transaction-manager.interface";
|
import { ITransactionManager } from "./transaction-manager.interface";
|
||||||
|
|
||||||
const logger = loggerSingleton();
|
|
||||||
|
|
||||||
export abstract class TransactionManager implements ITransactionManager {
|
export abstract class TransactionManager implements ITransactionManager {
|
||||||
protected _transaction: unknown | null = null;
|
protected _transaction: unknown | null = null;
|
||||||
protected _isCompleted = false;
|
protected _isCompleted = false;
|
||||||
|
|||||||
@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DomainValidationError,
|
DomainValidationError,
|
||||||
ValidationErrorCollection,
|
|
||||||
isDomainValidationError,
|
isDomainValidationError,
|
||||||
isValidationErrorCollection,
|
isValidationErrorCollection,
|
||||||
|
ValidationErrorCollection,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { logger } from "../../../helpers";
|
||||||
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "../api-error-mapper";
|
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "../api-error-mapper";
|
||||||
|
|
||||||
// ✅ Construye tu mapper una vez (composition root del adaptador HTTP)
|
// ✅ Construye tu mapper una vez (composition root del adaptador HTTP)
|
||||||
@ -10,7 +11,7 @@ export const globalErrorHandler = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
console.error(`❌ Global unhandled error: ${error.message}`);
|
//console.error(`❌ Global unhandled error: ${error.message}`);
|
||||||
|
|
||||||
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
@ -26,7 +27,9 @@ export const globalErrorHandler = async (
|
|||||||
const body = toProblemJson(apiError, ctx);
|
const body = toProblemJson(apiError, ctx);
|
||||||
|
|
||||||
// 👇 Log interno con cause/traza (no lo exponemos al cliente)
|
// 👇 Log interno con cause/traza (no lo exponemos al cliente)
|
||||||
// logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`);
|
logger.error(`❌ Global unhandled API error: ${error.message}`, {
|
||||||
|
label: "globalErrorHandler",
|
||||||
|
});
|
||||||
|
|
||||||
res.status(apiError.status).json(body);
|
res.status(apiError.status).json(body);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -69,10 +69,11 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
payment_method: payment,
|
payment_method: payment,
|
||||||
|
|
||||||
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
||||||
|
items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
|
||||||
|
|
||||||
discount_percentage: invoice.discountPercentage.toObjectString(),
|
discount_percentage: invoice.discountPercentage.toObjectString(),
|
||||||
|
discount_amount: allAmounts.headerDiscountAmount.toObjectString(),
|
||||||
|
|
||||||
discount_amount: allAmounts.discountAmount.toObjectString(),
|
|
||||||
taxable_amount: allAmounts.taxableAmount.toObjectString(),
|
taxable_amount: allAmounts.taxableAmount.toObjectString(),
|
||||||
taxes_amount: allAmounts.taxesAmount.toObjectString(),
|
taxes_amount: allAmounts.taxesAmount.toObjectString(),
|
||||||
total_amount: allAmounts.totalAmount.toObjectString(),
|
total_amount: allAmounts.totalAmount.toObjectString(),
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { ITransactionManager } from "@erp/core/api";
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { InvalidProformaStatusError } from "../../domain";
|
import { ProformaCannotBeConvertedToInvoiceError } from "../../domain";
|
||||||
import { StatusInvoiceIsApprovedSpecification } from "../../domain/specs";
|
import { ProformaCanTranstionToIssuedSpecification } from "../../domain/specs";
|
||||||
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
||||||
|
|
||||||
type IssueCustomerInvoiceUseCaseInput = {
|
type IssueCustomerInvoiceUseCaseInput = {
|
||||||
@ -52,9 +52,9 @@ export class IssueCustomerInvoiceUseCase {
|
|||||||
const proforma = proformaResult.data;
|
const proforma = proformaResult.data;
|
||||||
|
|
||||||
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
||||||
const isApprovedSpec = new StatusInvoiceIsApprovedSpecification();
|
const isOk = new ProformaCanTranstionToIssuedSpecification();
|
||||||
if (!(await isApprovedSpec.isSatisfiedBy(proforma))) {
|
if (!(await isOk.isSatisfiedBy(proforma))) {
|
||||||
return Result.fail(new InvalidProformaStatusError(proformaId.toString()));
|
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proformaId.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 3. Generar nueva factura */
|
/** 3. Generar nueva factura */
|
||||||
@ -71,7 +71,7 @@ export class IssueCustomerInvoiceUseCase {
|
|||||||
|
|
||||||
// props base obtenidas del agregado proforma
|
// props base obtenidas del agregado proforma
|
||||||
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
||||||
invoiceNumber: Maybe.some(newIssueNumber),
|
invoiceNumber: newIssueNumber,
|
||||||
invoiceDate: UtcDate.today(),
|
invoiceDate: UtcDate.today(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -60,11 +60,13 @@ export type CustomerInvoicePatchProps = Partial<
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ICustomerInvoice {
|
export interface ICustomerInvoice {
|
||||||
|
canTransitionTo(nextStatus: string): boolean;
|
||||||
|
|
||||||
hasRecipient: boolean;
|
hasRecipient: boolean;
|
||||||
hasPaymentMethod: boolean;
|
hasPaymentMethod: boolean;
|
||||||
|
|
||||||
getSubtotalAmount(): InvoiceAmount;
|
_getSubtotalAmount(): InvoiceAmount;
|
||||||
getDiscountAmount(): InvoiceAmount;
|
getHeaderDiscountAmount(): InvoiceAmount;
|
||||||
|
|
||||||
getTaxableAmount(): InvoiceAmount;
|
getTaxableAmount(): InvoiceAmount;
|
||||||
getTaxesAmount(): InvoiceAmount;
|
getTaxesAmount(): InvoiceAmount;
|
||||||
@ -92,6 +94,18 @@ export class CustomerInvoice
|
|||||||
currencyCode: props.currencyCode,
|
currencyCode: props.currencyCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getHeaderDiscountAmount(): InvoiceAmount {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getTaxableAmount(): InvoiceAmount {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getTaxesAmount(): InvoiceAmount {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getTotalAmount(): InvoiceAmount {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
||||||
const customerInvoice = new CustomerInvoice(props, id);
|
const customerInvoice = new CustomerInvoice(props, id);
|
||||||
@ -155,6 +169,10 @@ export class CustomerInvoice
|
|||||||
return this.props.status;
|
return this.props.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canTransitionTo(nextStatus: string): boolean {
|
||||||
|
return this.props.status.canTransitionTo(nextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
public get series(): Maybe<CustomerInvoiceSerie> {
|
public get series(): Maybe<CustomerInvoiceSerie> {
|
||||||
return this.props.series;
|
return this.props.series;
|
||||||
}
|
}
|
||||||
@ -216,15 +234,19 @@ export class CustomerInvoice
|
|||||||
return this.paymentMethod.isSome();
|
return this.paymentMethod.isSome();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDiscountAmount(subtotalAmount: InvoiceAmount): InvoiceAmount {
|
private _getHeaderDiscountAmount(
|
||||||
return subtotalAmount.percentage(this.discountPercentage);
|
subtotalAmount: InvoiceAmount,
|
||||||
|
itemsDiscountAmount: InvoiceAmount
|
||||||
|
): InvoiceAmount {
|
||||||
|
return subtotalAmount.subtract(itemsDiscountAmount).percentage(this.discountPercentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTaxableAmount(
|
private _getTaxableAmount(
|
||||||
subtotalAmount: InvoiceAmount,
|
subtotalAmount: InvoiceAmount,
|
||||||
discountAmount: InvoiceAmount
|
itemsDiscountAmount: InvoiceAmount,
|
||||||
|
headerDiscountAmount: InvoiceAmount
|
||||||
): InvoiceAmount {
|
): InvoiceAmount {
|
||||||
return subtotalAmount.subtract(discountAmount);
|
return subtotalAmount.subtract(itemsDiscountAmount).subtract(headerDiscountAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
|
private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
|
||||||
@ -247,7 +269,7 @@ export class CustomerInvoice
|
|||||||
return taxableAmount.add(taxesAmount);
|
return taxableAmount.add(taxesAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSubtotalAmount(): InvoiceAmount {
|
public _getSubtotalAmount(): InvoiceAmount {
|
||||||
const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2);
|
const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2);
|
||||||
|
|
||||||
return InvoiceAmount.create({
|
return InvoiceAmount.create({
|
||||||
@ -256,12 +278,21 @@ export class CustomerInvoice
|
|||||||
}).data as InvoiceAmount;
|
}).data as InvoiceAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDiscountAmount(): InvoiceAmount {
|
public _getItemsDiscountAmount(): InvoiceAmount {
|
||||||
return this._getDiscountAmount(this.getSubtotalAmount());
|
const itemsDiscountAmount = this.items.getDiscountAmount().convertScale(2);
|
||||||
|
|
||||||
|
return InvoiceAmount.create({
|
||||||
|
value: itemsDiscountAmount.value,
|
||||||
|
currency_code: this.currencyCode.code,
|
||||||
|
}).data as InvoiceAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public getHeaderDiscountAmount(): InvoiceAmount {
|
||||||
|
return this._getHeaderDiscountAmount(this.getSubtotalAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxableAmount(): InvoiceAmount {
|
public getTaxableAmount(): InvoiceAmount {
|
||||||
return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
|
return this._getTaxableAmount(this.getSubtotalAmount(), this.getHeaderDiscountAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxesAmount(): InvoiceAmount {
|
public getTaxesAmount(): InvoiceAmount {
|
||||||
@ -273,7 +304,7 @@ export class CustomerInvoice
|
|||||||
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
||||||
|
|
||||||
return this._getTotalAmount(taxableAmount, taxesAmount);
|
return this._getTotalAmount(taxableAmount, taxesAmount);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
public getTaxes(): InvoiceTaxTotal[] {
|
public getTaxes(): InvoiceTaxTotal[] {
|
||||||
const itemTaxes = this.items.getTaxesAmountByTaxes();
|
const itemTaxes = this.items.getTaxesAmountByTaxes();
|
||||||
@ -294,15 +325,23 @@ export class CustomerInvoice
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAllAmounts() {
|
public getAllAmounts() {
|
||||||
const subtotalAmount = this.getSubtotalAmount();
|
const subtotalAmount = this._getSubtotalAmount(); // Sin IVA ni dtos de línea
|
||||||
const discountAmount = this._getDiscountAmount(subtotalAmount);
|
const itemDiscountAmount = this._getItemsDiscountAmount(); // Suma de los Importes de descuentos de linea
|
||||||
const taxableAmount = this._getTaxableAmount(subtotalAmount, discountAmount);
|
const headerDiscountAmount = this._getHeaderDiscountAmount(subtotalAmount, itemDiscountAmount); // Importe de descuento de cabecera
|
||||||
|
|
||||||
|
const taxableAmount = this._getTaxableAmount(
|
||||||
|
subtotalAmount,
|
||||||
|
itemDiscountAmount,
|
||||||
|
headerDiscountAmount
|
||||||
|
); //
|
||||||
|
|
||||||
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
||||||
const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount);
|
const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotalAmount,
|
subtotalAmount,
|
||||||
discountAmount,
|
itemDiscountAmount,
|
||||||
|
headerDiscountAmount,
|
||||||
taxableAmount,
|
taxableAmount,
|
||||||
taxesAmount,
|
taxesAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|||||||
*/
|
*/
|
||||||
public getSubtotalAmount(): ItemAmount {
|
public getSubtotalAmount(): ItemAmount {
|
||||||
return this.getAll().reduce(
|
return this.getAll().reduce(
|
||||||
(total, tax) => total.add(tax.getSubtotalAmount()),
|
(total, item) => total.add(item.getSubtotalAmount()),
|
||||||
ItemAmount.zero(this._currencyCode.code)
|
ItemAmount.zero(this._currencyCode.code)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./customer-invoice-id-already-exits-error";
|
export * from "./customer-invoice-id-already-exits-error";
|
||||||
export * from "./invalid-proforma-status-error";
|
export * from "./proforma-cannot-be-converted-to-invoice-error";
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import { DomainError } from "@repo/rdx-ddd";
|
|
||||||
|
|
||||||
export class InvalidProformaStatusError extends DomainError {
|
|
||||||
constructor(id: string, options?: ErrorOptions) {
|
|
||||||
super(`Error. Proforma with id '${id}' has invalid status.`, options);
|
|
||||||
this.name = "InvalidProformaStatusError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isInvalidProformaStatusError = (e: unknown): e is InvalidProformaStatusError =>
|
|
||||||
e instanceof InvalidProformaStatusError;
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { DomainError } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error de dominio que indica que una Proforma no puede convertirse a Factura (issue).
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Se lanza cuando el flujo de emisión (issue) desde una Proforma no es válido
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class ProformaCannotBeConvertedToInvoiceError extends DomainError {
|
||||||
|
/**
|
||||||
|
* Crea una instancia del error con el identificador de la Proforma.
|
||||||
|
*
|
||||||
|
* @param id - Identificador de la Proforma.
|
||||||
|
* @param options - Opciones nativas de Error (puedes pasar `cause`).
|
||||||
|
*/
|
||||||
|
constructor(id: string, options?: ErrorOptions) {
|
||||||
|
super(`Error. Proforma with id '${id}' cannot be converted to an Invoice.`, options);
|
||||||
|
this.name = "ProformaCannotBeConvertedToInvoiceError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* *Type guard* para `ProformaCannotBeConvertedToInvoiceError`.
|
||||||
|
*
|
||||||
|
* @param e - Error desconocido
|
||||||
|
* @returns `true` si `e` es `ProformaCannotBeConvertedToInvoiceError`
|
||||||
|
*/
|
||||||
|
export const isProformaCannotBeConvertedToInvoiceError = (
|
||||||
|
e: unknown
|
||||||
|
): e is ProformaCannotBeConvertedToInvoiceError =>
|
||||||
|
e instanceof ProformaCannotBeConvertedToInvoiceError;
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./status-invoice-is-approved.specification";
|
export * from "./proforma-can-transtion-to-issued.specification";
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { CompositeSpecification } from "@repo/rdx-ddd";
|
||||||
|
import { CustomerInvoice } from "../aggregates";
|
||||||
|
import { INVOICE_STATUS } from "../value-objects";
|
||||||
|
|
||||||
|
export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||||
|
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
|
||||||
|
return proforma.isProforma && proforma.canTransitionTo(INVOICE_STATUS.ISSUED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { CompositeSpecification } from "@repo/rdx-ddd";
|
|
||||||
import { CustomerInvoice } from "../aggregates";
|
|
||||||
|
|
||||||
export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification<CustomerInvoice> {
|
|
||||||
public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> {
|
|
||||||
return invoice.status.isApproved();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,9 +11,8 @@ export enum INVOICE_STATUS {
|
|||||||
APPROVED = "approved", // <- Proforma
|
APPROVED = "approved", // <- Proforma
|
||||||
REJECTED = "rejected", // <- Proforma
|
REJECTED = "rejected", // <- Proforma
|
||||||
|
|
||||||
// status === issued <- (si is_proforma === true) => Es una proforma (histórica)
|
// status === "issued" <- (si is_proforma === true) => Es una proforma (histórica)
|
||||||
// status === issued <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
// status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||||
|
|
||||||
ISSUED = "issued",
|
ISSUED = "issued",
|
||||||
}
|
}
|
||||||
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
||||||
@ -93,15 +92,6 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
|
|||||||
return CustomerInvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
return CustomerInvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
transitionTo(nextStatus: string): Result<CustomerInvoiceStatus, Error> {
|
|
||||||
if (!this.canTransitionTo(nextStatus)) {
|
|
||||||
return Result.fail(
|
|
||||||
new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CustomerInvoiceStatus.create(nextStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return String(this.props.value);
|
return String(this.props.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
|
|
||||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||||
import { CreateCustomerInvoiceUseCase } from "../../../application";
|
import { CreateCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class CreateCustomerInvoiceController extends ExpressController {
|
export class CreateCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(private readonly useCase: CreateCustomerInvoiceUseCase) {
|
public constructor(private readonly useCase: CreateCustomerInvoiceUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { DeleteCustomerInvoiceUseCase } from "../../../application";
|
import { DeleteCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class DeleteCustomerInvoiceController extends ExpressController {
|
export class DeleteCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -7,6 +8,8 @@ export class DeleteCustomerInvoiceController extends ExpressController {
|
|||||||
/* private readonly presenter: any */
|
/* private readonly presenter: any */
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { GetCustomerInvoiceUseCase } from "../../../application";
|
import { GetCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class GetCustomerInvoiceController extends ExpressController {
|
export class GetCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(private readonly useCase: GetCustomerInvoiceUseCase) {
|
public constructor(private readonly useCase: GetCustomerInvoiceUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { IssueCustomerInvoiceUseCase } from "../../../application";
|
import { IssueCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class IssueCustomerInvoiceController extends ExpressController {
|
export class IssueCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) {
|
public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { ListCustomerInvoicesUseCase } from "../../../application";
|
import { ListCustomerInvoicesUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class ListCustomerInvoicesController extends ExpressController {
|
export class ListCustomerInvoicesController extends ExpressController {
|
||||||
public constructor(private readonly useCase: ListCustomerInvoicesUseCase) {
|
public constructor(private readonly useCase: ListCustomerInvoicesUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { ReportCustomerInvoiceUseCase } from "../../../application";
|
import { ReportCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class ReportCustomerInvoiceController extends ExpressController {
|
export class ReportCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
|
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
|
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
|
||||||
import { UpdateCustomerInvoiceUseCase } from "../../../application";
|
import { UpdateCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||||
|
|
||||||
export class UpdateCustomerInvoiceController extends ExpressController {
|
export class UpdateCustomerInvoiceController extends ExpressController {
|
||||||
public constructor(private readonly useCase: UpdateCustomerInvoiceUseCase) {
|
public constructor(private readonly useCase: UpdateCustomerInvoiceUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError
|
// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError
|
||||||
// (si defines un error más ubicuo dentro del BC con su propia clase)
|
// (si defines un error más ubicuo dentro del BC con su propia clase)
|
||||||
|
|
||||||
import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api";
|
import {
|
||||||
|
ApiErrorMapper,
|
||||||
|
ConflictApiError,
|
||||||
|
ErrorToApiRule,
|
||||||
|
ValidationApiError,
|
||||||
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceIdAlreadyExistsError,
|
CustomerInvoiceIdAlreadyExistsError,
|
||||||
isCustomerInvoiceIdAlreadyExistsError,
|
isCustomerInvoiceIdAlreadyExistsError,
|
||||||
|
isProformaCannotBeConvertedToInvoiceError,
|
||||||
|
ProformaCannotBeConvertedToInvoiceError,
|
||||||
} from "../../domain";
|
} from "../../domain";
|
||||||
|
|
||||||
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||||
@ -18,6 +25,17 @@ const invoiceDuplicateRule: ErrorToApiRule = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const proformaConversionRule: ErrorToApiRule = {
|
||||||
|
priority: 120,
|
||||||
|
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
|
||||||
|
build: (e) =>
|
||||||
|
new ValidationApiError(
|
||||||
|
(e as ProformaCannotBeConvertedToInvoiceError).message ||
|
||||||
|
"Proforma cannot be converted to an Invoice."
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper =
|
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||||
ApiErrorMapper.default().register(invoiceDuplicateRule);
|
.register(invoiceDuplicateRule)
|
||||||
|
.register(proformaConversionRule);
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
|
items_discount_amount: MoneySchema,
|
||||||
discount_percentage: PercentageSchema,
|
discount_percentage: PercentageSchema,
|
||||||
discount_amount: MoneySchema,
|
discount_amount: MoneySchema,
|
||||||
taxable_amount: MoneySchema,
|
taxable_amount: MoneySchema,
|
||||||
|
|||||||
@ -1,20 +1,19 @@
|
|||||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||||
import { InputGroup, InputGroupTextarea } from "@repo/shadcn-ui/components";
|
import { InputGroup, InputGroupTextarea } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { useInvoiceContext } from '../../../context';
|
import { useInvoiceContext } from "../../../context";
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
|
||||||
import { AmountInputField } from './amount-input-field';
|
import { AmountInputField } from "./amount-input-field";
|
||||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
|
||||||
import { ItemDataTableRowActions } from './items-data-table-row-actions';
|
import { ItemDataTableRowActions } from "./items-data-table-row-actions";
|
||||||
import { PercentageInputField } from './percentage-input-field';
|
import { PercentageInputField } from "./percentage-input-field";
|
||||||
import { QuantityInputField } from './quantity-input-field';
|
import { QuantityInputField } from "./quantity-input-field";
|
||||||
|
|
||||||
|
|
||||||
export interface InvoiceItemFormData {
|
export interface InvoiceItemFormData {
|
||||||
id: string; // ← mapea RHF field.id aquí
|
id: string; // ← mapea RHF field.id aquí
|
||||||
description: string;
|
description: string;
|
||||||
quantity: number | "";
|
quantity: number | "";
|
||||||
unit_amount: number | "";
|
unit_amount: number | "";
|
||||||
@ -22,53 +21,62 @@ export interface InvoiceItemFormData {
|
|||||||
tax_codes: string[];
|
tax_codes: string[];
|
||||||
total_amount: number | ""; // readonly calculado
|
total_amount: number | ""; // readonly calculado
|
||||||
}
|
}
|
||||||
export interface InvoiceFormData { items: InvoiceItemFormData[] }
|
export interface InvoiceFormData {
|
||||||
|
items: InvoiceItemFormData[];
|
||||||
|
}
|
||||||
|
|
||||||
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||||
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
|
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
|
||||||
const { control } = useFormContext<InvoiceFormData>();
|
const { control } = useFormContext<InvoiceFormData>();
|
||||||
|
|
||||||
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
|
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
|
||||||
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(() => [
|
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(
|
||||||
{
|
() => [
|
||||||
id: 'position',
|
{
|
||||||
header: ({ column }) => (
|
id: "position",
|
||||||
<DataTableColumnHeader column={column} title={"#"} className='text-center' />
|
header: ({ column }) => (
|
||||||
),
|
<DataTableColumnHeader column={column} title={"#"} className='text-center' />
|
||||||
cell: ({ row }) => row.index + 1,
|
),
|
||||||
enableSorting: false,
|
cell: ({ row }) => row.index + 1,
|
||||||
size: 32,
|
enableSorting: false,
|
||||||
},
|
size: 32,
|
||||||
{
|
},
|
||||||
accessorKey: "description",
|
{
|
||||||
header: ({ column }) => (
|
accessorKey: "description",
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.description.label")} className='text-left' />
|
header: ({ column }) => (
|
||||||
),
|
<DataTableColumnHeader
|
||||||
cell: ({ row }) => (
|
column={column}
|
||||||
<Controller
|
title={t("form_fields.item.description.label")}
|
||||||
control={control}
|
className='text-left'
|
||||||
name={`items.${row.index}.description`}
|
/>
|
||||||
render={({ field }) => (
|
),
|
||||||
<InputGroup>
|
cell: ({ row }) => (
|
||||||
<InputGroupTextarea {...field}
|
<Controller
|
||||||
id={`desc-${row.original.id}`} // ← estable
|
control={control}
|
||||||
rows={1}
|
name={`items.${row.index}.description`}
|
||||||
aria-label={t("form_fields.item.description.label")}
|
render={({ field }) => (
|
||||||
spellCheck
|
<InputGroup>
|
||||||
readOnly={readOnly}
|
<InputGroupTextarea
|
||||||
// auto-grow simple
|
{...field}
|
||||||
onInput={(e) => {
|
id={`desc-${row.original.id}`} // ← estable
|
||||||
const el = e.currentTarget;
|
rows={1}
|
||||||
el.style.height = "auto";
|
aria-label={t("form_fields.item.description.label")}
|
||||||
el.style.height = `${el.scrollHeight}px`;
|
spellCheck
|
||||||
}}
|
readOnly={readOnly}
|
||||||
className={cn(
|
// auto-grow simple
|
||||||
"min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition",
|
onInput={(e) => {
|
||||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
|
const el = e.currentTarget;
|
||||||
"focus:resize-y"
|
el.style.height = "auto";
|
||||||
)}
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
data-cell-focus />
|
}}
|
||||||
{/*<InputGroupAddon align="block-end">
|
className={cn(
|
||||||
|
"min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
|
||||||
|
"focus:resize-y"
|
||||||
|
)}
|
||||||
|
data-cell-focus
|
||||||
|
/>
|
||||||
|
{/*<InputGroupAddon align="block-end">
|
||||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
variant="default"
|
variant="default"
|
||||||
@ -80,129 +88,186 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
|||||||
<span className="sr-only">Send</span>
|
<span className="sr-only">Send</span>
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
</InputGroupAddon>*/}
|
</InputGroupAddon>*/}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
),
|
||||||
),
|
enableSorting: false,
|
||||||
enableSorting: false,
|
size: 480,
|
||||||
size: 480, minSize: 240, maxSize: 768,
|
minSize: 240,
|
||||||
},
|
maxSize: 768,
|
||||||
{
|
},
|
||||||
accessorKey: "quantity",
|
{
|
||||||
header: ({ column }) => (
|
accessorKey: "quantity",
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.quantity.label")} className='text-right' />
|
header: ({ column }) => (
|
||||||
),
|
<DataTableColumnHeader
|
||||||
cell: ({ row }) => (
|
column={column}
|
||||||
<QuantityInputField
|
title={t("form_fields.item.quantity.label")}
|
||||||
control={control}
|
className='text-right'
|
||||||
name={`items.${row.index}.quantity`}
|
/>
|
||||||
readOnly={readOnly}
|
),
|
||||||
inputId={`qty-${row.original.id}`}
|
cell: ({ row }) => (
|
||||||
emptyMode="blank"
|
<QuantityInputField
|
||||||
data-row-index={row.index}
|
control={control}
|
||||||
data-col-index={4}
|
name={`items.${row.index}.quantity`}
|
||||||
data-cell-focus
|
readOnly={readOnly}
|
||||||
className="font-base"
|
inputId={`qty-${row.original.id}`}
|
||||||
/>
|
emptyMode='blank'
|
||||||
),
|
data-row-index={row.index}
|
||||||
enableSorting: false,
|
data-col-index={4}
|
||||||
size: 52, minSize: 48, maxSize: 64,
|
data-cell-focus
|
||||||
},
|
className='font-base'
|
||||||
{
|
/>
|
||||||
accessorKey: "unit_amount",
|
),
|
||||||
header: ({ column }) => (
|
enableSorting: false,
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.unit_amount.label")} className='text-right' />
|
size: 52,
|
||||||
),
|
minSize: 48,
|
||||||
cell: ({ row }) => (
|
maxSize: 64,
|
||||||
<AmountInputField
|
},
|
||||||
control={control}
|
{
|
||||||
name={`items.${row.index}.unit_amount`}
|
accessorKey: "unit_amount",
|
||||||
readOnly={readOnly}
|
header: ({ column }) => (
|
||||||
inputId={`unit-${row.original.id}`}
|
<DataTableColumnHeader
|
||||||
scale={4}
|
column={column}
|
||||||
currencyCode={currency_code}
|
title={t("form_fields.item.unit_amount.label")}
|
||||||
languageCode={language_code}
|
className='text-right'
|
||||||
data-row-index={row.index}
|
/>
|
||||||
data-col-index={5}
|
),
|
||||||
data-cell-focus
|
cell: ({ row }) => (
|
||||||
className="font-base"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 120, minSize: 100, maxSize: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "discount_percentage",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.discount_percentage.label")} className='text-right' />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<PercentageInputField
|
|
||||||
control={control}
|
|
||||||
name={`items.${row.index}.discount_percentage`}
|
|
||||||
readOnly={readOnly}
|
|
||||||
inputId={`disc-${row.original.id}`}
|
|
||||||
scale={4}
|
|
||||||
data-row-index={row.index}
|
|
||||||
data-col-index={6}
|
|
||||||
data-cell-focus
|
|
||||||
className="font-base"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 40, minSize: 40
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "tax_codes",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`items.${row.index}.tax_codes`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
|
||||||
{...field}
|
|
||||||
inputId={`tax-${row.original.id}`}
|
|
||||||
data-row-index={row.index}
|
|
||||||
data-col-index={7}
|
|
||||||
data-cell-focus
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 240, minSize: 232, maxSize: 320,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "total_amount",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("form_fields.item.total_amount.label")} className='text-right' />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<HoverCardTotalsSummary rowIndex={row.index}>
|
|
||||||
<AmountInputField
|
<AmountInputField
|
||||||
control={control}
|
control={control}
|
||||||
name={`items.${row.index}.total_amount`}
|
name={`items.${row.index}.unit_amount`}
|
||||||
readOnly
|
readOnly={readOnly}
|
||||||
inputId={`total-${row.original.id}`}
|
inputId={`unit-${row.original.id}`}
|
||||||
|
scale={4}
|
||||||
currencyCode={currency_code}
|
currencyCode={currency_code}
|
||||||
languageCode={language_code}
|
languageCode={language_code}
|
||||||
className="font-semibold"
|
data-row-index={row.index}
|
||||||
|
data-col-index={5}
|
||||||
|
data-cell-focus
|
||||||
|
className='font-base'
|
||||||
/>
|
/>
|
||||||
</HoverCardTotalsSummary>
|
),
|
||||||
),
|
enableSorting: false,
|
||||||
enableSorting: false,
|
size: 120,
|
||||||
size: 120, minSize: 100, maxSize: 160,
|
minSize: 100,
|
||||||
},
|
maxSize: 160,
|
||||||
{
|
},
|
||||||
id: "actions",
|
{
|
||||||
header: ({ column }) => (
|
accessorKey: "discount_percentage",
|
||||||
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
|
header: ({ column }) => (
|
||||||
),
|
<DataTableColumnHeader
|
||||||
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
|
column={column}
|
||||||
},
|
title={t("form_fields.item.discount_percentage.label")}
|
||||||
], [t, readOnly, control, currency_code, language_code,]);
|
className='text-right'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<PercentageInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.discount_percentage`}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputId={`disc-${row.original.id}`}
|
||||||
|
scale={4}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={6}
|
||||||
|
data-cell-focus
|
||||||
|
className='font-base'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 40,
|
||||||
|
minSize: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "taxable_amount",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("form_fields.item.taxable_amount.label")}
|
||||||
|
className='text-right'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<AmountInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.taxable_amount`}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputId={`unit-${row.original.id}`}
|
||||||
|
scale={4}
|
||||||
|
currencyCode={currency_code}
|
||||||
|
languageCode={language_code}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={5}
|
||||||
|
data-cell-focus
|
||||||
|
className='font-base'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 120,
|
||||||
|
minSize: 100,
|
||||||
|
maxSize: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "tax_codes",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.tax_codes`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
|
{...field}
|
||||||
|
inputId={`tax-${row.original.id}`}
|
||||||
|
data-row-index={row.index}
|
||||||
|
data-col-index={7}
|
||||||
|
data-cell-focus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 120,
|
||||||
|
minSize: 130,
|
||||||
|
maxSize: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "total_amount",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("form_fields.item.total_amount.label")}
|
||||||
|
className='text-right'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<HoverCardTotalsSummary rowIndex={row.index}>
|
||||||
|
<AmountInputField
|
||||||
|
control={control}
|
||||||
|
name={`items.${row.index}.total_amount`}
|
||||||
|
readOnly
|
||||||
|
inputId={`total-${row.original.id}`}
|
||||||
|
currencyCode={currency_code}
|
||||||
|
languageCode={language_code}
|
||||||
|
className='font-semibold'
|
||||||
|
/>
|
||||||
|
</HoverCardTotalsSummary>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
size: 120,
|
||||||
|
minSize: 100,
|
||||||
|
maxSize: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
|
||||||
|
),
|
||||||
|
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, readOnly, control, currency_code, language_code]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { CreateCustomerRequestDTO } from "../../../../common/dto";
|
import { CreateCustomerRequestDTO } from "../../../../common/dto";
|
||||||
import { CreateCustomerUseCase } from "../../../application";
|
import { CreateCustomerUseCase } from "../../../application";
|
||||||
|
import { customersApiErrorMapper } from "../customer-api-error-mapper";
|
||||||
|
|
||||||
export class CreateCustomerController extends ExpressController {
|
export class CreateCustomerController extends ExpressController {
|
||||||
public constructor(private readonly useCase: CreateCustomerUseCase) {
|
public constructor(private readonly useCase: CreateCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customersApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { DeleteCustomerUseCase } from "../../../application";
|
import { DeleteCustomerUseCase } from "../../../application";
|
||||||
|
import { customersApiErrorMapper } from "../customer-api-error-mapper";
|
||||||
|
|
||||||
export class DeleteCustomerController extends ExpressController {
|
export class DeleteCustomerController extends ExpressController {
|
||||||
public constructor(private readonly useCase: DeleteCustomerUseCase) {
|
public constructor(private readonly useCase: DeleteCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customersApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { GetCustomerUseCase } from "../../../application";
|
import { GetCustomerUseCase } from "../../../application";
|
||||||
|
import { customersApiErrorMapper } from "../customer-api-error-mapper";
|
||||||
|
|
||||||
export class GetCustomerController extends ExpressController {
|
export class GetCustomerController extends ExpressController {
|
||||||
public constructor(private readonly useCase: GetCustomerUseCase) {
|
public constructor(private readonly useCase: GetCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customersApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { ListCustomersUseCase } from "../../../application";
|
import { ListCustomersUseCase } from "../../../application";
|
||||||
|
import { customersApiErrorMapper } from "../customer-api-error-mapper";
|
||||||
|
|
||||||
export class ListCustomersController extends ExpressController {
|
export class ListCustomersController extends ExpressController {
|
||||||
public constructor(private readonly listCustomers: ListCustomersUseCase) {
|
public constructor(private readonly listCustomers: ListCustomersUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customersApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||||
import { UpdateCustomerUseCase } from "../../../application";
|
import { UpdateCustomerUseCase } from "../../../application";
|
||||||
|
import { customersApiErrorMapper } from "../customer-api-error-mapper";
|
||||||
|
|
||||||
export class UpdateCustomerController extends ExpressController {
|
export class UpdateCustomerController extends ExpressController {
|
||||||
public constructor(private readonly useCase: UpdateCustomerUseCase) {
|
public constructor(private readonly useCase: UpdateCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
this.errorMapper = customersApiErrorMapper;
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api";
|
import { ApiErrorMapper, ErrorToApiRule, NotFoundApiError } from "@erp/core/api";
|
||||||
import { CustomerNotFoundError, isCustomerNotFoundError } from "../../domain";
|
import { CustomerNotFoundError, isCustomerNotFoundError } from "../../domain";
|
||||||
|
|
||||||
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||||
@ -6,7 +6,7 @@ const customerNotFoundRule: ErrorToApiRule = {
|
|||||||
priority: 120,
|
priority: 120,
|
||||||
matches: (e) => isCustomerNotFoundError(e),
|
matches: (e) => isCustomerNotFoundError(e),
|
||||||
build: (e) =>
|
build: (e) =>
|
||||||
new ConflictApiError(
|
new NotFoundApiError(
|
||||||
(e as CustomerNotFoundError).message || "Customer with the provided id not exists."
|
(e as CustomerNotFoundError).message || "Customer with the provided id not exists."
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user