Compare commits
No commits in common. "84bb21e5276401e9901dcedd6e92750732c49af8" and "1db1339bff87e1df3d99b89fa078a76736a32647" have entirely different histories.
84bb21e527
...
1db1339bff
@ -2,27 +2,6 @@
|
|||||||
|
|
||||||
Este directorio reúne la documentación operativa y arquitectónica del ERP.
|
Este directorio reúne la documentación operativa y arquitectónica del ERP.
|
||||||
|
|
||||||
## Contexto técnico
|
|
||||||
Monorepo con:
|
|
||||||
- Node.js >= 22
|
|
||||||
- TypeScript
|
|
||||||
- PNPM
|
|
||||||
- Turborepo
|
|
||||||
- ExpressJS
|
|
||||||
- MariaDB
|
|
||||||
- Sequelize v6
|
|
||||||
- Vitest
|
|
||||||
|
|
||||||
Arquitectura:
|
|
||||||
- DDD pragmático
|
|
||||||
- Clean Architecture
|
|
||||||
- Separación Domain / Application / Infrastructure
|
|
||||||
- SOLID, DRY, KISS, YAGNI, SSOT
|
|
||||||
- Fail Fast
|
|
||||||
- Defensive Programming
|
|
||||||
- Cambios incrementales
|
|
||||||
- Evitar sobreingeniería
|
|
||||||
|
|
||||||
## Documentación vigente
|
## Documentación vigente
|
||||||
|
|
||||||
### Arquitectura general
|
### Arquitectura general
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
# FactuGES -> Proformas V2
|
|
||||||
|
|
||||||
## Resumen
|
|
||||||
|
|
||||||
FactuGES no envia `proformaSeriesCode`, `targetInvoiceSeriesCode` ni `taxConfig`.
|
|
||||||
|
|
||||||
El mapper de `factuges` complementa esos datos antes de delegar en el flujo actual de creacion de proformas V2 de `customer-invoices`.
|
|
||||||
|
|
||||||
## Reglas aplicadas
|
|
||||||
|
|
||||||
- `proformaSeriesCode = null`
|
|
||||||
- `targetInvoiceSeriesCode = null`
|
|
||||||
- `taxConfig.taxMode = single`
|
|
||||||
- `taxConfig.defaultIvaCode = iva_21`
|
|
||||||
- `taxConfig.usesEquivalenceSurcharge = false`
|
|
||||||
- `taxConfig.defaultRecCode = null`
|
|
||||||
- `taxConfig.usesRetention = false`
|
|
||||||
- `taxConfig.defaultRetentionCode = null`
|
|
||||||
|
|
||||||
## Semantica de series
|
|
||||||
|
|
||||||
- `proformaSeriesCode = null` permite que `document-series` asigne la serie por defecto de `proforma`
|
|
||||||
- `targetInvoiceSeriesCode = null` deja la futura factura emitida sin serie fijada y al emitir se usara la serie por defecto de `issued_invoice`
|
|
||||||
- el campo legacy `series` puede seguir llegando desde FactuGES, pero ya no se usa para crear la proforma V2
|
|
||||||
|
|
||||||
## Fiscalidad
|
|
||||||
|
|
||||||
- la cabecera de la proforma se crea con `tax_mode = single`
|
|
||||||
- las lineas se envian con `iva_code = iva_21`
|
|
||||||
- `rec_code` queda `null`
|
|
||||||
- `retention_code` queda `null`
|
|
||||||
|
|
||||||
## Estado inicial
|
|
||||||
|
|
||||||
El estado inicial se conserva segun el flujo actual de FactuGES.
|
|
||||||
|
|
||||||
Actualmente el caso de uso de FactuGES sigue creando la proforma con `status = approved`, por lo que esta adaptacion no fuerza `draft` ni introduce una regla nueva de estado.
|
|
||||||
@ -42,6 +42,7 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
|||||||
): RequestHandler => {
|
): RequestHandler => {
|
||||||
return async (req, res, next) => {
|
return async (req, res, next) => {
|
||||||
console.debug(`Validating request ${source} with schema.`);
|
console.debug(`Validating request ${source} with schema.`);
|
||||||
|
console.debug(req[source]);
|
||||||
|
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
console.debug("ERROR: Undefined schema!!");
|
console.debug("ERROR: Undefined schema!!");
|
||||||
@ -52,8 +53,6 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
|||||||
const result = schema.safeParse(req[source]);
|
const result = schema.safeParse(req[source]);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.debug(req[source]);
|
|
||||||
console.debug("ERROR: Validation failed with errors!!");
|
|
||||||
// Construye errores detallados
|
// Construye errores detallados
|
||||||
const validationErrors = result.error.issues.map((err) => ({
|
const validationErrors = result.error.issues.map((err) => ({
|
||||||
field: err.path.join("."),
|
field: err.path.join("."),
|
||||||
|
|||||||
@ -13,21 +13,21 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, NumberHelper, Result } from "@repo/rdx-utils";
|
import { Maybe, NumberHelper, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateProformaRequestDTO, TaxCombinationCodeDTO } from "../../../../common";
|
import type { CreateProformaRequestDTO } from "../../../../common";
|
||||||
import {
|
import {
|
||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
type ProformaRecipient,
|
|
||||||
type ProformaTaxMode,
|
type ProformaTaxMode,
|
||||||
|
type ProformaRecipient,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type {
|
import type {
|
||||||
ProformaCreateInputProps,
|
ProformaCreateInputProps,
|
||||||
ProformaItemCreateInputProps,
|
ProformaItemCreateInputProps,
|
||||||
ProformaItemTaxCodesInput,
|
|
||||||
ProformaTaxConfigInputProps,
|
ProformaTaxConfigInputProps,
|
||||||
|
ProformaItemTaxCodesInput,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
|
||||||
export interface ICreateProformaInputMapper {
|
export interface ICreateProformaInputMapper {
|
||||||
@ -67,16 +67,14 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const targetInvoiceSeriesCode = extractOrPushError(
|
const targetInvoiceSeriesCode = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.target_invoice_series_code, (value) =>
|
maybeFromNullableResult(dto.target_invoice_series_code, (value) => InvoiceSerie.create(value)),
|
||||||
InvoiceSerie.create(value)
|
|
||||||
),
|
|
||||||
"target_invoice_series_code",
|
"target_invoice_series_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const proformaDate = extractOrPushError(
|
const proformaDate = extractOrPushError(
|
||||||
UtcDate.createFromISO(dto.proforma_date),
|
UtcDate.createFromISO(dto.proforma_date),
|
||||||
"proforma_date",
|
"invoice_date",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -196,9 +194,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
}
|
}
|
||||||
): ProformaItemCreateInputProps[] {
|
): ProformaItemCreateInputProps[] {
|
||||||
const normalizedItemsDTO = itemsDTO ?? [];
|
return itemsDTO.map((item, index) => {
|
||||||
|
|
||||||
return normalizedItemsDTO.map((item, index) => {
|
|
||||||
const description = extractOrPushError(
|
const description = extractOrPushError(
|
||||||
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
|
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
|
||||||
`items[${index}].description`,
|
`items[${index}].description`,
|
||||||
@ -252,7 +248,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mapTaxesProps(
|
private mapTaxesProps(
|
||||||
taxesDTO: TaxCombinationCodeDTO,
|
taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"],
|
||||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||||
): ProformaItemTaxCodesInput {
|
): ProformaItemTaxCodesInput {
|
||||||
const parts = taxesDTO.split(";");
|
const parts = taxesDTO.split(";");
|
||||||
@ -369,4 +365,5 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
|
import type { IProformaCreateProps, IProformaItemCreateProps, ProformaTaxMode } from "../../../domain";
|
||||||
import type { Maybe } from "@repo/rdx-utils";
|
import type { Maybe } from "@repo/rdx-utils";
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
import type {
|
|
||||||
IProformaCreateProps,
|
|
||||||
IProformaItemCreateProps,
|
|
||||||
InvoiceSerie,
|
|
||||||
ProformaTaxMode,
|
|
||||||
} from "../../../domain";
|
|
||||||
|
|
||||||
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
||||||
|
|
||||||
@ -30,13 +25,7 @@ export interface ProformaTaxConfigInputProps {
|
|||||||
|
|
||||||
export type ProformaCreateInputProps = Omit<
|
export type ProformaCreateInputProps = Omit<
|
||||||
IProformaCreateProps,
|
IProformaCreateProps,
|
||||||
| "items"
|
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series" | "taxConfig"
|
||||||
| "proformaReference"
|
|
||||||
| "proformaNumber"
|
|
||||||
| "documentSeriesId"
|
|
||||||
| "series"
|
|
||||||
| "taxConfig"
|
|
||||||
| "archivedAt"
|
|
||||||
> & {
|
> & {
|
||||||
proformaSeriesCode: Maybe<InvoiceSerie>;
|
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||||
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
@ -192,20 +191,14 @@ const ProformasSelectionActions = ({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent align="start" className="w-52">
|
||||||
align="start"
|
<DropdownMenuLabel>Selección</DropdownMenuLabel>
|
||||||
className="w-52"
|
<DropdownMenuSeparator />
|
||||||
render={
|
<DropdownMenuItem onClick={() => onDelete(selectedProformas)} variant="destructive">
|
||||||
<DropdownMenuGroup>
|
<Trash2Icon aria-hidden="true" className="mr-2 size-4" />
|
||||||
<DropdownMenuLabel>Selección</DropdownMenuLabel>
|
<span>Eliminar borradores</span>
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onDelete(selectedProformas)} variant="destructive">
|
</DropdownMenuContent>
|
||||||
<Trash2Icon aria-hidden="true" className="mr-2 size-4" />
|
|
||||||
<span>Eliminar borradores</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@ -340,7 +333,7 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
onRowClick={
|
onRowClick={
|
||||||
scope === "deleted" ? undefined : (proformaId) => handleOpenDetailClick(proformaId)
|
scope !== "deleted" ? (proformaId) => handleOpenDetailClick(proformaId) : undefined
|
||||||
}
|
}
|
||||||
onSortChange={listCtrl.setSort}
|
onSortChange={listCtrl.setSort}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
InvoiceAmount,
|
InvoiceAmount,
|
||||||
type InvoiceSerie,
|
InvoiceSerie,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
@ -89,8 +89,7 @@ export type ProformaDraftItem = {
|
|||||||
|
|
||||||
export type ProformaDraft = {
|
export type ProformaDraft = {
|
||||||
factugesID: string;
|
factugesID: string;
|
||||||
proformaSeriesCode: Maybe<InvoiceSerie>;
|
series: Maybe<InvoiceSerie>;
|
||||||
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
|
||||||
invoiceDate: UtcDate;
|
invoiceDate: UtcDate;
|
||||||
operationDate: Maybe<UtcDate>;
|
operationDate: Maybe<UtcDate>;
|
||||||
reference: Maybe<string>;
|
reference: Maybe<string>;
|
||||||
@ -102,7 +101,6 @@ export type ProformaDraft = {
|
|||||||
globalDiscountPercentage: DiscountPercentage;
|
globalDiscountPercentage: DiscountPercentage;
|
||||||
itemsDiscountAmount: Maybe<InvoiceAmount>;
|
itemsDiscountAmount: Maybe<InvoiceAmount>;
|
||||||
taxableAmount: Maybe<InvoiceAmount>;
|
taxableAmount: Maybe<InvoiceAmount>;
|
||||||
taxConfig: FactugesDefaultProformaTaxConfig;
|
|
||||||
taxes: ProformaItemTaxesProps;
|
taxes: ProformaItemTaxesProps;
|
||||||
taxesAmount: Maybe<InvoiceAmount>;
|
taxesAmount: Maybe<InvoiceAmount>;
|
||||||
totalAmount: Maybe<InvoiceAmount>;
|
totalAmount: Maybe<InvoiceAmount>;
|
||||||
@ -122,24 +120,6 @@ export type FactugesProformaPayload = {
|
|||||||
proformaDraft: ProformaDraft;
|
proformaDraft: ProformaDraft;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FactugesDefaultProformaTaxConfig = {
|
|
||||||
taxMode: "single";
|
|
||||||
defaultIvaCode: Maybe<string>;
|
|
||||||
usesEquivalenceSurcharge: false;
|
|
||||||
defaultRecCode: Maybe<string>;
|
|
||||||
usesRetention: false;
|
|
||||||
defaultRetentionCode: Maybe<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FACTUGES_DEFAULT_PROFORMA_TAX_CONFIG: FactugesDefaultProformaTaxConfig = {
|
|
||||||
taxMode: "single",
|
|
||||||
defaultIvaCode: Maybe.some("iva_21"),
|
|
||||||
usesEquivalenceSurcharge: false,
|
|
||||||
defaultRecCode: Maybe.none(),
|
|
||||||
usesRetention: false,
|
|
||||||
defaultRetentionCode: Maybe.none(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ICreateProformaFromFactugesInputMapper {
|
export interface ICreateProformaFromFactugesInputMapper {
|
||||||
map(
|
map(
|
||||||
dto: CreateProformaFromFactugesRequestDTO,
|
dto: CreateProformaFromFactugesRequestDTO,
|
||||||
@ -200,17 +180,17 @@ export class CreateProformaFromFactugesInputMapper
|
|||||||
}
|
}
|
||||||
): ProformaDraft {
|
): ProformaDraft {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
const { companyId } = params;
|
||||||
|
|
||||||
//const defaultStatus = InvoiceStatus.fromApproved();
|
//const defaultStatus = InvoiceStatus.fromApproved();
|
||||||
|
|
||||||
//const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
//const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||||
|
|
||||||
/**
|
const series = extractOrPushError(
|
||||||
* Complementa los datos legacy con los defaults actuales de proformas.
|
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
|
||||||
*
|
"series",
|
||||||
* La serie legacy `series` no se usa porque la numeracion documental
|
errors
|
||||||
* se resuelve mediante `document-series`.
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
/*const proformaNumber = extractOrPushError(
|
/*const proformaNumber = extractOrPushError(
|
||||||
InvoiceNumber.create(dto.),
|
InvoiceNumber.create(dto.),
|
||||||
@ -227,7 +207,7 @@ export class CreateProformaFromFactugesInputMapper
|
|||||||
);
|
);
|
||||||
|
|
||||||
const invoiceDate = extractOrPushError(
|
const invoiceDate = extractOrPushError(
|
||||||
UtcDate.createFromISO(dto.proforma_date),
|
UtcDate.createFromISO(dto.invoice_date),
|
||||||
"invoice_date",
|
"invoice_date",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -324,8 +304,7 @@ export class CreateProformaFromFactugesInputMapper
|
|||||||
|
|
||||||
//invoiceNumber: proformaNumber!,
|
//invoiceNumber: proformaNumber!,
|
||||||
factugesID: factugesID,
|
factugesID: factugesID,
|
||||||
proformaSeriesCode: Maybe.none(),
|
series: series!,
|
||||||
targetInvoiceSeriesCode: Maybe.none(),
|
|
||||||
|
|
||||||
invoiceDate: invoiceDate!,
|
invoiceDate: invoiceDate!,
|
||||||
operationDate: operationDate!,
|
operationDate: operationDate!,
|
||||||
@ -345,7 +324,6 @@ export class CreateProformaFromFactugesInputMapper
|
|||||||
itemsDiscountAmount: itemsDiscountAmount!,
|
itemsDiscountAmount: itemsDiscountAmount!,
|
||||||
|
|
||||||
taxableAmount: taxableAmount!,
|
taxableAmount: taxableAmount!,
|
||||||
taxConfig: FACTUGES_DEFAULT_PROFORMA_TAX_CONFIG,
|
|
||||||
taxes: taxes,
|
taxes: taxes,
|
||||||
taxesAmount: taxesAmount!,
|
taxesAmount: taxesAmount!,
|
||||||
totalAmount: totalAmount!,
|
totalAmount: totalAmount!,
|
||||||
@ -629,28 +607,86 @@ export class CreateProformaFromFactugesInputMapper
|
|||||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
||||||
|
|
||||||
private mapItemTaxesProps(
|
private mapItemTaxesProps(
|
||||||
_itemDTO: CreateProformaItemFromFactugesRequestDTO,
|
itemDTO: CreateProformaItemFromFactugesRequestDTO,
|
||||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||||
): ProformaItemTaxesProps {
|
): ProformaItemTaxesProps {
|
||||||
const iva = extractOrPushError(
|
const { itemIndex, errors } = params;
|
||||||
this.mapTaxToDomain({
|
|
||||||
code: "iva_21",
|
|
||||||
percentageValue: 21,
|
|
||||||
group: "iva",
|
|
||||||
calculationBehavior: "additive",
|
|
||||||
fieldPath: `items[${params.itemIndex}].iva`,
|
|
||||||
}),
|
|
||||||
`items[${params.itemIndex}].iva`,
|
|
||||||
params.errors
|
|
||||||
);
|
|
||||||
|
|
||||||
this.throwIfValidationErrors(params.errors);
|
const taxesProps: ProformaItemTaxesProps = {
|
||||||
|
iva: Maybe.none(),
|
||||||
return {
|
|
||||||
iva: iva ?? Maybe.none(),
|
|
||||||
retention: Maybe.none(),
|
retention: Maybe.none(),
|
||||||
rec: Maybe.none(),
|
rec: Maybe.none(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const iva = extractOrPushError(
|
||||||
|
this.mapTaxToDomain({
|
||||||
|
code: itemDTO.iva_code,
|
||||||
|
percentageValue: Number(itemDTO.iva_percentage_value),
|
||||||
|
group: "iva",
|
||||||
|
calculationBehavior: "additive",
|
||||||
|
fieldPath: `items[${itemIndex}].iva`,
|
||||||
|
}),
|
||||||
|
`items[${itemIndex}].iva`,
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const rec = extractOrPushError(
|
||||||
|
this.mapTaxToDomain({
|
||||||
|
code: itemDTO.rec_code,
|
||||||
|
percentageValue: Number(itemDTO.rec_percentage_value),
|
||||||
|
group: "surcharge",
|
||||||
|
calculationBehavior: "additive",
|
||||||
|
fieldPath: `items[${itemIndex}].rec`,
|
||||||
|
}),
|
||||||
|
`items[${itemIndex}].rec`,
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const retention = extractOrPushError(
|
||||||
|
this.mapTaxToDomain({
|
||||||
|
code: itemDTO.retention_code,
|
||||||
|
percentageValue: Number(itemDTO.retention_percentage_value),
|
||||||
|
group: "retention",
|
||||||
|
calculationBehavior: "subtractive",
|
||||||
|
fieldPath: `items[${itemIndex}].retention`,
|
||||||
|
}),
|
||||||
|
`items[${itemIndex}].retention`,
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if (iva) {
|
||||||
|
if (taxesProps.iva.isSome()) {
|
||||||
|
errors.push({
|
||||||
|
path: `items[${itemIndex}].taxes`,
|
||||||
|
message: "Multiple taxes for group VAT are not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
taxesProps.iva = iva!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec) {
|
||||||
|
if (taxesProps.rec.isSome()) {
|
||||||
|
errors.push({
|
||||||
|
path: `items[${itemIndex}].taxes`,
|
||||||
|
message: "Multiple taxes for group rec are not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
taxesProps.rec!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retention) {
|
||||||
|
if (taxesProps.retention.isSome()) {
|
||||||
|
errors.push({
|
||||||
|
path: `items[${itemIndex}].taxes`,
|
||||||
|
message: "Multiple taxes for group retention are not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
taxesProps.retention!;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
|
return taxesProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTaxToDomain(params: {
|
private mapTaxToDomain(params: {
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import { type ITransactionManager, isEntityNotFoundError } from "@erp/core/api";
|
|||||||
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
|
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
|
||||||
import {
|
import {
|
||||||
type InvoiceAmount,
|
type InvoiceAmount,
|
||||||
|
type InvoiceRecipient,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
type ItemAmount,
|
type ItemAmount,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaRecipient,
|
|
||||||
} from "@erp/customer-invoices/api/domain";
|
} from "@erp/customer-invoices/api/domain";
|
||||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||||
import {
|
import {
|
||||||
@ -293,6 +293,43 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
return `${baseMessage} FactuGES: ${expected.formattedValue}. Calculado: ${actual.formattedValue}.`;
|
return `${baseMessage} FactuGES: ${expected.formattedValue}. Calculado: ${actual.formattedValue}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida un importe opcional esperado contra un importe real también opcional.
|
||||||
|
*
|
||||||
|
* Motivo:
|
||||||
|
* - Algunos campos pueden faltar tanto en el payload importado como en
|
||||||
|
* la proyección o snapshot generado.
|
||||||
|
* - Si el esperado existe pero el real no, se considera discrepancia.
|
||||||
|
*/
|
||||||
|
private validateOptionalMaybeAmount(params: {
|
||||||
|
expected: Maybe<InvoiceAmount | ItemAmount>;
|
||||||
|
actual: Maybe<InvoiceAmount | ItemAmount>;
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
errors: ValidationErrorDetail[];
|
||||||
|
}): void {
|
||||||
|
const { expected, actual, path, message, errors } = params;
|
||||||
|
|
||||||
|
if (expected.isNone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual.isNone()) {
|
||||||
|
errors.push({
|
||||||
|
path,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actual.unwrap().equals(expected.unwrap())) {
|
||||||
|
errors.push({
|
||||||
|
path,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildProformaCreateProps(deps: {
|
private buildProformaCreateProps(deps: {
|
||||||
proformaDraft: FactugesProformaPayload["proformaDraft"];
|
proformaDraft: FactugesProformaPayload["proformaDraft"];
|
||||||
customerId: UniqueID;
|
customerId: UniqueID;
|
||||||
@ -306,9 +343,10 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
const { companyId } = context;
|
const { companyId } = context;
|
||||||
|
|
||||||
const defaultStatus = InvoiceStatus.approved();
|
const defaultStatus = InvoiceStatus.approved();
|
||||||
const recipient = Maybe.none<ProformaRecipient>();
|
const recipient = Maybe.none<InvoiceRecipient>();
|
||||||
const linkedInvoiceId = Maybe.none<UniqueID>();
|
const linkedInvoiceId = Maybe.none<UniqueID>();
|
||||||
const paymentMethodId = Maybe.some(payment.id);
|
const paymentMethodId = Maybe.some(payment.id);
|
||||||
|
const paymentTermId = Maybe.none();
|
||||||
const taxRegimeCode = Maybe.some("01");
|
const taxRegimeCode = Maybe.some("01");
|
||||||
|
|
||||||
const items: CreateProformaProps["items"] = proformaDraft.items.map((draftItem) => {
|
const items: CreateProformaProps["items"] = proformaDraft.items.map((draftItem) => {
|
||||||
@ -344,10 +382,8 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
currencyCode: proformaDraft.currencyCode,
|
currencyCode: proformaDraft.currencyCode,
|
||||||
notes: proformaDraft.notes,
|
notes: proformaDraft.notes,
|
||||||
operationDate: proformaDraft.operationDate,
|
operationDate: proformaDraft.operationDate,
|
||||||
proformaSeriesCode: proformaDraft.proformaSeriesCode,
|
series: proformaDraft.series,
|
||||||
targetInvoiceSeriesCode: proformaDraft.targetInvoiceSeriesCode,
|
|
||||||
reference: proformaDraft.reference,
|
reference: proformaDraft.reference,
|
||||||
taxConfig: proformaDraft.taxConfig,
|
|
||||||
items,
|
items,
|
||||||
companyId,
|
companyId,
|
||||||
customerId,
|
customerId,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const CreateProformaFromFactugesRequestSchema = z.object({
|
|||||||
reference: z.string().default(""),
|
reference: z.string().default(""),
|
||||||
description: z.string().default(""),
|
description: z.string().default(""),
|
||||||
|
|
||||||
proforma_date: z.string(),
|
invoice_date: z.string(),
|
||||||
operation_date: z.string().default(""),
|
operation_date: z.string().default(""),
|
||||||
|
|
||||||
notes: z.string().default(""),
|
notes: z.string().default(""),
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
|
||||||
|
|
||||||
import { buildBooleanFullTextQuery } from "../build-boolean-full-text-query";
|
|
||||||
|
|
||||||
describe("buildBooleanFullTextQuery", () => {
|
|
||||||
test.each([
|
|
||||||
["pr", "+pr*"],
|
|
||||||
["pr-", "+pr*"],
|
|
||||||
["-pr", "+pr*"],
|
|
||||||
["cliente prueba", "+cliente* +prueba*"],
|
|
||||||
["cliente-prueba", "+cliente* +prueba*"],
|
|
||||||
["José Pérez", "+José* +Pérez*"],
|
|
||||||
["++ -- **", ""],
|
|
||||||
["", ""],
|
|
||||||
[" ", ""],
|
|
||||||
['+ - > < ( ) ~ * " @', ""],
|
|
||||||
])("normalizes %p into %p", (input, expected) => {
|
|
||||||
expect(buildBooleanFullTextQuery(input)).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
|
||||||
import { Op, type Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
import { Criteria } from "../critera";
|
|
||||||
import { CriteriaToSequelizeConverter } from "../criteria-to-sequelize-converter";
|
|
||||||
|
|
||||||
const createDatabaseStub = () =>
|
|
||||||
({
|
|
||||||
escape: (value: string) => `'${value.replaceAll("'", "''")}'`,
|
|
||||||
}) as Sequelize;
|
|
||||||
|
|
||||||
describe("CriteriaToSequelizeConverter quick search", () => {
|
|
||||||
test("builds a full-text filter and score for free text search", () => {
|
|
||||||
const converter = new CriteriaToSequelizeConverter();
|
|
||||||
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "pr-");
|
|
||||||
|
|
||||||
const options = converter.convert(criteria, {
|
|
||||||
searchableFields: ["name", "trade_name"],
|
|
||||||
sortableFields: ["name"],
|
|
||||||
enableFullText: true,
|
|
||||||
database: createDatabaseStub(),
|
|
||||||
strictMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(options.attributes).toEqual({
|
|
||||||
include: [[expect.objectContaining({ val: expect.stringContaining("'+pr*'") }), "score"]],
|
|
||||||
});
|
|
||||||
expect(options.order).toEqual([
|
|
||||||
[expect.objectContaining({ val: "score" }), "DESC"],
|
|
||||||
["name", "ASC"],
|
|
||||||
]);
|
|
||||||
expect(options.where).toEqual({
|
|
||||||
[Op.and]: expect.any(Array),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("omits MATCH, score and score ordering when the normalized search is empty", () => {
|
|
||||||
const converter = new CriteriaToSequelizeConverter();
|
|
||||||
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "++ -- **");
|
|
||||||
|
|
||||||
const options = converter.convert(criteria, {
|
|
||||||
searchableFields: ["name", "trade_name"],
|
|
||||||
sortableFields: ["name"],
|
|
||||||
enableFullText: true,
|
|
||||||
database: createDatabaseStub(),
|
|
||||||
strictMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(options.attributes).toBeUndefined();
|
|
||||||
expect(options.order).toEqual([["name", "ASC"]]);
|
|
||||||
expect(options.where).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
const FULL_TEXT_TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
|
|
||||||
|
|
||||||
export function buildBooleanFullTextQuery(search: string): string {
|
|
||||||
if (!search) return "";
|
|
||||||
|
|
||||||
const terms = search.normalize("NFKC").match(FULL_TEXT_TOKEN_PATTERN) ?? [];
|
|
||||||
|
|
||||||
return terms.map((term) => `+${term}*`).join(" ");
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ import type {
|
|||||||
CriteriaMappings,
|
CriteriaMappings,
|
||||||
ICriteriaToOrmConverter,
|
ICriteriaToOrmConverter,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import { buildBooleanFullTextQuery } from "./build-boolean-full-text-query.js";
|
|
||||||
import { appendOrder, normalizeOrder, prependOrder } from "./utils";
|
import { appendOrder, normalizeOrder, prependOrder } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,8 +66,11 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const booleanTerm = buildBooleanFullTextQuery(term);
|
const booleanTerm = term
|
||||||
if (!booleanTerm) return;
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((word) => `+${word}*`)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
const qualifiedFields = searchableFields.map((field) =>
|
const qualifiedFields = searchableFields.map((field) =>
|
||||||
this.qualifyRootColumn(field, fullTextTableAlias)
|
this.qualifyRootColumn(field, fullTextTableAlias)
|
||||||
@ -214,6 +216,7 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
return Op.lt;
|
return Op.lt;
|
||||||
case "LOWER_THAN_OR_EQUAL":
|
case "LOWER_THAN_OR_EQUAL":
|
||||||
return Op.lte;
|
return Op.lte;
|
||||||
|
case "EQUALS":
|
||||||
default:
|
default:
|
||||||
return Op.eq;
|
return Op.eq;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./critera";
|
export * from "./critera";
|
||||||
export * from "./build-boolean-full-text-query";
|
|
||||||
export * from "./criteria-from-url-converter";
|
export * from "./criteria-from-url-converter";
|
||||||
export * from "./criteria-to-sequelize-converter";
|
export * from "./criteria-to-sequelize-converter";
|
||||||
export * from "./defaults";
|
export * from "./defaults";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user