Refactor FactuGES Proforma Integration: Update Data Mapping and Add Documentation
This commit is contained in:
parent
163f2b9fa4
commit
84bb21e527
37
docs/factuges/create-proforma-from-factuges.md
Normal file
37
docs/factuges/create-proforma-from-factuges.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 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,7 +42,6 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
||||
): RequestHandler => {
|
||||
return async (req, res, next) => {
|
||||
console.debug(`Validating request ${source} with schema.`);
|
||||
console.debug(req[source]);
|
||||
|
||||
if (!schema) {
|
||||
console.debug("ERROR: Undefined schema!!");
|
||||
@ -53,6 +52,7 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
||||
const result = schema.safeParse(req[source]);
|
||||
|
||||
if (!result.success) {
|
||||
console.debug(req[source]);
|
||||
console.debug("ERROR: Validation failed with errors!!");
|
||||
// Construye errores detallados
|
||||
const validationErrors = result.error.issues.map((err) => ({
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
CardContent,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@ -191,14 +192,20 @@ const ProformasSelectionActions = ({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-52"
|
||||
render={
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Selección</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onDelete(selectedProformas)} variant="destructive">
|
||||
<Trash2Icon aria-hidden="true" className="mr-2 size-4" />
|
||||
<span>Eliminar borradores</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuGroup>
|
||||
}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</>
|
||||
@ -333,7 +340,7 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
onRowClick={
|
||||
scope !== "deleted" ? (proformaId) => handleOpenDetailClick(proformaId) : undefined
|
||||
scope === "deleted" ? undefined : (proformaId) => handleOpenDetailClick(proformaId)
|
||||
}
|
||||
onSortChange={listCtrl.setSort}
|
||||
pageIndex={listCtrl.pageIndex}
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
InvoiceAmount,
|
||||
InvoiceSerie,
|
||||
type InvoiceSerie,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemQuantity,
|
||||
@ -89,7 +89,8 @@ export type ProformaDraftItem = {
|
||||
|
||||
export type ProformaDraft = {
|
||||
factugesID: string;
|
||||
series: Maybe<InvoiceSerie>;
|
||||
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||
invoiceDate: UtcDate;
|
||||
operationDate: Maybe<UtcDate>;
|
||||
reference: Maybe<string>;
|
||||
@ -101,6 +102,7 @@ export type ProformaDraft = {
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
itemsDiscountAmount: Maybe<InvoiceAmount>;
|
||||
taxableAmount: Maybe<InvoiceAmount>;
|
||||
taxConfig: FactugesDefaultProformaTaxConfig;
|
||||
taxes: ProformaItemTaxesProps;
|
||||
taxesAmount: Maybe<InvoiceAmount>;
|
||||
totalAmount: Maybe<InvoiceAmount>;
|
||||
@ -120,6 +122,24 @@ export type FactugesProformaPayload = {
|
||||
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 {
|
||||
map(
|
||||
dto: CreateProformaFromFactugesRequestDTO,
|
||||
@ -180,17 +200,17 @@ export class CreateProformaFromFactugesInputMapper
|
||||
}
|
||||
): ProformaDraft {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const { companyId } = params;
|
||||
|
||||
//const defaultStatus = InvoiceStatus.fromApproved();
|
||||
|
||||
//const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
const series = extractOrPushError(
|
||||
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
|
||||
"series",
|
||||
errors
|
||||
);
|
||||
/**
|
||||
* Complementa los datos legacy con los defaults actuales de proformas.
|
||||
*
|
||||
* La serie legacy `series` no se usa porque la numeracion documental
|
||||
* se resuelve mediante `document-series`.
|
||||
*/
|
||||
|
||||
/*const proformaNumber = extractOrPushError(
|
||||
InvoiceNumber.create(dto.),
|
||||
@ -207,7 +227,7 @@ export class CreateProformaFromFactugesInputMapper
|
||||
);
|
||||
|
||||
const invoiceDate = extractOrPushError(
|
||||
UtcDate.createFromISO(dto.invoice_date),
|
||||
UtcDate.createFromISO(dto.proforma_date),
|
||||
"invoice_date",
|
||||
errors
|
||||
);
|
||||
@ -304,7 +324,8 @@ export class CreateProformaFromFactugesInputMapper
|
||||
|
||||
//invoiceNumber: proformaNumber!,
|
||||
factugesID: factugesID,
|
||||
series: series!,
|
||||
proformaSeriesCode: Maybe.none(),
|
||||
targetInvoiceSeriesCode: Maybe.none(),
|
||||
|
||||
invoiceDate: invoiceDate!,
|
||||
operationDate: operationDate!,
|
||||
@ -324,6 +345,7 @@ export class CreateProformaFromFactugesInputMapper
|
||||
itemsDiscountAmount: itemsDiscountAmount!,
|
||||
|
||||
taxableAmount: taxableAmount!,
|
||||
taxConfig: FACTUGES_DEFAULT_PROFORMA_TAX_CONFIG,
|
||||
taxes: taxes,
|
||||
taxesAmount: taxesAmount!,
|
||||
totalAmount: totalAmount!,
|
||||
@ -607,86 +629,28 @@ export class CreateProformaFromFactugesInputMapper
|
||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
||||
|
||||
private mapItemTaxesProps(
|
||||
itemDTO: CreateProformaItemFromFactugesRequestDTO,
|
||||
_itemDTO: CreateProformaItemFromFactugesRequestDTO,
|
||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||
): ProformaItemTaxesProps {
|
||||
const { itemIndex, errors } = params;
|
||||
const iva = extractOrPushError(
|
||||
this.mapTaxToDomain({
|
||||
code: "iva_21",
|
||||
percentageValue: 21,
|
||||
group: "iva",
|
||||
calculationBehavior: "additive",
|
||||
fieldPath: `items[${params.itemIndex}].iva`,
|
||||
}),
|
||||
`items[${params.itemIndex}].iva`,
|
||||
params.errors
|
||||
);
|
||||
|
||||
const taxesProps: ProformaItemTaxesProps = {
|
||||
iva: Maybe.none(),
|
||||
this.throwIfValidationErrors(params.errors);
|
||||
|
||||
return {
|
||||
iva: iva ?? Maybe.none(),
|
||||
retention: 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: {
|
||||
|
||||
@ -2,10 +2,10 @@ import { type ITransactionManager, isEntityNotFoundError } from "@erp/core/api";
|
||||
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
|
||||
import {
|
||||
type InvoiceAmount,
|
||||
type InvoiceRecipient,
|
||||
InvoiceStatus,
|
||||
type ItemAmount,
|
||||
type Proforma,
|
||||
type ProformaRecipient,
|
||||
} from "@erp/customer-invoices/api/domain";
|
||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||
import {
|
||||
@ -293,43 +293,6 @@ export class CreateProformaFromFactugesUseCase {
|
||||
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: {
|
||||
proformaDraft: FactugesProformaPayload["proformaDraft"];
|
||||
customerId: UniqueID;
|
||||
@ -343,10 +306,9 @@ export class CreateProformaFromFactugesUseCase {
|
||||
const { companyId } = context;
|
||||
|
||||
const defaultStatus = InvoiceStatus.approved();
|
||||
const recipient = Maybe.none<InvoiceRecipient>();
|
||||
const recipient = Maybe.none<ProformaRecipient>();
|
||||
const linkedInvoiceId = Maybe.none<UniqueID>();
|
||||
const paymentMethodId = Maybe.some(payment.id);
|
||||
const paymentTermId = Maybe.none();
|
||||
const taxRegimeCode = Maybe.some("01");
|
||||
|
||||
const items: CreateProformaProps["items"] = proformaDraft.items.map((draftItem) => {
|
||||
@ -382,8 +344,10 @@ export class CreateProformaFromFactugesUseCase {
|
||||
currencyCode: proformaDraft.currencyCode,
|
||||
notes: proformaDraft.notes,
|
||||
operationDate: proformaDraft.operationDate,
|
||||
series: proformaDraft.series,
|
||||
proformaSeriesCode: proformaDraft.proformaSeriesCode,
|
||||
targetInvoiceSeriesCode: proformaDraft.targetInvoiceSeriesCode,
|
||||
reference: proformaDraft.reference,
|
||||
taxConfig: proformaDraft.taxConfig,
|
||||
items,
|
||||
companyId,
|
||||
customerId,
|
||||
|
||||
@ -49,7 +49,7 @@ export const CreateProformaFromFactugesRequestSchema = z.object({
|
||||
reference: z.string().default(""),
|
||||
description: z.string().default(""),
|
||||
|
||||
invoice_date: z.string(),
|
||||
proforma_date: z.string(),
|
||||
operation_date: z.string().default(""),
|
||||
|
||||
notes: z.string().default(""),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user