Compare commits

..

No commits in common. "5f08cfaa15166376a91bdb8c1f69b9440db01c02" and "4d76919e44681cb3c2fee054b5f801a54a2e253e" have entirely different histories.

33 changed files with 158 additions and 577 deletions

View File

@ -1,5 +1,5 @@
import type { IPresenter, IPresenterOutputParams } from "./presenter.interface"; import { IPresenterRegistry } from "./presenter-registry.interface";
import type { IPresenterRegistry } from "./presenter-registry.interface"; import { IPresenter, IPresenterOutputParams } from "./presenter.interface";
export abstract class Presenter<TSource = unknown, TOutput = unknown> export abstract class Presenter<TSource = unknown, TOutput = unknown>
implements IPresenter<TSource, TOutput> implements IPresenter<TSource, TOutput>

View File

@ -1,9 +1,8 @@
// --- Adaptador que carga el catálogo JSON en memoria e indexa por code --- // --- Adaptador que carga el catálogo JSON en memoria e indexa por code ---
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { TaxCatalogProvider } from "./tax-catalog.provider";
import type { TaxCatalogProvider } from "./tax-catalog.provider"; import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
import type { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
export class JsonTaxCatalogProvider implements TaxCatalogProvider { export class JsonTaxCatalogProvider implements TaxCatalogProvider {
// Índice por código normalizado // Índice por código normalizado

View File

@ -1,6 +1,7 @@
import type { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe // --- Puerto (interfaz) para resolver tasas desde un catálogo ---
import type { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types"; import { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe
import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
export interface TaxCatalogProvider { export interface TaxCatalogProvider {
/** /**

View File

@ -1,47 +0,0 @@
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
import { type FormatMoneyOptions, formatMoneyDTO } from "../../helpers";
type CustomerInvoiceTaxesDTO = GetIssueInvoiceByIdResponseDTO["taxes"];
type CustomerInvoiceTaxDTO = ArrayElement<CustomerInvoiceTaxesDTO>;
export class CustomerInvoiceTaxesReportPresenter extends Presenter<
CustomerInvoiceTaxesDTO,
unknown
> {
private _locale!: string;
private _taxCatalog!: JsonTaxCatalogProvider;
private _mapTax(taxItem: CustomerInvoiceTaxDTO) {
const moneyOptions: FormatMoneyOptions = {
locale: this._locale,
hideZeros: true,
newScale: 2,
};
const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code);
return {
tax_code: taxItem.tax_code,
tax_name: taxCatalogItem.unwrap().name,
taxable_amount: formatMoneyDTO(taxItem.taxable_amount, moneyOptions),
taxes_amount: formatMoneyDTO(taxItem.taxes_amount, moneyOptions),
};
}
toOutput(taxes: CustomerInvoiceTaxesDTO, params: IPresenterOutputParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
this._taxCatalog = SpainTaxCatalogProvider();
return taxes.map((item, _index) => {
return this._mapTax(item);
});
}
}

View File

@ -20,21 +20,11 @@ export class CustomerInvoiceReportPresenter extends Presenter<
format: "JSON", format: "JSON",
}); });
const taxesPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-taxes",
projection: "REPORT",
format: "JSON",
});
const locale = invoiceDTO.language_code; const locale = invoiceDTO.language_code;
const itemsDTO = itemsPresenter.toOutput(invoiceDTO.items, { const itemsDTO = itemsPresenter.toOutput(invoiceDTO.items, {
locale, locale,
}); });
const taxesDTO = taxesPresenter.toOutput(invoiceDTO.taxes, {
locale,
});
const moneyOptions: FormatMoneyOptions = { const moneyOptions: FormatMoneyOptions = {
locale, locale,
hideZeros: true, hideZeros: true,
@ -43,7 +33,6 @@ export class CustomerInvoiceReportPresenter extends Presenter<
return { return {
...invoiceDTO, ...invoiceDTO,
taxes: taxesDTO,
items: itemsDTO, items: itemsDTO,
invoice_date: formatDateDTO(invoiceDTO.invoice_date), invoice_date: formatDateDTO(invoiceDTO.invoice_date),

View File

@ -1,4 +1,3 @@
export * from "./customer-invoice.report.presenter";
export * from "./customer-invoice-items.report.presenter"; export * from "./customer-invoice-items.report.presenter";
export * from "./customer-invoice-taxes.report.presenter"; export * from "./customer-invoice.report.presenter";
export * from "./list-customer-invoices.presenter"; export * from "./list-customer-invoices.presenter";

View File

@ -3,14 +3,11 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Maybe, Result } from "@repo/rdx-utils"; import { type Collection, Maybe, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { import type {
CustomerInvoiceIsProformaSpecification, CustomerInvoiceNumber,
type CustomerInvoiceNumber, CustomerInvoiceSerie,
type CustomerInvoiceSerie, CustomerInvoiceStatus,
type CustomerInvoiceStatus, ICustomerInvoiceNumberGenerator,
type ICustomerInvoiceNumberGenerator,
ProformaCannotBeDeletedError,
StatusInvoiceIsDraftSpecification,
} from "../../domain"; } from "../../domain";
import { import {
CustomerInvoice, CustomerInvoice,
@ -72,27 +69,6 @@ export class CustomerInvoiceApplicationService {
return CustomerInvoice.create({ ...props, companyId }, proformaId); return CustomerInvoice.create({ ...props, companyId }, proformaId);
} }
/**
* Guarda una nueva factura y devuelve la factura guardada.
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param proforma - La factura a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - La factura guardada o un error si falla la operación.
*/
async createIssueInvoiceInCompany(
companyId: UniqueID,
invoice: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(invoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
return this.getIssueInvoiceByIdInCompany(companyId, invoice.id, transaction);
}
/** /**
* Guarda una nueva proforma y devuelve la proforma guardada. * Guarda una nueva proforma y devuelve la proforma guardada.
* *
@ -101,7 +77,7 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - La proforma guardada o un error si falla la operación. * @returns Result<CustomerInvoice, Error> - La proforma guardada o un error si falla la operación.
*/ */
async createProformaInCompany( async createInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
proforma: CustomerInvoice, proforma: CustomerInvoice,
transaction: Transaction transaction: Transaction
@ -137,27 +113,7 @@ export class CustomerInvoiceApplicationService {
/** /**
* *
* Comprueba si existe o no en persistencia una proforma con el ID proporcionado * Comprueba si existe o no en persistencia una factura con el ID proporcionado
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param proformaId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe la factura o no.
*/
async existsProformaByIdInCompany(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, proformaId, transaction, {
is_proforma: true,
});
}
/**
*
* Comprueba si existe o no en persistencia una proforma con el ID proporcionado
* *
* @param companyId - Identificador de la empresa a la que pertenece la factura. * @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param invoiceId - Identificador UUID de la factura. * @param invoiceId - Identificador UUID de la factura.
@ -165,14 +121,12 @@ export class CustomerInvoiceApplicationService {
* @returns Result<Boolean, Error> - Existe la factura o no. * @returns Result<Boolean, Error> - Existe la factura o no.
*/ */
async existsIssueInvoiceByIdInCompany( async existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, { return this.repository.existsByIdInCompany(companyId, invoiceId, transaction);
is_proforma: false,
});
} }
/** /**
@ -245,13 +199,13 @@ export class CustomerInvoiceApplicationService {
changes: CustomerInvoicePatchProps, changes: CustomerInvoicePatchProps,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); const invoiceResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
if (proformaResult.isFailure) { if (invoiceResult.isFailure) {
return Result.fail(proformaResult.error); return Result.fail(invoiceResult.error);
} }
const updated = proformaResult.data.update(changes); const updated = invoiceResult.data.update(changes);
if (updated.isFailure) { if (updated.isFailure) {
return Result.fail(updated.error); return Result.fail(updated.error);
@ -261,48 +215,27 @@ export class CustomerInvoiceApplicationService {
} }
/** /**
* Elimina (o marca como eliminada) una proforma según su ID. * Elimina (o marca como eliminada) una factura según su ID.
* *
* @param companyId - Identificador de la empresa a la que pertenece la proforma. * @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param proformaId - Identificador UUID de la proforma. * @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación. * @returns Result<boolean, Error> - Resultado de la operación.
*/ */
async deleteProformaByIdInCompany( async deleteInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
// 1) Buscar la proforma return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
const proforma = proformaResult.data;
// 2) Validar: es proforma
const isProforma = new CustomerInvoiceIsProformaSpecification();
if (!(await isProforma.isSatisfiedBy(proforma))) {
return Result.fail(new ProformaCannotBeDeletedError(proformaId.toString(), "not a proforma"));
}
// 3) Validar: estado draft
const isDraft = new StatusInvoiceIsDraftSpecification();
if (!(await isDraft.isSatisfiedBy(proforma))) {
return Result.fail(
new ProformaCannotBeDeletedError(proformaId.toString(), "status is not 'draft'")
);
}
// 4) Borrar la proforma (baja lógica)
return this.repository.deleteProformaByIdInCompany(companyId, proformaId, transaction);
} }
/** /**
* *
* Actualiza el "status" de una proforma * Actualiza el "status" de una proforma
* *
* @param companyId - Identificador UUID de la empresa a la que pertenece la proforma. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param proformaId - UUID de la proforma a actualizar. * @param proformaId - UUID de la factura a eliminar.
* @param newStatus - nuevo estado * @param newStatus - nuevo estado
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> * @returns Result<boolean, Error>

View File

@ -37,7 +37,7 @@ export class ChangeStatusProformaUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
/** 1. Recuperamos la proforma */ /** 1. Recuperamos la proforma */
const proformaResult = await this.service.getProformaByIdInCompany( const proformaResult = await this.service.getInvoiceByIdInCompany(
companyId, companyId,
proformaId, proformaId,
transaction transaction

View File

@ -1,4 +1,4 @@
import type { ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, type ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
@ -19,15 +19,32 @@ export class DeleteProformaUseCase {
const { proforma_id, companyId } = params; const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id); const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const proformaId = idOrError.data; const invoiceId = idOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
return await this.service.deleteProformaByIdInCompany(companyId, proformaId, transaction); const existsCheck = await this.service.existsByIdInCompany(
companyId,
invoiceId,
transaction
);
if (existsCheck.isFailure) {
return Result.fail(existsCheck.error);
}
const invoiceExists = existsCheck.data;
if (!invoiceExists) {
return Result.fail(new EntityNotFoundError("Proforma", "id", invoiceId.toString()));
}
return await this.service.deleteInvoiceByIdInCompany(companyId, invoiceId, transaction);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -50,7 +50,7 @@ export class IssueProformaInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
/** 1. Recuperamos la proforma */ /** 1. Recuperamos la proforma */
const proformaResult = await this.service.getProformaByIdInCompany( const proformaResult = await this.service.getInvoiceByIdInCompany(
companyId, companyId,
proformaId, proformaId,
transaction transaction
@ -74,7 +74,7 @@ export class IssueProformaInvoiceUseCase {
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error); if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
/** 5. Guardar la nueva factura */ /** 5. Guardar la nueva factura */
const saveInvoiceResult = await this.service.createIssueInvoiceInCompany( const saveInvoiceResult = await this.service.createInvoiceInCompany(
companyId, companyId,
issuedInvoiceOrError.data, issuedInvoiceOrError.data,
transaction transaction

View File

@ -8,7 +8,7 @@
<title>Factura F26200</title> <title>Factura F26200</title>
<style> <style>
body { body {
font-family: Tahoma, sans-serif; font-family: Arial, sans-serif;
margin: 40px; margin: 40px;
color: #333; color: #333;
font-size: 11pt; font-size: 11pt;
@ -55,9 +55,7 @@
table th, table th,
table td { table td {
border-top: 1px solid; border: 0px solid;
border-left: 1px solid;
border-bottom: 0px solid;
padding: 3px 10px; padding: 3px 10px;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
@ -116,24 +114,13 @@
<aside class="flex items-start mb-4 w-full"> <aside class="flex items-start mb-4 w-full">
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda --> <!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
<div class="w-[70%] flex flex-col items-start text-left"> <div class="w-[70%] flex flex-col items-start text-left">
<img src="https://rodax-software.com/images/logo_acana.jpg" alt="Logo Acana" class="block h-24 w-auto mb-1" /> <img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
<div class="p-3 not-italic text-xs leading-tight" style="font-size: 8pt;">
<p>Aliso Design S.L. B86913910</p>
<p>C/ La Fundición, 27. Pol. Santa Ana</p>
<p>Rivas Vaciamadrid 28522 Madrid</p>
<p>Telf: 91 301 65 57 / 91 301 65 58</p>
<p><a href="mailto:info@acanainteriorismo.com"
class="hover:underline">info@acanainteriorismo.com</a>&nbsp;-&nbsp;<a
href="https://www.acanainteriorismo.com" target="_blank" rel="noopener"
class="hover:underline">www.acanainteriorismo.com</a></p>
</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="p-3 "> <div class="p-1 ">
<p>Factura nº:<strong>&nbsp;{{series}}{{invoice_number}}</strong></p> <p>Factura nº:<strong>&nbsp;{{series}}{{invoice_number}}</strong></p>
<p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p> <p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p>
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
</div> </div>
<div class="p-3 ml-9"> <div class="p-1 ml-9">
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2> <h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
<p>{{recipient.tin}}</p> <p>{{recipient.tin}}</p>
<p>{{recipient.street}}</p> <p>{{recipient.street}}</p>
@ -144,8 +131,14 @@
<!-- Bloque DERECHO: logo2 arriba y texto DEBAJO --> <!-- Bloque DERECHO: logo2 arriba y texto DEBAJO -->
<div class="ml-auto flex flex-col items-end text-right"> <div class="ml-auto flex flex-col items-end text-right">
<img src="https://rodax-software.com/images/factura_acana.jpg" alt="Factura" <img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario"
class="block h-14 w-auto md:h-8 mb-1" /> class="block h-5 w-auto md:h-8 mb-1" />
<div class="not-italic text-xs leading-tight">
<p>Telf: 91 785 02 47 / 686 62 10 59</p>
<p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p>
<p><a href="https://www.rodax-software.com" target="_blank" rel="noopener"
class="hover:underline">www.rodax-software.com</a></p>
</div>
</div> </div>
</aside> </aside>
</header> </header>
@ -153,15 +146,25 @@
<main id="main"> <main id="main">
<section id="details" class="border-b border-black "> <section id="details" class="border-b border-black ">
<div class="relative pt-0 border-b border-black">
<!-- Badge TOTAL decorado con imagen -->
<div class="absolute -top-9 right-0">
<div class="relative text-sm font-semibold text-black pr-2 pl-10 py-2 justify-center bg-red-900"
style="background-image: url('https://rodax-software.com/images/img-total2.jpg'); background-size: cover; background-position: left;">
<!-- Texto del total -->
<span>TOTAL: {{total_amount}}</span>
</div>
</div>
</div>
<!-- Tu tabla --> <!-- Tu tabla -->
<table class="table-header"> <table class="table-header">
<thead> <thead>
<tr class="text-left bg-gray-200 text-red-500"> <tr class="text-left">
<th class="py-2">Concepto</th> <th class="py-2">Concepto</th>
<th class="py-2">Cantidad</th> <th class="py-2">Cantidad</th>
<th class="py-2">Precio&nbsp;unidad</th> <th class="py-2">Precio&nbsp;unidad</th>
<th class="py-2">Dto</th>
<th class="py-2">Importe&nbsp;total</th> <th class="py-2">Importe&nbsp;total</th>
</tr> </tr>
</thead> </thead>
@ -222,12 +225,21 @@
<td class="w-5">&nbsp;</td> <td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{taxable_amount}}</td> <td class="px-4 text-right">{{taxable_amount}}</td>
</tr> </tr>
{{#each taxes}} {{#if taxes_amount }}
<tr> <tr>
<td class="px-4 text-right">{{tax_name}}</td> <td class="px-4 text-right">IVA&nbsp;21%</td>
<td class="w-5">&nbsp;</td> <td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{taxes_amount}}</td> <td class="px-4 text-right">{{taxes_amount}}</td>
</tr> </tr>
{{else}}
<!-- iva 0-->
{{/if}}
{{#each taxes}}
<tr>
<td class="px-4 text-right">IVA&nbsp;{{tax_code}}%</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{taxes_amount.value}}</td>
</tr>
{{/each}} {{/each}}
<tr class=""> <tr class="">
<td class="px-4 text-right accent-color"> <td class="px-4 text-right accent-color">

View File

@ -1,260 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.css"
referrerpolicy="no-referrer" />
<title>Factura F26200</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
color: #333;
font-size: 11pt;
line-height: 1.6;
}
header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
margin-bottom: 0;
padding-bottom: 0;
}
.accent-color {
background-color: #F08119;
}
.company-info,
.invoice-meta {
width: 48%;
}
.invoice-meta {
text-align: right;
}
h1 {
font-size: 20px;
margin-bottom: 5px;
}
.contact {
font-size: 14px;
margin-top: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0px;
margin-bottom: 15px;
}
table th,
table td {
border: 0px solid;
padding: 3px 10px;
text-align: left;
vertical-align: top;
}
table th {
margin-bottom: 10px;
}
.totals {
margin-top: 20px;
width: 100%;
}
.totals td {
padding: 5px 10px;
}
.totals td.label {
text-align: right;
font-weight: bold;
}
footer {
margin-top: 40px;
font-size: 10px;
}
.highlight {
background-color: #eef;
}
.accent-color {
background-color: #F08119;
}
@media print {
* {
-webkit-print-color-adjust: exact;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}
</style>
</head>
<body>
<header>
<aside class="flex items-start mb-4 w-full">
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
<div class="w-[70%] flex flex-col items-start text-left">
<img src="https://rodax-software.com/images/logo_rodax.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
<div class="flex w-full">
<div class="p-1 ">
<p>Factura nº:<strong>&nbsp;{{series}}{{invoice_number}}</strong></p>
<p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p>
</div>
<div class="p-1 ml-9">
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
<p>{{recipient.tin}}</p>
<p>{{recipient.street}}</p>
<p>{{recipient.postal_code}}&nbsp;&nbsp;{{recipient.city}}&nbsp;&nbsp;{{recipient.province}}</p>
</div>
</div>
</div>
<!-- Bloque DERECHO: logo2 arriba y texto DEBAJO -->
<div class="ml-auto flex flex-col items-end text-right">
<img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario"
class="block h-5 w-auto md:h-8 mb-1" />
<div class="not-italic text-xs leading-tight">
<p>Telf: 91 785 02 47 / 686 62 10 59</p>
<p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p>
<p><a href="https://www.rodax-software.com" target="_blank" rel="noopener"
class="hover:underline">www.rodax-software.com</a></p>
</div>
</div>
</aside>
</header>
<main id="main">
<section id="details" class="border-b border-black ">
<div class="relative pt-0 border-b border-black">
<!-- Badge TOTAL decorado con imagen -->
<div class="absolute -top-9 right-0">
<div class="relative text-sm font-semibold text-black pr-2 pl-10 py-2 justify-center bg-red-900"
style="background-image: url('https://rodax-software.com/images/img-total2.jpg'); background-size: cover; background-position: left;">
<!-- Texto del total -->
<span>TOTAL: {{total_amount}}</span>
</div>
</div>
</div>
<!-- Tu tabla -->
<table class="table-header">
<thead>
<tr class="text-left">
<th class="py-2">Concepto</th>
<th class="py-2">Cantidad</th>
<th class="py-2">Precio&nbsp;unidad</th>
<th class="py-2">Importe&nbsp;total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if taxable_amount}}{{taxable_amount}}{{else}}&nbsp;{{/if}}</td>
</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
<section id="resume" class="flex items-center justify-between pb-4 mb-4">
<div class="grow relative pt-10 self-start">
{{#if payment_method}}
<div class="">
<p class=" text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
</div>
{{else}}
<!-- Empty payment method-->
{{/if}}
{{#if notes}}
<div class="pt-4">
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
</div>
{{else}}
<!-- Empty notes-->
{{/if}}
</div>
<div class="relative pt-10 grow">
<table class=" table-header min-w-full bg-transparent">
<tbody>
{{#if discount_percentage}}
<tr>
<td class="px-4 text-right">Importe&nbsp;neto</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{subtotal_amount}}</td>
</tr>
<tr>
<td class="px-4 text-right">Descuento&nbsp;{{discount_percentage}}</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{discount_amount.value}}</td>
</tr>
{{else}}
<!-- dto 0-->
{{/if}}
<tr>
<td class="px-4 text-right">Base&nbsp;imponible</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{taxable_amount}}</td>
</tr>
{{#each taxes}}
<tr>
<td class="px-4 text-right">{{tax_name}}</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right">{{taxes_amount}}</td>
</tr>
{{/each}}
<tr class="">
<td class="px-4 text-right accent-color">
Total&nbsp;factura
</td>
<td class="w-5">&nbsp;</td>
<td class="px-4 text-right accent-color">
{{total_amount}}</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<footer id="footer" class="mt-4">
<aside>
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
| CIF: B83999441 -
Rodax Software S.L.</p>
</aside>
</footer>
</body>
</html>

View File

@ -1 +1 @@
export * from "./update-proforma.use-case"; export * from "./update-customer-invoice.use-case";

View File

@ -10,20 +10,20 @@ import type { CustomerInvoiceApplicationService } from "../../../services/custom
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
type UpdateProformaUseCaseInput = { type UpdateCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
proforma_id: string; proforma_id: string;
dto: UpdateProformaByIdRequestDTO; dto: UpdateProformaByIdRequestDTO;
}; };
export class UpdateProformaUseCase { export class UpdateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: UpdateProformaUseCaseInput) { public execute(params: UpdateCustomerInvoiceUseCaseInput) {
const { companyId, proforma_id, dto } = params; const { companyId, proforma_id, dto } = params;
const idOrError = UniqueID.create(proforma_id); const idOrError = UniqueID.create(proforma_id);

View File

@ -2,4 +2,3 @@ export * from "./customer-invoice-id-already-exits-error";
export * from "./entity-is-not-proforma-error"; export * from "./entity-is-not-proforma-error";
export * from "./invalid-proforma-transition-error"; export * from "./invalid-proforma-transition-error";
export * from "./proforma-cannot-be-converted-to-invoice-error"; export * from "./proforma-cannot-be-converted-to-invoice-error";
export * from "./proforma-cannot-be-deleted-error";

View File

@ -1,12 +0,0 @@
// domain/errors/proforma-cannot-be-deleted.error.ts
import { DomainError } from "@repo/rdx-ddd";
/** Proforma solo se puede borrar si está en 'draft' */
export class ProformaCannotBeDeletedError extends DomainError {
constructor(id: string, reason?: string, options?: ErrorOptions) {
super(`Proforma '${id}' cannot be deleted${reason ? `: ${reason}` : ""}.`, options);
this.name = "ProformaCannotBeDeletedError";
}
}
export const isProformaCannotBeDeletedError = (e: unknown): e is ProformaCannotBeDeletedError =>
e instanceof ProformaCannotBeDeletedError;

View File

@ -36,8 +36,7 @@ export interface ICustomerInvoiceRepository {
existsByIdInCompany( existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: unknown, transaction?: unknown
options: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
/** /**
@ -72,14 +71,14 @@ export interface ICustomerInvoiceRepository {
/** /**
* *
* Elimina o marca como eliminada una proforma dentro de una empresa. * Elimina o marca como eliminada una factura dentro de una empresa.
* *
* @param companyId - ID de la empresa. * @param companyId - ID de la empresa.
* @param id - UUID de la proforma a eliminar. * @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<void, Error>
*/ */
deleteProformaByIdInCompany( deleteByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: unknown transaction: unknown

View File

@ -1,13 +1,12 @@
import type { UtcDate } from "@repo/rdx-ddd"; import { UtcDate } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates"; import { CustomerInvoice } from "../aggregates";
import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors"; import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors";
import { import {
CustomerInvoiceIsProformaSpecification, CustomerInvoiceIsProformaSpecification,
ProformaCanTranstionToIssuedSpecification, ProformaCanTranstionToIssuedSpecification,
} from "../specs"; } from "../specs";
import { type CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects"; import { CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects";
/** /**
* Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma.
@ -16,6 +15,11 @@ export class IssueCustomerInvoiceDomainService {
private readonly isProformaSpec = new CustomerInvoiceIsProformaSpecification(); private readonly isProformaSpec = new CustomerInvoiceIsProformaSpecification();
private readonly isApprovedSpec = new ProformaCanTranstionToIssuedSpecification(); private readonly isApprovedSpec = new ProformaCanTranstionToIssuedSpecification();
public linkWithProforma(
invoice: CustomerInvoice,
proforma: CustomerInvoice
): Result<CustomerInvoice, Error> {}
/** /**
* Convierte una proforma en factura definitiva. * Convierte una proforma en factura definitiva.
* *

View File

@ -1,5 +1,4 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates"; import { CustomerInvoice } from "../aggregates";
import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors"; import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors";
import { CustomerInvoiceIsProformaSpecification } from "../specs"; import { CustomerInvoiceIsProformaSpecification } from "../specs";

View File

@ -1,6 +1,5 @@
import { CompositeSpecification } from "@repo/rdx-ddd"; import { CompositeSpecification } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../aggregates";
import type { CustomerInvoice } from "../aggregates";
export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification<CustomerInvoice> { export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification<CustomerInvoice> {
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> { public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {

View File

@ -1,3 +1,2 @@
export * from "./customer-invoice-is-proforma.specification"; export * from "./customer-invoice-is-proforma.specification";
export * from "./proforma-can-transtion-to-issued.specification"; export * from "./proforma-can-transtion-to-issued.specification";
export * from "./status-invoice-is-draft.specification";

View File

@ -1,11 +0,0 @@
// domain/specifications/status-invoice-is-draft.specification.ts
import { CompositeSpecification } from "@repo/rdx-ddd";
import type { CustomerInvoice } from "../aggregates";
/** Verifica que el estado es borrador */
export class StatusInvoiceIsDraftSpecification extends CompositeSpecification<CustomerInvoice> {
public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> {
return invoice.status.isDraft();
}
}

View File

@ -18,15 +18,13 @@ import {
CustomerInvoiceReportHTMLPresenter, CustomerInvoiceReportHTMLPresenter,
CustomerInvoiceReportPDFPresenter, CustomerInvoiceReportPDFPresenter,
CustomerInvoiceReportPresenter, CustomerInvoiceReportPresenter,
CustomerInvoiceTaxesReportPresenter,
DeleteProformaUseCase,
GetProformaUseCase, GetProformaUseCase,
IssueProformaInvoiceUseCase, IssueProformaInvoiceUseCase,
ListCustomerInvoicesPresenter, ListCustomerInvoicesPresenter,
ListProformasUseCase, ListProformasUseCase,
RecipientInvoiceFullPresenter, RecipientInvoiceFullPresenter,
ReportProformaUseCase, ReportProformaUseCase,
UpdateProformaUseCase, UpdateCustomerInvoiceUseCase,
} from "../application"; } from "../application";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
@ -43,14 +41,14 @@ export type CustomerInvoiceDeps = {
taxes: JsonTaxCatalogProvider; taxes: JsonTaxCatalogProvider;
}; };
useCases: { useCases: {
list_proformas: () => ListProformasUseCase; list: () => ListProformasUseCase;
get_proforma: () => GetProformaUseCase; get: () => GetProformaUseCase;
create_proforma: () => CreateProformaUseCase; create: () => CreateProformaUseCase;
update_proforma: () => UpdateProformaUseCase; update: () => UpdateCustomerInvoiceUseCase;
delete_proforma: () => DeleteProformaUseCase; //delete: () => DeleteCustomerInvoiceUseCase;
report_proforma: () => ReportProformaUseCase; report: () => ReportProformaUseCase;
issue_proforma: () => IssueProformaInvoiceUseCase; issue: () => IssueProformaInvoiceUseCase;
changeStatus_proforma: () => ChangeStatusProformaUseCase; changeStatus: () => ChangeStatusProformaUseCase;
}; };
}; };
@ -106,10 +104,6 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
key: { resource: "customer-invoice", projection: "REPORT", format: "JSON" }, key: { resource: "customer-invoice", projection: "REPORT", format: "JSON" },
presenter: new CustomerInvoiceReportPresenter(presenterRegistry), presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
}, },
{
key: { resource: "customer-invoice-taxes", projection: "REPORT", format: "JSON" },
presenter: new CustomerInvoiceTaxesReportPresenter(presenterRegistry),
},
{ {
key: { resource: "customer-invoice-items", projection: "REPORT", format: "JSON" }, key: { resource: "customer-invoice-items", projection: "REPORT", format: "JSON" },
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry), presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
@ -124,20 +118,16 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
}, },
]); ]);
const useCases: CustomerInvoiceDeps["useCases"] = { const useCases = {
list_proformas: () => list: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry),
new ListProformasUseCase(appService, transactionManager, presenterRegistry), get: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry),
get_proforma: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry), create: () =>
create_proforma: () =>
new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes), new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes),
update_proforma: () => update: () =>
new UpdateProformaUseCase(appService, transactionManager, presenterRegistry), new UpdateCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
delete_proforma: () => new DeleteProformaUseCase(appService, transactionManager), report: () => new ReportProformaUseCase(appService, transactionManager, presenterRegistry),
report_proforma: () => issue: () => new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry),
new ReportProformaUseCase(appService, transactionManager, presenterRegistry), changeStatus: () => new ChangeStatusProformaUseCase(appService, transactionManager),
issue_proforma: () =>
new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry),
changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager),
}; };
return { return {

View File

@ -4,7 +4,10 @@ import type { DeleteProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class DeleteProformaController extends ExpressController { export class DeleteProformaController extends ExpressController {
public constructor(private readonly useCase: DeleteProformaUseCase) { public constructor(
private readonly useCase: DeleteProformaUseCase
/* private readonly presenter: any */
) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
@ -19,9 +22,6 @@ export class DeleteProformaController extends ExpressController {
} }
const { proforma_id } = this.req.params; const { proforma_id } = this.req.params;
if (!proforma_id) {
return this.invalidInputError("Proforma ID missing");
}
const result = await this.useCase.execute({ proforma_id, companyId }); const result = await this.useCase.execute({ proforma_id, companyId });

View File

@ -1,11 +1,11 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import type { UpdateProformaUseCase } from "../../../../application"; import type { UpdateCustomerInvoiceUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class UpdateProformaController extends ExpressController { export class UpdateProformaController extends ExpressController {
public constructor(private readonly useCase: UpdateProformaUseCase) { public constructor(private readonly useCase: UpdateCustomerInvoiceUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -15,7 +15,6 @@ import {
isCustomerInvoiceIdAlreadyExistsError, isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError, isEntityIsNotProformaError,
isProformaCannotBeConvertedToInvoiceError, isProformaCannotBeConvertedToInvoiceError,
isProformaCannotBeDeletedError,
} from "../../domain"; } from "../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
@ -48,18 +47,8 @@ const proformaConversionRule: ErrorToApiRule = {
), ),
}; };
const proformaCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isProformaCannotBeDeletedError(e),
build: (e) =>
new ValidationApiError(
(e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted."
),
};
// 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 = ApiErrorMapper.default() export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invoiceDuplicateRule) .register(invoiceDuplicateRule)
.register(entityIsNotProformaError) .register(entityIsNotProformaError)
.register(proformaConversionRule) .register(proformaConversionRule);
.register(proformaCannotBeDeletedRule);

View File

@ -4,9 +4,7 @@ import {
ChangeStatusProformaByIdParamsRequestSchema, ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema, ChangeStatusProformaByIdRequestSchema,
CreateProformaRequestSchema, CreateProformaRequestSchema,
DeleteProformaByIdParamsRequestSchema,
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
ReportProformaByIdRequestSchema, ReportProformaByIdRequestSchema,
UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdParamsRequestSchema,
@ -21,13 +19,12 @@ import { buildCustomerInvoiceDependencies } from "../dependencies";
import { import {
ChangeStatusProformaController, ChangeStatusProformaController,
CreateProformaController, CreateProformaController,
DeleteProformaController,
GetProformaController, GetProformaController,
IssueProformaController,
ListProformasController, ListProformasController,
ReportProformaController, ReportProformaController,
UpdateProformaController, UpdateProformaController,
} from "./controllers/proformas"; } from "./controllers/proformas";
import { IssueProformaController } from "./controllers/proformas/issue-proforma.controller";
export const proformasRouter = (params: ModuleParams) => { export const proformasRouter = (params: ModuleParams) => {
const { app, baseRoutePath, logger } = params as { const { app, baseRoutePath, logger } = params as {
@ -63,7 +60,7 @@ export const proformasRouter = (params: ModuleParams) => {
//checkTabContext, //checkTabContext,
validateRequest(ListProformasRequestSchema, "params"), validateRequest(ListProformasRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.list_proformas(); const useCase = deps.useCases.list();
const controller = new ListProformasController(useCase /*, deps.presenters.list */); const controller = new ListProformasController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -74,7 +71,7 @@ export const proformasRouter = (params: ModuleParams) => {
//checkTabContext, //checkTabContext,
validateRequest(GetProformaByIdRequestSchema, "params"), validateRequest(GetProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.get_proforma(); const useCase = deps.useCases.get();
const controller = new GetProformaController(useCase); const controller = new GetProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -86,7 +83,7 @@ export const proformasRouter = (params: ModuleParams) => {
validateRequest(CreateProformaRequestSchema, "body"), validateRequest(CreateProformaRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.create_proforma(); const useCase = deps.useCases.create();
const controller = new CreateProformaController(useCase); const controller = new CreateProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -99,30 +96,30 @@ export const proformasRouter = (params: ModuleParams) => {
validateRequest(UpdateProformaByIdParamsRequestSchema, "params"), validateRequest(UpdateProformaByIdParamsRequestSchema, "params"),
validateRequest(UpdateProformaByIdRequestSchema, "body"), validateRequest(UpdateProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.update_proforma(); const useCase = deps.useCases.update();
const controller = new UpdateProformaController(useCase); const controller = new UpdateProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
router.delete( /*router.delete(
"/:proforma_id", "/:proforma_id",
//checkTabContext, //checkTabContext,
validateRequest(DeleteProformaByIdParamsRequestSchema, "params"), validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.delete_proforma(); const useCase = deps.useCases.delete();
const controller = new DeleteProformaController(useCase); const controller = new DeleteCustomerInvoiceController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );*/
router.get( router.get(
"/:proforma_id/report", "/:proforma_id/report",
//checkTabContext, //checkTabContext,
validateRequest(ReportProformaByIdRequestSchema, "params"), validateRequest(ReportProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report_proforma(); const useCase = deps.useCases.report();
const controller = new ReportProformaController(useCase); const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -136,7 +133,7 @@ export const proformasRouter = (params: ModuleParams) => {
validateRequest(ChangeStatusProformaByIdRequestSchema, "body"), validateRequest(ChangeStatusProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.changeStatus_proforma(); const useCase = deps.useCases.changeStatus();
const controller = new ChangeStatusProformaController(useCase); const controller = new ChangeStatusProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -146,10 +143,11 @@ export const proformasRouter = (params: ModuleParams) => {
"/:proforma_id/issue", "/:proforma_id/issue",
//checkTabContext, //checkTabContext,
validateRequest(IssueProformaByIdParamsRequestSchema, "params"), /*validateRequest(XXX, "params"),
validateRequest(XXX, "body"),*/
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issue_proforma(); const useCase = deps.useCases.issue();
const controller = new IssueProformaController(useCase); const controller = new IssueProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }

View File

@ -166,23 +166,16 @@ export class CustomerInvoiceRepository
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura. * @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - Identificador UUID de la factura. * @param id - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
* @returns Result<boolean, Error> * @returns Result<boolean, Error>
*/ */
async existsByIdInCompany( async existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: Transaction, transaction?: Transaction
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
try { try {
const count = await CustomerInvoiceModel.count({ const count = await CustomerInvoiceModel.count({
...options, where: { id: id.toString(), company_id: companyId.toString() },
where: {
id: id.toString(),
company_id: companyId.toString(),
...(options.where ?? {}),
},
transaction, transaction,
}); });
return Result.ok(Boolean(count > 0)); return Result.ok(Boolean(count > 0));
@ -198,7 +191,7 @@ export class CustomerInvoiceRepository
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura. * @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - UUID de la factura. * @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions) * @params options - Opciones adicionales para la consulta (Sequelize FindOptions)
* @returns Result<CustomerInvoice, Error> * @returns Result<CustomerInvoice, Error>
*/ */
async getByIdInCompany( async getByIdInCompany(
@ -374,25 +367,21 @@ export class CustomerInvoiceRepository
/** /**
* *
* Elimina o marca como eliminada una proforma dentro de una empresa. * Elimina o marca como eliminada una factura.
* *
* @param companyId - ID de la empresa. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param id - UUID de la proforma a eliminar. * @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<boolean, Error>
*/ */
async deleteProformaByIdInCompany( async deleteByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: Transaction transaction: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
try { try {
const deleted = await CustomerInvoiceModel.destroy({ const deleted = await CustomerInvoiceModel.destroy({
where: { where: { id: id.toString(), company_id: companyId.toString() },
id: id.toString(),
company_id: companyId.toString(),
is_proforma: true,
},
transaction, transaction,
}); });
@ -435,6 +424,8 @@ export class CustomerInvoiceRepository
} }
); );
console.log(affected);
if (affected === 0) { if (affected === 0) {
return Result.fail( return Result.fail(
new InfrastructureRepositoryError( new InfrastructureRepositoryError(

View File

@ -6,8 +6,8 @@ import { z } from "zod/v4";
* *
*/ */
export const DeleteProformaByIdParamsRequestSchema = z.object({ export const DeleteProformaByIdRequestSchema = z.object({
proforma_id: z.string(), id: z.string(),
}); });
export type DeleteProformaByIdRequestDTO = z.infer<typeof DeleteProformaByIdParamsRequestSchema>; export type DeleteProformaByIdRequestDTO = z.infer<typeof DeleteProformaByIdRequestSchema>;

View File

@ -2,7 +2,6 @@ export * from "./change-status-proforma-by-id.request.dto";
export * from "./create-proforma.request.dto"; export * from "./create-proforma.request.dto";
export * from "./delete-proforma-by-id.request.dto"; export * from "./delete-proforma-by-id.request.dto";
export * from "./get-proforma-by-id.request.dto"; export * from "./get-proforma-by-id.request.dto";
export * from "./issue-proforma-by-id.request.dto";
export * from "./list-proformas.request.dto"; export * from "./list-proformas.request.dto";
export * from "./report-proforma-by-id.request.dto"; export * from "./report-proforma-by-id.request.dto";
export * from "./update-proforma-by-id.request.dto"; export * from "./update-proforma-by-id.request.dto";

View File

@ -1,5 +0,0 @@
import { z } from "zod/v4";
export const IssueProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});