Clientes y Facturas de cliente

This commit is contained in:
David Arranz 2025-10-28 18:52:30 +01:00
parent 88f5062c03
commit 8a3bc7ac45
47 changed files with 321 additions and 210 deletions

20
.vscode/tasks.json vendored
View File

@ -1,20 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "web:dev",
"type": "shell",
"command": "pnpm --filter web dev",
"isBackground": true,
"problemMatcher": {
"owner": "vite",
"pattern": [{ "regexp": "." }],
"background": {
"activeOnStart": true,
"beginsPattern": ".*Local:.*http://.*:5173/.*",
"endsPattern": ".*ready in .*"
}
}
}
]
}

13
apps/server/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# server/Dockerfile
FROM node:22.13.1
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

View File

@ -43,7 +43,6 @@
"@erp/core": "workspace:*",
"@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*",
"@erp/verifactu": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3",
@ -79,9 +78,14 @@
"node": ">=22"
},
"tsup": {
"entry": ["src/index.ts"],
"entry": [
"src/index.ts"
],
"outDir": "dist",
"format": ["esm", "cjs"],
"format": [
"esm",
"cjs"
],
"target": "ES2022",
"sourcemap": true,
"clean": true,

View File

@ -18,7 +18,7 @@ export function getService<T = any>(name: string): T {
if (!service) {
throw new Error(`❌ Servicio "${name}" no encontrado.`);
}
return service;
return service as T;
}
/**

View File

@ -1,6 +1,6 @@
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
import customersAPIModule from "@erp/customers/api";
import verifactuAPIModule from "@erp/verifactu/api";
//import verifactuAPIModule from "@erp/verifactu/api";
import { registerModule } from "./lib";
@ -8,5 +8,5 @@ export const registerModules = () => {
//registerModule(authAPIModule);
registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule);
registerModule(verifactuAPIModule);
//registerModule(verifactuAPIModule);
};

14
apps/web/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:22.13.1 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Servir con Vite (modo dev) o un servidor web
FROM node:22.13.1
WORKDIR /app
COPY --from=build /app .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

View File

@ -10,7 +10,6 @@ export default defineConfig({
},
plugins: [react(), tailwindcss()],
resolve: {
dedupe: ["react", "react-dom"],
alias: {
"@": path.resolve(__dirname, "./src"),
},

View File

@ -1,7 +1,7 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false, "ignore": ["dist"] },
"files": { "ignoreUnknown": false, "includes": ["**", "!**/dist"] },
"formatter": {
"enabled": true,
"useEditorconfig": true,
@ -13,7 +13,7 @@
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": { "enabled": true },
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
@ -38,7 +38,15 @@
"useImportType": "off",
"noInferrableTypes": "off",
"noNonNullAssertion": "info",
"noUselessElse": "off"
"noUselessElse": "off",
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error"
},
"a11y": {
"useSemanticElements": "info"

View File

@ -1,40 +1,31 @@
version: "3.8"
name: factuges
services:
database:
image: postgres:15
container_name: myapp_db
mariadb:
image: mariadb:latest
container_name: mariadb
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydatabase
ports:
- "5432:5432"
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: factuges_db
MYSQL_USER: factuges_usr
MYSQL_PASSWORD: factuges_pass
volumes:
- db_data:/var/lib/postgresql/data
- mariadb_data:/var/lib/mysql
ports:
- "3306:3306"
backend:
build: ./apps/server
container_name: myapp_backend
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
restart: always
environment:
PMA_HOST: mariadb
MYSQL_ROOT_PASSWORD: rootpass
ports:
- "5000:5000"
- "8080:80"
depends_on:
- database
env_file: ./apps/server/.env
volumes:
- ./apps/server:/app
- /app/node_modules
frontend:
build: ./apps/client
container_name: myapp_frontend
ports:
- "3000:3000"
depends_on:
- backend
env_file: ./apps/client/.env
volumes:
- ./apps/client:/app
- /app/node_modules
- mariadb
volumes:
db_data:
mariadb_data:

View File

@ -19,7 +19,6 @@
"@types/express": "^4.17.21",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@types/react-i18next": "^8.1.0",
"typescript": "^5.8.3"
},
"dependencies": {

View File

@ -3,6 +3,7 @@
"version": "0.0.1",
"exports": {
".": "./src/common/index.ts",
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts",
"./globals.css": "./src/web/globals.css",
@ -15,7 +16,6 @@
},
"devDependencies": {
"@hookform/devtools": "^4.4.0",
"@types/axios": "^0.14.4",
"@types/dinero.js": "^1.9.4",
"@types/express": "^4.17.21",
"@types/react": "^19.1.2",
@ -25,6 +25,7 @@
"@hookform/resolvers": "^5.0.1",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"@repo/rdx-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",

View File

@ -1 +1,2 @@
export * from "./logger";
export * from "./sequelize-func";

View File

@ -0,0 +1,3 @@
import { loggerSingleton } from "@repo/rdx-logger";
export const logger = loggerSingleton();

View File

@ -1,35 +1,26 @@
import { loggerSingleton } from "@repo/rdx-logger";
import { Result } from "@repo/rdx-utils";
import { ILogger } from "../../logger";
import { ITransactionManager } from "./transaction-manager.interface";
const logger = loggerSingleton();
export abstract class TransactionManager implements ITransactionManager {
protected _transaction: unknown | null = null;
protected _isCompleted = false;
protected readonly logger!: ILogger;
constructor(logger?: ILogger) {
// Si no hay logger, usa console adaptado
this.logger = logger ?? {
info: (msg, meta) => console.info(msg, meta),
warn: (msg, meta) => console.warn(msg, meta),
error: (msg, err) => console.error(msg, err),
debug: (msg, meta) => console.debug(msg, meta),
};
}
/**
* 🔹 Inicia una transacción si no hay una activa
*/
async start(): Promise<void> {
if (this._transaction) {
this.logger.error("❌ Transaction already started. Nested transactions are not allowed.", {
logger.error("❌ Transaction already started. Nested transactions are not allowed.", {
label: "TransactionManager.start",
});
throw new Error("A transaction is already active. Nested transactions are not allowed.");
}
this._transaction = await this._startTransaction();
this._isCompleted = false;
this.logger.debug("Transaction started", {
logger.debug("Transaction started", {
label: "TransactionManager.start",
});
}
@ -39,13 +30,13 @@ export abstract class TransactionManager implements ITransactionManager {
*/
getTransaction(): any {
if (!this._transaction) {
this.logger.error("❌ No active transaction. Call start() first.", {
logger.error("❌ No active transaction. Call start() first.", {
label: "TransactionManager.getTransaction",
});
throw new Error("No active transaction. Call start() first.");
}
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed.");
logger.error("❌ Transaction already completed.");
throw new Error("Transaction already completed.");
}
@ -57,7 +48,7 @@ export abstract class TransactionManager implements ITransactionManager {
*/
async complete<T>(work: (transaction: unknown) => Promise<T>): Promise<T> {
if (this._transaction) {
this.logger.error(
logger.error(
"❌ Cannot start a new transaction inside another. Nested transactions are not allowed.",
{ label: "TransactionManager.complete" }
);
@ -77,7 +68,7 @@ export abstract class TransactionManager implements ITransactionManager {
} catch (err) {
await this.rollback();
const error = err as Error;
this.logger.error(`❌ Transaction rolled back due to error: ${error.message}`, {
logger.error(`❌ Transaction rolled back due to error: ${error.message}`, {
//stack: error.stack,
label: "TransactionManager.start",
});
@ -89,27 +80,27 @@ export abstract class TransactionManager implements ITransactionManager {
* 🔹 Métodos abstractos para manejar transacciones
*/
protected abstract _startTransaction(): Promise<any>;
protected abstract _commitTransaction(): Promise<void>;
protected abstract _commitTransaction(transaction: unknown): Promise<void>;
protected abstract _rollbackTransaction(): Promise<void>;
async commit(): Promise<void> {
if (!this._transaction) {
this.logger.error("❌ No transaction to commit.", { label: "TransactionManager.commit" });
logger.error("❌ No transaction to commit.", { label: "TransactionManager.commit" });
throw new Error("No transaction to commit.");
}
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed. Cannot commit again.", {
logger.error("❌ Transaction already completed. Cannot commit again.", {
label: "TransactionManager.commit",
});
throw new Error("Transaction already completed.");
}
try {
await this._commitTransaction();
this.logger.info("Transaction committed.", { label: "TransactionManager.commit" });
await this._commitTransaction(this._transaction);
logger.info("Transaction committed.", { label: "TransactionManager.commit" });
} catch (err) {
const error = err as Error;
this.logger.error(`❌ Error during commit: ${error.message}`, {
logger.error(`❌ Error during commit: ${error.message}`, {
stack: error.stack,
label: "TransactionManager.commit",
});
@ -123,11 +114,11 @@ export abstract class TransactionManager implements ITransactionManager {
async rollback(): Promise<void> {
if (!this._transaction) {
this.logger.error("❌ No transaction to rollback.", { label: "TransactionManager.rollback" });
logger.error("❌ No transaction to rollback.", { label: "TransactionManager.rollback" });
throw new Error("No transaction to rollback.");
}
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed. Cannot rollback again.", {
logger.error("❌ Transaction already completed. Cannot rollback again.", {
label: "TransactionManager.rollback",
});
throw new Error("Transaction already completed.");
@ -135,10 +126,10 @@ export abstract class TransactionManager implements ITransactionManager {
try {
await this._rollbackTransaction();
this.logger.info("Transaction rolled back.");
logger.info("Transaction rolled back.");
} catch (err) {
const error = err as Error;
this.logger.error(`❌ Error during rollback: ${error.message}`, {
logger.error(`❌ Error during rollback: ${error.message}`, {
stack: error.stack,
label: "TransactionManager.rollback",
});

View File

@ -1,5 +1,6 @@
import { Result } from "@repo/rdx-utils";
import { Sequelize, Transaction } from "sequelize";
import { logger } from "../../helpers";
import { TransactionManager } from "../database";
import { InfrastructureError, InfrastructureUnavailableError } from "../errors";
@ -14,9 +15,9 @@ export class SequelizeTransactionManager extends TransactionManager {
});
}
protected async _commitTransaction(): Promise<void> {
if (this._transaction) {
await this._transaction.commit();
protected async _commitTransaction(transaction: Transaction): Promise<void> {
if (transaction) {
await transaction.commit();
}
}
@ -52,7 +53,7 @@ export class SequelizeTransactionManager extends TransactionManager {
// Evita transacciones anidadas según la política del TransactionManager base
if (this._transaction) {
this.logger.error(
logger.error(
"❌ Cannot start a new transaction inside another. Nested transactions are not allowed.",
{ label: "SequelizeTransactionManager.complete" }
);
@ -74,7 +75,7 @@ export class SequelizeTransactionManager extends TransactionManager {
return result as T;
} catch (err) {
const error = err as Error;
this.logger.error(`❌ Transaction rolled back due to error: ${error.message}`, {
logger.error(`❌ Transaction rolled back due to error: ${error.message}`, {
//stack: error.stack,
label: "SequelizeTransactionManager.complete",
});

View File

@ -45,7 +45,7 @@ export function normalizeCriteriaDTO(criteria: CriteriaDTO = {}) {
// Para mantener un orden estable de filtros
const stableFilters = [...filters].sort(
(a, b) => a.field.localeCompare(b.field) || a.op.localeCompare(b.op)
(a, b) => a.field.localeCompare(b.field) || a.operator.localeCompare(b.operator)
);
return { pageNumber, pageSize, q, filters: stableFilters, orderBy, order };

View File

@ -1,5 +1,5 @@
import type { MoneyDTO } from "@erp/core/common";
import Dinero from "dinero.js";
import Dinero, { Currency } from "dinero.js";
import { MoneyDTO } from "../dto";
type DineroPlain = { amount: number; precision: number; currency: string };
@ -93,7 +93,7 @@ function dineroFromDTO(dto: MoneyDTO, fallbackCurrency = "EUR"): Dinero.Dinero {
return Dinero({
amount: Number.parseInt(n.value, 10),
precision: Number.parseInt(n.scale, 10),
currency: n.currency_code as string,
currency: n.currency_code as Currency,
});
}

View File

@ -5,6 +5,7 @@
"types": "src/index.ts",
"exports": {
".": "./src/common/index.ts",
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts",
"./globals.css": "./src/web/globals.css"
@ -31,7 +32,6 @@
"@types/express": "^4.17.21",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@types/react-i18next": "^8.1.0",
"@types/react-router-dom": "^5.3.3",
"typescript": "^5.8.3"
},

View File

@ -3,4 +3,4 @@ export * from "./format-money-dto";
export * from "./format-payment_method-dto";
export * from "./format-percentage-dto";
export * from "./format-quantity-dto";
export * from "./map-dto-to-customer-invoice-props";
//export * from "./map-dto-to-customer-invoice-props";

View File

@ -1,8 +1,8 @@
import {
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
ValidationErrorCollection,
ValidationErrorDetail,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common";

View File

@ -1,9 +1,9 @@
import { Presenter } from "@erp/core/api";
import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure";
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils";
import { ListCustomerInvoicesResponseDTO } from "../../../../common/dto";
import { CustomerInvoiceListDTO } from "../../../infrastructure";
export class ListCustomerInvoicesPresenter extends Presenter {
protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
@ -24,10 +24,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
reference: toEmptyString(invoice.reference, (value) => value.toString()),
description: toEmptyString(invoice.description, (value) => value.toString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipientDTO,
},
recipient: recipientDTO,
language_code: invoice.languageCode.code,
currency_code: invoice.currencyCode.code,

View File

@ -30,7 +30,7 @@ export class CreateCustomerInvoiceUseCase {
// 1) Mapear DTO → props de dominio
const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog });
const dtoResult = dtoMapper.map(dto);
const dtoResult = dtoMapper.map(dto, companyId);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}

View File

@ -1,19 +1,19 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import { Tax, Taxes } from "@erp/core/api";
import { Tax } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
extractOrPushError,
LanguageCode,
maybeFromNullableVO,
Percentage,
TextValue,
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { Maybe, Result } from "@repo/rdx-utils";
import {
CreateCustomerInvoiceItemRequestDTO,
CreateCustomerInvoiceRequestDTO,
@ -27,9 +27,12 @@ import {
CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
InvoicePaymentMethod,
InvoiceRecipient,
ItemAmount,
ItemDiscount,
ItemQuantity,
ItemTaxes,
} from "../../../domain";
/**
@ -53,24 +56,24 @@ export class CreateCustomerInvoicePropsMapper {
this.errors = [];
}
public map(dto: CreateCustomerInvoiceRequestDTO) {
public map(dto: CreateCustomerInvoiceRequestDTO, companyId: UniqueID) {
try {
this.errors = [];
const defaultStatus = CustomerInvoiceStatus.createDraft();
const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
const companyId = extractOrPushError(
UniqueID.create(dto.company_id),
"company_id",
this.errors
);
const isProforma = true;
const customerId = extractOrPushError(
UniqueID.create(dto.customer_id),
"customer_id",
this.errors
);
const recipient = Maybe.none<InvoiceRecipient>();
const invoiceNumber = extractOrPushError(
maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)),
"invoice_number",
@ -95,6 +98,18 @@ export class CreateCustomerInvoicePropsMapper {
this.errors
);
const reference = extractOrPushError(
maybeFromNullableVO(dto.reference, (value) => Result.ok(String(value))),
"reference",
this.errors
);
const description = extractOrPushError(
maybeFromNullableVO(dto.reference, (value) => Result.ok(String(value))),
"description",
this.errors
);
const notes = extractOrPushError(
maybeFromNullableVO(dto.notes, (value) => TextValue.create(value)),
"notes",
@ -113,6 +128,14 @@ export class CreateCustomerInvoicePropsMapper {
this.errors
);
const paymentMethod = extractOrPushError(
maybeFromNullableVO(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
this.errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: Number(dto.discount_percentage.value),
@ -131,26 +154,31 @@ export class CreateCustomerInvoicePropsMapper {
}
const invoiceProps: CustomerInvoiceProps = {
companyId: companyId!,
companyId,
isProforma,
status: defaultStatus!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
series: series!,
invoiceNumber: invoiceNumber!,
notes: notes!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
customerId: customerId!,
recipient: recipient!,
reference: reference!,
description: description!,
notes: notes!,
languageCode: this.languageCode!,
currencyCode: this.currencyCode!,
discountPercentage: discountPercentage!,
taxes: Taxes.create([]),
items: items,
paymentMethod: paymentMethod!,
discountPercentage: discountPercentage!,
};
return Result.ok({ id: invoiceId!, props: invoiceProps });
@ -202,7 +230,7 @@ export class CreateCustomerInvoicePropsMapper {
quantity: quantity!,
unitAmount: unitAmount!,
discountPercentage: discountPercentage!,
taxes,
taxes: taxes,
};
const itemResult = CustomerInvoiceItem.create(itemProps);
@ -219,7 +247,7 @@ export class CreateCustomerInvoicePropsMapper {
}
private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) {
const taxes = Taxes.create([]);
const taxes = ItemTaxes.create([]);
item.taxes.split(",").every((tax_code, taxIndex) => {
const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);

View File

@ -1,7 +1,8 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceApplicationService, CustomerInvoiceNumber } from "../../domain";
import { CustomerInvoiceNumber } from "../../domain";
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
import { StatusInvoiceIsApprovedSpecification } from "../specs";
type IssueCustomerInvoiceUseCaseInput = {
@ -56,12 +57,14 @@ export class IssueCustomerInvoiceUseCase {
const issuedInvoiceResult = invoiceProforma.issueInvoice(newInvoiceNumber);
if (issuedInvoiceResult.isFailure) {
return Result.fail(new EntityNotFoundError("Customer invoice", "id", error));
return Result.fail(
new EntityNotFoundError("Customer invoice", "id", issuedInvoiceResult.error)
);
}
const issuedInvoice = issuedInvoiceResult.data;
this.service.saveInvoice(issuedInvoice, transaction);
this.service.updateInvoiceInCompany(companyId, issuedInvoice, transaction);
//return await this.service.IssueInvoiceByIdInCompany(companyId, invoiceId, transaction);
} catch (error: unknown) {

View File

@ -1,7 +1,7 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceApplicationService } from "../../../domain";
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
import { CustomerInvoiceReportPDFPresenter } from "./reporter";
type ReportCustomerInvoiceUseCaseInput = {

View File

@ -1,7 +1,7 @@
export * from "./create-customer-invoice.controller";
export * from "./delete-customer-invoice.controller";
//export * from "./delete-customer-invoice.controller";
export * from "./get-customer-invoice.controller";
export * from "./issue-customer-invoice.controller";
//export * from "./issue-customer-invoice.controller";
export * from "./list-customer-invoices.controller";
export * from "./report-customer-invoice.controller";
export * from "./update-customer-invoice.controller";

View File

@ -1,18 +1,18 @@
import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
extractOrPushError,
LanguageCode,
maybeFromNullableVO,
Percentage,
TextValue,
toNullable,
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd";
import { Collection, Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { Collection, isNullishOrEmpty, Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoice,
CustomerInvoiceItems,
@ -315,8 +315,7 @@ export class CustomerInvoiceDomainMapper
const items = itemsResult.data;
// 2) Taxes
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.taxes), {
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), {
errors,
parent: source,
...params,
@ -407,7 +406,7 @@ export class CustomerInvoiceDomainMapper
};
const hasRecipient = source.hasRecipient;
const recipient = source.recipient!.getOrUndefined();
const recipient = source.recipient?.getOrUndefined();
if (!source.isProforma && !hasRecipient) {
errors.push({
@ -417,25 +416,25 @@ export class CustomerInvoiceDomainMapper
}
const recipientValues = {
customer_tin: !source.isProforma ? recipient!.tin.toPrimitive() : null,
customer_name: !source.isProforma ? recipient!.name.toPrimitive() : null,
customer_tin: !source.isProforma ? recipient?.tin.toPrimitive() : null,
customer_name: !source.isProforma ? recipient?.name.toPrimitive() : null,
customer_street: !source.isProforma
? toNullable(recipient!.street, (v) => v.toPrimitive())
? toNullable(recipient?.street, (v) => v.toPrimitive())
: null,
customer_street2: !source.isProforma
? toNullable(recipient!.street2, (v) => v.toPrimitive())
? toNullable(recipient?.street2, (v) => v.toPrimitive())
: null,
customer_city: !source.isProforma
? toNullable(recipient!.city, (v) => v.toPrimitive())
? toNullable(recipient?.city, (v) => v.toPrimitive())
: null,
customer_province: !source.isProforma
? toNullable(recipient!.province, (v) => v.toPrimitive())
? toNullable(recipient?.province, (v) => v.toPrimitive())
: null,
customer_postal_code: !source.isProforma
? toNullable(recipient!.postalCode, (v) => v.toPrimitive())
? toNullable(recipient?.postalCode, (v) => v.toPrimitive())
: null,
customer_country: !source.isProforma
? toNullable(recipient!.country, (v) => v.toPrimitive())
? toNullable(recipient?.country, (v) => v.toPrimitive())
: null,
};

View File

@ -1,4 +1,3 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import {
ISequelizeDomainMapper,
MapperParamsType,
@ -6,6 +5,8 @@ import {
Tax,
} from "@erp/core/api";
import { JsonTaxCatalogProvider } from "@erp/core";
import {
UniqueID,
ValidationErrorCollection,

View File

@ -1,18 +1,14 @@
import { ISequelizeQueryMapper, MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import {
CurrencyCode,
extractOrPushError,
LanguageCode,
maybeFromNullableVO,
Percentage,
UniqueID,
UtcDate,
maybeFromNullableVO,
ValidationErrorCollection,
ValidationErrorDetail,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";

View File

@ -22,6 +22,7 @@ export const CreateCustomerInvoiceRequestSchema = z.object({
customer_id: z.uuid(),
reference: z.string().default(""),
notes: z.string().default(""),
language_code: z.string().toLowerCase().default("es"),
@ -32,6 +33,8 @@ export const CreateCustomerInvoiceRequestSchema = z.object({
scale: "2",
}),
payment_method: z.string().default(""),
items: z.array(CreateCustomerInvoiceItemRequestSchema).default([]),
});

View File

@ -5,6 +5,7 @@
"types": "src/index.ts",
"exports": {
".": "./src/common/index.ts",
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts",
"./globals.css": "./src/web/globals.css",

View File

@ -1,10 +1,10 @@
import { CriteriaDTO } from "@erp/core";
import { Presenter } from "@erp/core/api";
import { CustomerListDTO } from "@erp/customer-invoices/api/infrastructure";
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { ListCustomersResponseDTO } from "../../../../common/dto";
import { CustomerListDTO } from "../../../infrastructure/mappers";
export class ListCustomersPresenter extends Presenter {
protected _mapCustomer(customer: CustomerListDTO) {

View File

@ -1,4 +1,5 @@
import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd";
import { Transaction } from "sequelize";
import { CustomerApplicationService } from "../../application";
import { logger } from "../../helpers";
@ -6,7 +7,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat
constructor(
private readonly service: CustomerApplicationService,
private readonly companyId: UniqueID,
private readonly transaction?: unknown
private readonly transaction?: Transaction
) {
super();
}

View File

@ -1,5 +1,3 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@repo/rdx-ddd";
import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
City,
@ -18,6 +16,8 @@ import {
TextValue,
URLAddress,
UniqueID,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
toNullable,

View File

@ -28,10 +28,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src",
"../core/src/web/components/form/form-debug.tsx",
"../customer-invoices/src/web/components/editor/invoice-basic-info-fields.tsx"
],
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -12,7 +12,9 @@
"express": "^4.18.2",
"zod": "^4.1.11"
},
"devDependencies": { "@types/express": "^4.17.21" },
"devDependencies": {
"@types/express": "^4.17.21"
},
"dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",

View File

@ -1,5 +1,5 @@
import { Presenter } from "@erp/core/api";
import { GetVerifactuRecordByIdResponseDTO } from "@erp/verifactu-records/common";
import { GetVerifactuRecordByIdResponseDTO } from "../../../../common";
import { VerifactuRecord } from "../../../domain";
export class VerifactuRecordFullPresenter extends Presenter<

View File

@ -54,7 +54,7 @@ export class SendInvoiceUseCase {
],
importe_total: "352.00",
};
const invoiceOrError = await this.service.sendInvoiceToVerifactu(invoice, transaction);
const invoiceOrError = await this.service.sendInvoiceToVerifactu(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}

View File

@ -1,7 +1,11 @@
{
"name": "uecko-erp-2025",
"private": true,
"workspaces": ["apps/*", "modules/*", "packages/*"],
"workspaces": [
"apps/*",
"modules/*",
"packages/*"
],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
@ -15,7 +19,7 @@
"clean": "find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \\;"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@biomejs/biome": "2.0.6",
"@repo/typescript-config": "workspace:*",
"change-case": "^5.4.4",
"inquirer": "^12.5.2",

View File

@ -10,9 +10,6 @@ export class DomainValidationError extends DomainError {
/** Discriminante para routing/telemetría */
public readonly kind = "VALIDATION" as const;
/** Regla/identificador de error de validación (ej. INVALID_EMAIL) */
public readonly code: string;
/** Campo afectado (path) */
public readonly field: string;
@ -20,13 +17,13 @@ export class DomainValidationError extends DomainError {
public readonly detail: string;
constructor(
code: string,
_code: string,
field: string,
detail: string,
options?: ErrorOptions & { metadata?: Record<string, unknown> }
) {
// Mensaje humano compacto y útil para logs
super(`[${field}] ${detail}`, code, {
super(`[${field}] ${detail}`, {
...options,
// Aseguramos metadatos ricos y estables
metadata: {
@ -38,7 +35,7 @@ export class DomainValidationError extends DomainError {
});
this.name = "DomainValidationError";
this.code = code;
//this.code = code;
this.field = field;
this.detail = detail;

View File

@ -15,7 +15,7 @@
*
*/
import { DomainError } from "./domain-error";
import { BaseError } from "./base-error";
import { DomainValidationError } from "./domain-validation-error";
export interface ValidationErrorDetail {
@ -38,7 +38,9 @@ export interface ValidationErrorDetail {
* ];
* throw new ValidationErrorCollection(message, errors);
*/
export class ValidationErrorCollection extends DomainError {
export class ValidationErrorCollection extends BaseError<"domain"> {
public readonly layer = "domain" as const;
public readonly kind = "VALIDATION" as const;
public readonly code = "MULTIPLE_VALIDATION_ERRORS" as const;
public readonly details: ValidationErrorDetail[];
@ -48,7 +50,7 @@ export class ValidationErrorCollection extends DomainError {
details: ValidationErrorDetail[],
options?: ErrorOptions & { metadata?: Record<string, unknown> }
) {
super(message, "MULTIPLE_VALIDATION_ERRORS", {
super("DomainError", message, "MULTIPLE_VALIDATION_ERRORS", {
...options,
metadata: { ...(options?.metadata ?? {}), errors: details },
});

View File

@ -69,7 +69,7 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
DineroFactory({
amount,
precision: scale || MoneyValue.DEFAULT_SCALE,
currency: (currency_code as Currency) || MoneyValue.DEFAULT_CURRENCY_CODE,
currency: (currency_code || MoneyValue.DEFAULT_CURRENCY_CODE) as Currency,
})
); // 🔒 Garantiza inmutabilidad
}

View File

@ -3,6 +3,6 @@
"compilerOptions": {
"composite": true
},
"include": ["src", "../../modules/core/src/web/lib/helpers/money-funcs.ts"],
"include": ["src"],
"exclude": ["src/**/__tests__/*"]
}

View File

@ -3,6 +3,6 @@
"compilerOptions": {
"composite": true
},
"include": ["src", "../../modules/core/src/web/lib/helpers/money-funcs.ts"],
"include": ["src"],
"exclude": ["src/**/__tests__/*"]
}

View File

@ -7,6 +7,7 @@
"paths": {
"@erp/core/*": ["modules/core/src/*"],
"@erp/auth/*": ["modules/auth/src/*"],
"@erp/customers/*": ["modules/customers/src/*"],
"@erp/customer-invoices/*": ["modules/customer-invoices/src/*"]
},

View File

@ -9,8 +9,8 @@ importers:
.:
devDependencies:
'@biomejs/biome':
specifier: 1.9.4
version: 1.9.4
specifier: 2.0.6
version: 2.0.6
'@repo/typescript-config':
specifier: workspace:*
version: link:packages/typescript-config
@ -359,9 +359,6 @@ importers:
'@types/react-dom':
specifier: ^19.1.3
version: 19.2.1(@types/react@19.2.2)
'@types/react-i18next':
specifier: ^8.1.0
version: 8.1.0(i18next@25.6.0(typescript@5.8.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.8.3)
typescript:
specifier: ^5.8.3
version: 5.8.3
@ -377,6 +374,9 @@ importers:
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/rdx-logger':
specifier: workspace:*
version: link:../../packages/rdx-logger
'@repo/rdx-ui':
specifier: workspace:*
version: link:../../packages/rdx-ui
@ -432,9 +432,6 @@ importers:
'@hookform/devtools':
specifier: ^4.4.0
version: 4.4.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@types/axios':
specifier: ^0.14.4
version: 0.14.4
'@types/dinero.js':
specifier: ^1.9.4
version: 1.9.4
@ -571,9 +568,6 @@ importers:
'@types/react-dom':
specifier: ^19.1.3
version: 19.2.1(@types/react@19.2.2)
'@types/react-i18next':
specifier: ^8.1.0
version: 8.1.0(i18next@25.6.0(typescript@5.8.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.8.3)
'@types/react-router-dom':
specifier: ^5.3.3
version: 5.3.3
@ -1246,54 +1240,107 @@ packages:
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/biome@2.0.6':
resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-arm64@2.0.6':
resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.0.6':
resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64-musl@2.0.6':
resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.0.6':
resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.0.6':
resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.0.6':
resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-arm64@2.0.6':
resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@biomejs/cli-win32-x64@2.0.6':
resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@codelytv/criteria@2.0.0':
resolution: {integrity: sha512-EgXNABlN3vBsfIplqsEMcUX836SzoTeHoE4m8H/9pujiAw/vuDam27x+AnwpJmW2XHkzMk39G9wsejopFN4kTQ==}
@ -2778,10 +2825,6 @@ packages:
resolution: {integrity: sha512-EE/27azLteK24It0B0IrjA7yWFC6jYZoTTUzL7R7HgiN0BWBPrTp6Ugpn0iE6+Bn9fFcjSp/IBBG8D8c7vXD1g==}
hasBin: true
'@types/axios@0.14.4':
resolution: {integrity: sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==}
deprecated: This is a stub types definition. axios provides its own type definitions, so you do not need this installed.
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -6541,30 +6584,65 @@ snapshots:
'@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 1.9.4
'@biomejs/biome@2.0.6':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.0.6
'@biomejs/cli-darwin-x64': 2.0.6
'@biomejs/cli-linux-arm64': 2.0.6
'@biomejs/cli-linux-arm64-musl': 2.0.6
'@biomejs/cli-linux-x64': 2.0.6
'@biomejs/cli-linux-x64-musl': 2.0.6
'@biomejs/cli-win32-arm64': 2.0.6
'@biomejs/cli-win32-x64': 2.0.6
'@biomejs/cli-darwin-arm64@1.9.4':
optional: true
'@biomejs/cli-darwin-arm64@2.0.6':
optional: true
'@biomejs/cli-darwin-x64@1.9.4':
optional: true
'@biomejs/cli-darwin-x64@2.0.6':
optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-arm64-musl@2.0.6':
optional: true
'@biomejs/cli-linux-arm64@1.9.4':
optional: true
'@biomejs/cli-linux-arm64@2.0.6':
optional: true
'@biomejs/cli-linux-x64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-x64-musl@2.0.6':
optional: true
'@biomejs/cli-linux-x64@1.9.4':
optional: true
'@biomejs/cli-linux-x64@2.0.6':
optional: true
'@biomejs/cli-win32-arm64@1.9.4':
optional: true
'@biomejs/cli-win32-arm64@2.0.6':
optional: true
'@biomejs/cli-win32-x64@1.9.4':
optional: true
'@biomejs/cli-win32-x64@2.0.6':
optional: true
'@codelytv/criteria@2.0.0': {}
'@colors/colors@1.6.0': {}
@ -8026,12 +8104,6 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@types/axios@0.14.4':
dependencies:
axios: 1.12.2
transitivePeerDependencies:
- debug
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.4

View File

@ -2,12 +2,15 @@ packages:
- packages/*
- modules/*
- apps/*
ignoredBuiltDependencies:
- esbuild
onlyBuiltDependencies:
- '@biomejs/biome'
- '@parcel/watcher'
- '@tailwindcss/oxide'
- bcrypt
- core-js-pure
- puppeteer
- sharp