Verifactu
This commit is contained in:
parent
89e22de2bd
commit
4c4afe2b3a
@ -1,7 +1,9 @@
|
||||
import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
|
||||
import { Model } from "sequelize";
|
||||
import { MapperParamsType } from "../../../domain";
|
||||
import { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
||||
import type { Model } from "sequelize";
|
||||
|
||||
import type { MapperParamsType } from "../../../domain";
|
||||
|
||||
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
||||
|
||||
export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttributes, TEntity>
|
||||
implements ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../domain";
|
||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../domain";
|
||||
|
||||
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
||||
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
|
||||
|
||||
@ -55,6 +55,7 @@ export class IssueProformaUseCase {
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
@ -64,6 +65,7 @@ export class IssueProformaUseCase {
|
||||
proforma.series,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (nextNumberResult.isFailure) return Result.fail(nextNumberResult.error);
|
||||
|
||||
/** 4. Crear factura definitiva (dominio) */
|
||||
@ -71,6 +73,7 @@ export class IssueProformaUseCase {
|
||||
issueNumber: nextNumberResult.data,
|
||||
issueDate: UtcDate.today(),
|
||||
});
|
||||
|
||||
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
|
||||
|
||||
/** 5. Guardar la nueva factura */
|
||||
|
||||
@ -0,0 +1,284 @@
|
||||
<!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: Tahoma, sans-serif;
|
||||
margin: 40px;
|
||||
color: #333;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
font-family: Tahoma, sans-serif;
|
||||
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-top: 0px solid;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
border-bottom: 0px solid;
|
||||
padding: 3px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table th {
|
||||
margin-bottom: 10px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #000;
|
||||
text-align: center;
|
||||
background-color: #e7e0df;
|
||||
color: #ff0014;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
border: 2px solid black;
|
||||
border-radius: 12px;
|
||||
padding: 3px 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@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_acana.jpg" alt="Logo Acana" class="block h-24 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> - <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="info-box" style="border: 2px solid black; border-radius: 12px; padding: 10px 20px;">
|
||||
<p>Factura nº:<strong> {{series}}{{invoice_number}}</strong></p>
|
||||
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
||||
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
|
||||
</div>
|
||||
<div class="p-3 ml-9">
|
||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||
<p>{{recipient.tin}}</p>
|
||||
<p>{{recipient.street}}</p>
|
||||
<p>{{recipient.postal_code}} {{recipient.city}} {{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/factura_acana.jpg" alt="Factura"
|
||||
class="block h-14 w-auto md:h-8 mb-1" />
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<section id="details" class="border-b border-black ">
|
||||
|
||||
|
||||
<!-- Tu tabla -->
|
||||
<table class="table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2">Concepto</th>
|
||||
<th class="py-2">Ud.</th>
|
||||
<th class="py-2">Imp.</th>
|
||||
<th class="py-2"> </th>
|
||||
<th class="py-2">Imp. total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each items}}
|
||||
<tr>
|
||||
<td>{{description}}</td>
|
||||
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if discount_percentage}}{{discount_percentage}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/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></td>
|
||||
<td class="px-4 text-right">Importe neto</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{subtotal_amount}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="px-4 text-right">Descuento {{discount_percentage}}</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{discount_amount.value}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<!-- dto 0-->
|
||||
{{/if}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="px-4 text-right">Base imponible</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxable_amount}}</td>
|
||||
</tr>
|
||||
{{#each taxes}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="px-4 text-right">{{tax_name}}</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxes_amount}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr class="">
|
||||
<td></td>
|
||||
<td class="px-4 text-right accent-color">
|
||||
Total factura
|
||||
</td>
|
||||
<td class="w-5"> </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 31.839, Libro 0, Folio 191, Sección 8, Hoja M-572991
|
||||
CIF: B86913910</p>
|
||||
<p class="text-left" style="font-size: 6pt;">Información en protección de datos<br />De conformidad con lo
|
||||
dispuesto en el RGPD y LOPDGDD,
|
||||
informamos que los datos personales serán tratados por
|
||||
ALISO DESIGN S.L para cumplir con la obligación tributaria de emitir facturas. Podrá solicitar más información,
|
||||
y ejercer sus derechos escribiendo a info@acanainteriorismo.com o mediante correo postal a la dirección CALLE LA
|
||||
FUNDICION 27 POL. IND. SANTA ANA (28522) RIVAS-VACIAMADRID, MADRID. Para el ejercicio de sus derechos, en caso
|
||||
de que sea necesario, se le solicitará documento que acredite su identidad. Si siente vulnerados sus derechos
|
||||
puede presentar una reclamación ante la AEPD, en su web: www.aepd.es.</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -7,6 +7,8 @@ export interface VerifactuRecordProps {
|
||||
estado: VerifactuRecordEstado;
|
||||
url: Maybe<URLAddress>;
|
||||
qrCode: Maybe<string>;
|
||||
uuid: Maybe<string>;
|
||||
operacion: Maybe<string>;
|
||||
}
|
||||
|
||||
export class VerifactuRecord extends DomainEntity<VerifactuRecordProps> {
|
||||
@ -32,6 +34,14 @@ export class VerifactuRecord extends DomainEntity<VerifactuRecordProps> {
|
||||
return this.props.qrCode;
|
||||
}
|
||||
|
||||
get uuid(): Maybe<string> {
|
||||
return this.props.uuid;
|
||||
}
|
||||
|
||||
get operacion(): Maybe<string> {
|
||||
return this.props.operacion;
|
||||
}
|
||||
|
||||
getProps(): VerifactuRecordProps {
|
||||
return this.props;
|
||||
}
|
||||
@ -45,6 +55,8 @@ export class VerifactuRecord extends DomainEntity<VerifactuRecordProps> {
|
||||
status: this.estado.toString(),
|
||||
url: toEmptyString(this.url, (value) => value.toString()),
|
||||
qr_code: toEmptyString(this.qrCode, (value) => value.toString()),
|
||||
uuid: toEmptyString(this.uuid, (value) => value.toString()),
|
||||
operacion: toEmptyString(this.operacion, (value) => value.toString()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import type { UtcDate } from "@repo/rdx-ddd";
|
||||
import { UniqueID, type UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
import { VerifactuRecord } from "../entities";
|
||||
import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors";
|
||||
import {
|
||||
CustomerInvoiceIsProformaSpecification,
|
||||
ProformaCanTranstionToIssuedSpecification,
|
||||
} from "../specs";
|
||||
import { type CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects";
|
||||
import {
|
||||
type CustomerInvoiceNumber,
|
||||
CustomerInvoiceStatus,
|
||||
VerifactuRecordEstado,
|
||||
} from "../value-objects";
|
||||
|
||||
/**
|
||||
* Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma.
|
||||
@ -43,6 +48,23 @@ export class IssueCustomerInvoiceDomainService {
|
||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proforma.id.toString()));
|
||||
}
|
||||
|
||||
const verifactuRecordOrError = VerifactuRecord.create(
|
||||
{
|
||||
estado: VerifactuRecordEstado.createPendiente(),
|
||||
qrCode: Maybe.none(),
|
||||
url: Maybe.none(),
|
||||
uuid: Maybe.none(),
|
||||
operacion: Maybe.none(),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
|
||||
if (verifactuRecordOrError.isFailure) {
|
||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proforma.id.toString()));
|
||||
}
|
||||
|
||||
const verifactuRecord = verifactuRecordOrError.data;
|
||||
|
||||
/** 3. Generar la nueva factura definitiva (inmutable) */
|
||||
const proformaProps = proforma.getProps();
|
||||
const newInvoiceOrError = CustomerInvoice.create({
|
||||
@ -53,6 +75,7 @@ export class IssueCustomerInvoiceDomainService {
|
||||
invoiceNumber: issueNumber,
|
||||
invoiceDate: issueDate,
|
||||
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
||||
verifactu: Maybe.some(verifactuRecord),
|
||||
});
|
||||
|
||||
if (newInvoiceOrError.isFailure) {
|
||||
|
||||
@ -1,25 +1,34 @@
|
||||
import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
type ISequelizeDomainMapper,
|
||||
type MapperParamsType,
|
||||
SequelizeDomainMapper,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
toNullable,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
CustomerInvoice,
|
||||
type CustomerInvoice,
|
||||
CustomerInvoiceItem,
|
||||
CustomerInvoiceItemDescription,
|
||||
CustomerInvoiceItemProps,
|
||||
CustomerInvoiceProps,
|
||||
type CustomerInvoiceItemProps,
|
||||
type CustomerInvoiceProps,
|
||||
ItemAmount,
|
||||
ItemDiscount,
|
||||
ItemQuantity,
|
||||
ItemTaxes,
|
||||
} from "../../../domain";
|
||||
import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../../sequelize";
|
||||
import type {
|
||||
CustomerInvoiceItemCreationAttributes,
|
||||
CustomerInvoiceItemModel,
|
||||
} from "../../sequelize";
|
||||
|
||||
import { ItemTaxesDomainMapper } from "./item-taxes.mapper";
|
||||
|
||||
export interface ICustomerInvoiceItemDomainMapper
|
||||
|
||||
@ -1,31 +1,38 @@
|
||||
import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
type ISequelizeDomainMapper,
|
||||
type MapperParamsType,
|
||||
SequelizeDomainMapper,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
extractOrPushError,
|
||||
LanguageCode,
|
||||
maybeFromNullableVO,
|
||||
Percentage,
|
||||
TextValue,
|
||||
toNullable,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
toNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Collection, isNullishOrEmpty, Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Collection, Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
CustomerInvoice,
|
||||
CustomerInvoiceItems,
|
||||
CustomerInvoiceNumber,
|
||||
CustomerInvoiceProps,
|
||||
type CustomerInvoiceProps,
|
||||
CustomerInvoiceSerie,
|
||||
CustomerInvoiceStatus,
|
||||
InvoicePaymentMethod,
|
||||
} from "../../../domain";
|
||||
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
|
||||
import type { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
|
||||
|
||||
import { CustomerInvoiceItemDomainMapper } from "./customer-invoice-item.mapper";
|
||||
import { InvoiceRecipientDomainMapper } from "./invoice-recipient.mapper";
|
||||
import { TaxesDomainMapper } from "./invoice-taxes.mapper";
|
||||
import { CustomerInvoiceVerifactuDomainMapper } from "./invoice-verifactu.mapper";
|
||||
|
||||
export interface ICustomerInvoiceDomainMapper
|
||||
extends ISequelizeDomainMapper<
|
||||
@ -45,6 +52,7 @@ export class CustomerInvoiceDomainMapper
|
||||
private _itemsMapper: CustomerInvoiceItemDomainMapper;
|
||||
private _recipientMapper: InvoiceRecipientDomainMapper;
|
||||
private _taxesMapper: TaxesDomainMapper;
|
||||
private _verifactuMapper: CustomerInvoiceVerifactuDomainMapper;
|
||||
|
||||
constructor(params: MapperParamsType) {
|
||||
super();
|
||||
@ -52,6 +60,7 @@ export class CustomerInvoiceDomainMapper
|
||||
this._itemsMapper = new CustomerInvoiceItemDomainMapper(params); // Instanciar el mapper de items
|
||||
this._recipientMapper = new InvoiceRecipientDomainMapper();
|
||||
this._taxesMapper = new TaxesDomainMapper(params);
|
||||
this._verifactuMapper = new CustomerInvoiceVerifactuDomainMapper();
|
||||
}
|
||||
|
||||
private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
|
||||
@ -219,7 +228,22 @@ export class CustomerInvoiceDomainMapper
|
||||
});
|
||||
}*/
|
||||
|
||||
// 3) Items (colección)
|
||||
// 3) Verifactu (snapshot en la factura o include)
|
||||
const verifactuResult = this._verifactuMapper.mapToDomain(source.verifactu, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
/*if (verifactuResult.isFailure) {
|
||||
errors.push({
|
||||
path: "verifactu",
|
||||
|
||||
message: verifactuResult.error.message,
|
||||
});
|
||||
}*/
|
||||
|
||||
// 4) Items (colección)
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(
|
||||
source.items,
|
||||
source.items.length,
|
||||
@ -249,6 +273,7 @@ export class CustomerInvoiceDomainMapper
|
||||
|
||||
// 6) Construcción del agregado (Dominio)
|
||||
|
||||
const verifactu = verifactuResult.data;
|
||||
const recipient = recipientResult.data;
|
||||
|
||||
const items = CustomerInvoiceItems.create({
|
||||
@ -283,6 +308,7 @@ export class CustomerInvoiceDomainMapper
|
||||
paymentMethod: attributes.paymentMethod!,
|
||||
|
||||
items,
|
||||
verifactu,
|
||||
};
|
||||
|
||||
const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId);
|
||||
@ -342,7 +368,14 @@ export class CustomerInvoiceDomainMapper
|
||||
...params,
|
||||
});
|
||||
|
||||
// 4) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
// 4) Verifactu
|
||||
const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, {
|
||||
errors,
|
||||
parent: source,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 5) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
|
||||
@ -351,6 +384,7 @@ export class CustomerInvoiceDomainMapper
|
||||
|
||||
const items = itemsResult.data;
|
||||
const taxes = taxesResult.data;
|
||||
const verifactu = verifactuResult.data;
|
||||
|
||||
const allAmounts = source.getAllAmounts(); // Da los totales ya calculados
|
||||
|
||||
@ -404,6 +438,7 @@ export class CustomerInvoiceDomainMapper
|
||||
|
||||
taxes,
|
||||
items,
|
||||
verifactu,
|
||||
};
|
||||
|
||||
return Result.ok<CustomerInvoiceCreationAttributes>(
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
import { MapperParamsType } from "@erp/core/api";
|
||||
import type { MapperParamsType } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
Name,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
toNullable,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
toNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
|
||||
import { CustomerInvoiceModel } from "../../sequelize";
|
||||
|
||||
import { type CustomerInvoice, type CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../sequelize";
|
||||
|
||||
export class InvoiceRecipientDomainMapper {
|
||||
public mapToDomain(
|
||||
@ -133,7 +134,7 @@ export class InvoiceRecipientDomainMapper {
|
||||
const { isProforma, hasRecipient } = parent;
|
||||
|
||||
// Validación: facturas emitidas deben tener destinatario.
|
||||
if (!isProforma && !hasRecipient) {
|
||||
if (!(isProforma || hasRecipient)) {
|
||||
errors.push({
|
||||
path: "recipient",
|
||||
message: "[CustomerInvoiceDomainMapper] Issued customer invoice without recipient data",
|
||||
|
||||
@ -0,0 +1,142 @@
|
||||
import type { MapperParamsType } from "@erp/core/api";
|
||||
import { type ISequelizeDomainMapper, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
URLAddress,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
toNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type CustomerInvoice,
|
||||
type CustomerInvoiceProps,
|
||||
VerifactuRecord,
|
||||
VerifactuRecordEstado,
|
||||
} from "../../../domain";
|
||||
import type { VerifactuRecordCreationAttributes, VerifactuRecordModel } from "../../sequelize";
|
||||
|
||||
export interface ICustomerInvoiceVerifactuDomainMapper
|
||||
extends ISequelizeDomainMapper<
|
||||
VerifactuRecordModel,
|
||||
VerifactuRecordCreationAttributes,
|
||||
Maybe<VerifactuRecord>
|
||||
> {}
|
||||
|
||||
export class CustomerInvoiceVerifactuDomainMapper
|
||||
extends SequelizeDomainMapper<
|
||||
VerifactuRecordModel,
|
||||
VerifactuRecordCreationAttributes,
|
||||
Maybe<VerifactuRecord>
|
||||
>
|
||||
implements ICustomerInvoiceVerifactuDomainMapper
|
||||
{
|
||||
public mapToDomain(
|
||||
source: VerifactuRecordModel,
|
||||
params?: MapperParamsType
|
||||
): Result<Maybe<VerifactuRecord>, Error> {
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<CustomerInvoiceProps>;
|
||||
};
|
||||
|
||||
if (!source) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const recordId = extractOrPushError(UniqueID.create(source.id), "id", errors);
|
||||
const estado = extractOrPushError(
|
||||
VerifactuRecordEstado.create(source.estado),
|
||||
"estado",
|
||||
errors
|
||||
);
|
||||
|
||||
const qr = extractOrPushError(
|
||||
maybeFromNullableVO(source.qr, (value) => Result.ok(String(value))),
|
||||
"qr",
|
||||
errors
|
||||
);
|
||||
|
||||
const url = extractOrPushError(
|
||||
maybeFromNullableVO(source.url, (value) => URLAddress.create(value)),
|
||||
"url",
|
||||
errors
|
||||
);
|
||||
|
||||
const uuid = extractOrPushError(
|
||||
maybeFromNullableVO(source.uuid, (value) => Result.ok(String(value))),
|
||||
"uuid",
|
||||
errors
|
||||
);
|
||||
|
||||
const operacion = extractOrPushError(
|
||||
maybeFromNullableVO(source.operacion, (value) => Result.ok(String(value))),
|
||||
"operacion",
|
||||
errors
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors)
|
||||
);
|
||||
}
|
||||
|
||||
const createResult = VerifactuRecord.create(
|
||||
{
|
||||
estado: estado!,
|
||||
qrCode: qr!,
|
||||
url: url!,
|
||||
uuid: uuid!,
|
||||
operacion: operacion!,
|
||||
},
|
||||
recordId!
|
||||
);
|
||||
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Invoice verifactu entity creation failed", [
|
||||
{ path: "verifactu", message: createResult.error.message },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(createResult.data));
|
||||
}
|
||||
|
||||
mapToPersistence(
|
||||
source: Maybe<VerifactuRecord>,
|
||||
params?: MapperParamsType
|
||||
): Result<VerifactuRecordCreationAttributes, Error> {
|
||||
const { errors, parent } = params as {
|
||||
parent: CustomerInvoice;
|
||||
errors: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
if (source.isNone()) {
|
||||
return Result.ok({
|
||||
id: UniqueID.generateNewID().toPrimitive(),
|
||||
invoice_id: parent.id.toPrimitive(),
|
||||
estado: VerifactuRecordEstado.createPendiente().toPrimitive(),
|
||||
qr: null,
|
||||
url: null,
|
||||
uuid: null,
|
||||
operacion: null,
|
||||
});
|
||||
}
|
||||
|
||||
const verifactu = source.unwrap();
|
||||
|
||||
return Result.ok({
|
||||
id: verifactu.id.toPrimitive(),
|
||||
invoice_id: parent.id.toPrimitive(),
|
||||
estado: verifactu.estado.toPrimitive(),
|
||||
qr: toNullable(verifactu.qrCode, (v) => v),
|
||||
url: toNullable(verifactu.url, (v) => v.toPrimitive()),
|
||||
uuid: toNullable(verifactu.uuid, (v) => v),
|
||||
operacion: toNullable(verifactu.operacion, (v) => v),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,18 @@ export class VerifactuRecordListMapper
|
||||
errors
|
||||
);
|
||||
|
||||
const uuid = extractOrPushError(
|
||||
maybeFromNullableVO(raw.uuid, (value) => Result.ok(String(value))),
|
||||
"uuid",
|
||||
errors
|
||||
);
|
||||
|
||||
const operacion = extractOrPushError(
|
||||
maybeFromNullableVO(raw.operacion, (value) => Result.ok(String(value))),
|
||||
"operacion",
|
||||
errors
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors)
|
||||
@ -57,6 +69,8 @@ export class VerifactuRecordListMapper
|
||||
estado: estado!,
|
||||
qrCode: qr!,
|
||||
url: url!,
|
||||
uuid: uuid!,
|
||||
operacion: operacion!,
|
||||
},
|
||||
recordId!
|
||||
);
|
||||
|
||||
@ -17,14 +17,18 @@ import type {
|
||||
CustomerInvoiceTaxCreationAttributes,
|
||||
CustomerInvoiceTaxModel,
|
||||
} from "./customer-invoice-tax.model";
|
||||
import type { VerifactuRecordModel } from "./verifactu-record.model";
|
||||
import type {
|
||||
VerifactuRecordCreationAttributes,
|
||||
VerifactuRecordModel,
|
||||
} from "./verifactu-record.model";
|
||||
|
||||
export type CustomerInvoiceCreationAttributes = InferCreationAttributes<
|
||||
CustomerInvoiceModel,
|
||||
{ omit: "items" | "taxes" | "current_customer" }
|
||||
{ omit: "items" | "taxes" | "current_customer" | "verifactu" }
|
||||
> & {
|
||||
items?: CustomerInvoiceItemCreationAttributes[];
|
||||
taxes?: CustomerInvoiceTaxCreationAttributes[];
|
||||
verifactu?: VerifactuRecordCreationAttributes;
|
||||
};
|
||||
|
||||
export class CustomerInvoiceModel extends Model<
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
type CreationOptional,
|
||||
DataTypes,
|
||||
type InferAttributes,
|
||||
type InferCreationAttributes,
|
||||
@ -6,7 +7,10 @@ import {
|
||||
type Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type VerifactuRecordCreationAttributes = InferCreationAttributes<VerifactuRecordModel>;
|
||||
export type VerifactuRecordCreationAttributes = InferCreationAttributes<
|
||||
VerifactuRecordModel,
|
||||
{ omit: "url" | "qr" | "uuid" | "operacion" }
|
||||
>;
|
||||
|
||||
export class VerifactuRecordModel extends Model<
|
||||
InferAttributes<VerifactuRecordModel>,
|
||||
@ -14,13 +18,12 @@ export class VerifactuRecordModel extends Model<
|
||||
> {
|
||||
declare id: string;
|
||||
declare invoice_id: string;
|
||||
|
||||
declare estado: string;
|
||||
declare url: string;
|
||||
declare qr: Blob;
|
||||
|
||||
declare uuid: string;
|
||||
declare operacion: string;
|
||||
declare url: CreationOptional<string>;
|
||||
declare qr: CreationOptional<Blob>;
|
||||
declare uuid: CreationOptional<string>;
|
||||
declare operacion: CreationOptional<string>;
|
||||
|
||||
static associate(database: Sequelize) {
|
||||
const models = database.models;
|
||||
@ -64,29 +67,31 @@ export default (database: Sequelize) => {
|
||||
|
||||
estado: {
|
||||
type: new DataTypes.TEXT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
url: {
|
||||
type: new DataTypes.TEXT(),
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
qr: {
|
||||
type: new DataTypes.BLOB(),
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
uuid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
operacion: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -28,11 +28,11 @@
|
||||
"proformas": {
|
||||
"status": {
|
||||
"all": "Todas",
|
||||
"draft": "Borradores",
|
||||
"sent": "Enviadas",
|
||||
"approved": "Aprovadas",
|
||||
"rejected": "Rechazadas",
|
||||
"issued": "Emitidas"
|
||||
"draft": "Borrador",
|
||||
"sent": "Enviada",
|
||||
"approved": "Aprobada",
|
||||
"rejected": "Rechazada",
|
||||
"issued": "Emitida"
|
||||
}
|
||||
},
|
||||
"issued_invoices": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user