Borrado de proformas draft
This commit is contained in:
parent
69bfbbb615
commit
718e8b13d9
@ -99,6 +99,12 @@ Adaptando nombres exactos al contrato publico actual:
|
||||
- al existir lineas valoradas, `update` recalcula `taxes` y `totals`
|
||||
- `issue` genera la `issued_invoice` materializada usando `target_invoice_series_code` si existe y, si no, la default activa de `issued_invoice`
|
||||
|
||||
## Relacion con borrado
|
||||
|
||||
- una proforma creada en `draft` puede borrarse logicamente mas adelante
|
||||
- ese borrado no modifica `tax_config`, impuestos, totales ni numeracion historica
|
||||
- la referencia visible consumida por create no vuelve a reutilizarse aunque la proforma se borre
|
||||
|
||||
## Wording de UI
|
||||
|
||||
La UI de create debe hablar de:
|
||||
|
||||
40
docs/customer-invoices/proforma-delete-contract.md
Normal file
40
docs/customer-invoices/proforma-delete-contract.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Proforma Delete Contract
|
||||
|
||||
## Objetivo
|
||||
|
||||
Definir el borrado funcional de proformas sin romper trazabilidad ni numeracion.
|
||||
|
||||
## Reglas funcionales
|
||||
|
||||
- solo se pueden borrar proformas en estado `draft`
|
||||
- `sent`, `approved`, `rejected` e `issued` devuelven conflicto
|
||||
- `rejected` no se borra; queda reservado para una futura operacion de archivado
|
||||
- el borrado es logico mediante `deleted_at`
|
||||
- la numeracion no se reutiliza
|
||||
- no se decrementa `document_series.next_number`
|
||||
- no se borran fisicamente `proforma_items` ni `proforma_taxes`
|
||||
|
||||
## Validaciones defensivas
|
||||
|
||||
- si `linked_invoice_id` tiene valor, el borrado se bloquea
|
||||
- si existe `issued_invoices.source_proforma_id = proforma.id`, el borrado se bloquea
|
||||
|
||||
## Contrato REST
|
||||
|
||||
- endpoint: `DELETE /proformas/:proforma_id`
|
||||
- respuesta satisfactoria: `204 No Content`
|
||||
- `404 Not Found` si no existe, no pertenece a la empresa activa o ya esta borrada
|
||||
- `409 Conflict` si existe pero no cumple las reglas de borrado
|
||||
|
||||
## Efecto operativo
|
||||
|
||||
- `GET /proformas/:id` debe tratar la proforma borrada como inexistente
|
||||
- `GET /proformas` no debe incluir proformas borradas
|
||||
- `PUT /proformas/:id` no debe editar proformas borradas
|
||||
- `POST /proformas/:id/issue` no debe emitir proformas borradas
|
||||
|
||||
## UI
|
||||
|
||||
- la accion `Eliminar` solo debe mostrarse para proformas `draft`
|
||||
- la confirmacion debe advertir que la referencia no se reutilizara
|
||||
- en esta fase la accion solo esta disponible en el listado
|
||||
@ -74,6 +74,13 @@ En proformas:
|
||||
- la UI nunca debe consumir numeracion directa desde `POST /document-series/assign-next`
|
||||
- los impuestos y totales reales se recalculan cuando la proforma ya tiene lineas en update
|
||||
|
||||
## Regla de borrado y numeracion
|
||||
|
||||
- borrar una proforma draft no libera ni reutiliza `proforma_reference`
|
||||
- el siguiente alta consume el siguiente numero disponible de `document-series`
|
||||
- no se decrementa `document_series.next_number`
|
||||
- las proformas `rejected` no se borran; quedan para una futura operacion de archivado
|
||||
|
||||
## Validacion defensiva
|
||||
|
||||
Si el request informa `target_invoice_series_code`:
|
||||
|
||||
@ -165,6 +165,15 @@ Preparar el esquema fisico para separar:
|
||||
- update sigue siendo el punto donde se anaden lineas y se materializan impuestos/totales reales
|
||||
- la UI deja de prometer que la fiscalidad queda "aplicada" en create y pasa a hablar de valores iniciales
|
||||
|
||||
## Estado Fase 2G
|
||||
|
||||
- `DELETE /proformas/:proforma_id` vuelve a estar operativo sobre persistencia V2
|
||||
- el borrado es logico mediante `deleted_at`
|
||||
- solo `draft` puede borrarse
|
||||
- `rejected` sigue fuera del alcance y queda reservado para archivado futuro
|
||||
- la validacion de borrado comprueba tambien `linked_invoice_id` y `issued_invoices.source_proforma_id`
|
||||
- la numeracion de `document-series` no se reutiliza ni se decrementa tras un borrado
|
||||
|
||||
## Rollback recomendado
|
||||
|
||||
- no aplicar nada en produccion en esta fase
|
||||
|
||||
@ -10,6 +10,7 @@ Este directorio mezcla SQL vigentes de soporte con SQL historicos/transicionales
|
||||
- `add-proforma-tax-config-columns.sql`
|
||||
- `fix-proforma-series-semantics.sql`
|
||||
- `validate-proforma-tax-config.sql`
|
||||
- `validate-proforma-soft-delete.sql`
|
||||
|
||||
## SQL históricos / transicionales
|
||||
|
||||
|
||||
14
docs/customer-invoices/sql/validate-proforma-soft-delete.sql
Normal file
14
docs/customer-invoices/sql/validate-proforma-soft-delete.sql
Normal file
@ -0,0 +1,14 @@
|
||||
SELECT id, proforma_reference, status, deleted_at
|
||||
FROM proformas
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND status <> 'draft';
|
||||
|
||||
SELECT id, proforma_reference, deleted_at
|
||||
FROM proformas
|
||||
WHERE deleted_at IS NOT NULL
|
||||
ORDER BY deleted_at DESC
|
||||
LIMIT 20;
|
||||
|
||||
SELECT ds.document_type, ds.code, ds.next_number
|
||||
FROM document_series ds
|
||||
WHERE ds.document_type = 'proforma';
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./proforma-catalog-resolvers.di";
|
||||
export * from "./proforma-creator.di";
|
||||
export * from "./proforma-deleter.di";
|
||||
export * from "./proforma-finder.di";
|
||||
export * from "./proforma-input-mappers.di";
|
||||
export * from "./proforma-issuer.di";
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import { type IProformaDeleter, ProformaDeleter } from "../services";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
export function buildProformaDeleter(deps: { repository: IProformaRepository }): IProformaDeleter {
|
||||
return new ProformaDeleter({
|
||||
repository: deps.repository,
|
||||
});
|
||||
}
|
||||
@ -5,6 +5,7 @@ import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type {
|
||||
ICompanyReportProfileFinder,
|
||||
IProformaDeleter,
|
||||
IProformaCreator,
|
||||
IProformaFinder,
|
||||
IProformaFullReadModelAssembler,
|
||||
@ -23,6 +24,7 @@ import type {
|
||||
import {
|
||||
ChangeStatusProformaUseCase,
|
||||
CreateProformaUseCase,
|
||||
DeleteProformaByIdUseCase,
|
||||
GetProformaByIdUseCase,
|
||||
IssueProformaUseCase,
|
||||
ListProformasUseCase,
|
||||
@ -163,10 +165,15 @@ export function buildUpdateProformaUseCase(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
|
||||
return new DeleteProformaUseCase(deps.finder);
|
||||
}*/
|
||||
export function buildDeleteProformaByIdUseCase(deps: {
|
||||
deleter: IProformaDeleter;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new DeleteProformaByIdUseCase({
|
||||
deleter: deps.deleter,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildChangeStatusProformaUseCase(deps: {
|
||||
statusChanger: IProformaStatusChanger;
|
||||
|
||||
@ -34,6 +34,12 @@ export interface IProformaRepository {
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
existsIssuedInvoiceBySourceProformaIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
updateStatusByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
|
||||
@ -4,6 +4,7 @@ export * from "./company-report-profile-finder";
|
||||
export * from "./proforma-creator";
|
||||
export * from "./proforma-document-generator.interface";
|
||||
export * from "./proforma-document-properties-factory";
|
||||
export * from "./proforma-deleter";
|
||||
export * from "./proforma-finder";
|
||||
export * from "./proforma-issuer";
|
||||
export * from "./proforma-number-generator.interface";
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { ProformaCannotBeDeletedError } from "../../../domain";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
export interface IProformaDeleter {
|
||||
delete(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<void, Error>>;
|
||||
}
|
||||
|
||||
export class ProformaDeleter implements IProformaDeleter {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
repository: IProformaRepository;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async delete(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<void, Error>> {
|
||||
const proformaResult = await this.deps.repository.getByIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const proforma = proformaResult.data;
|
||||
const canDeleteResult = proforma.ensureCanBeDeleted();
|
||||
|
||||
if (canDeleteResult.isFailure) {
|
||||
return Result.fail(canDeleteResult.error);
|
||||
}
|
||||
|
||||
const hasIssuedInvoiceResult =
|
||||
await this.deps.repository.existsIssuedInvoiceBySourceProformaIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction
|
||||
);
|
||||
|
||||
if (hasIssuedInvoiceResult.isFailure) {
|
||||
return Result.fail(hasIssuedInvoiceResult.error);
|
||||
}
|
||||
|
||||
if (hasIssuedInvoiceResult.data) {
|
||||
return Result.fail(
|
||||
new ProformaCannotBeDeletedError(params.id.toString(), "Issued proformas cannot be deleted.")
|
||||
);
|
||||
}
|
||||
|
||||
const deleteResult = await this.deps.repository.deleteByIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction
|
||||
);
|
||||
|
||||
if (deleteResult.isFailure) {
|
||||
return Result.fail(deleteResult.error);
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IProformaDeleter } from "../services";
|
||||
|
||||
type DeleteProformaByIdUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
};
|
||||
|
||||
export class DeleteProformaByIdUseCase {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
deleter: IProformaDeleter;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: DeleteProformaByIdUseCaseInput) {
|
||||
const proformaIdResult = UniqueID.create(params.proforma_id);
|
||||
|
||||
if (proformaIdResult.isFailure) {
|
||||
return Result.fail(proformaIdResult.error);
|
||||
}
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
return await this.deps.deleter.delete({
|
||||
companyId: params.companyId,
|
||||
id: proformaIdResult.data,
|
||||
transaction,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
export * from "./change-status-proforma.use-case";
|
||||
export * from "./create-proforma.use-case";
|
||||
//export * from "./delete-proforma.use-case";
|
||||
export * from "./delete-proforma-by-id.use-case";
|
||||
export * from "./get-proforma-by-id.use-case";
|
||||
export * from "./issue-proforma.use-case";
|
||||
export * from "./list-proformas.use-case";
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
type ProformaRecipient,
|
||||
type ProformaTaxConfig,
|
||||
} from "../value-objects";
|
||||
import { ProformaCannotBeDeletedError } from "../errors";
|
||||
|
||||
export interface IProformaCreateProps {
|
||||
companyId: UniqueID;
|
||||
@ -386,6 +387,30 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public ensureCanBeDeleted(): Result<void, Error> {
|
||||
if (!this.status.isDraft()) {
|
||||
return Result.fail(
|
||||
new ProformaCannotBeDeletedError(
|
||||
this.id.toString(),
|
||||
this.status.isIssued()
|
||||
? "Issued proformas cannot be deleted."
|
||||
: "Only draft proformas can be deleted."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.linkedInvoiceId.isSome()) {
|
||||
return Result.fail(
|
||||
new ProformaCannotBeDeletedError(
|
||||
this.id.toString(),
|
||||
"Issued proformas cannot be deleted."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private validateCanBeIssued(): Result<void, Error> {
|
||||
if (this.description.isNone()) {
|
||||
return Result.fail(
|
||||
|
||||
@ -8,5 +8,6 @@ export class ProformaCannotBeDeletedError extends DomainError {
|
||||
this.name = "ProformaCannotBeDeletedError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isProformaCannotBeDeletedError = (e: unknown): e is ProformaCannotBeDeletedError =>
|
||||
e instanceof ProformaCannotBeDeletedError;
|
||||
|
||||
@ -6,6 +6,7 @@ import type { ICompanyPublicServices } from "../../../../../../companies/src/api
|
||||
import {
|
||||
type ChangeStatusProformaUseCase,
|
||||
type CreateProformaUseCase,
|
||||
type DeleteProformaByIdUseCase,
|
||||
type GetProformaByIdUseCase,
|
||||
type IIssuedInvoicePublicServices,
|
||||
type IssueProformaUseCase,
|
||||
@ -15,12 +16,14 @@ import {
|
||||
type UpdateProformaByIdUseCase,
|
||||
buildChangeStatusProformaUseCase,
|
||||
buildCreateProformaUseCase,
|
||||
buildDeleteProformaByIdUseCase,
|
||||
buildGetProformaByIdUseCase,
|
||||
buildIssueProformaUseCase,
|
||||
buildListProformasUseCase,
|
||||
buildPreviewProformaReportUseCase,
|
||||
buildProformaCatalogResolvers,
|
||||
buildProformaCreator,
|
||||
buildProformaDeleter,
|
||||
buildProformaFinder,
|
||||
buildProformaInputMappers,
|
||||
buildProformaIssueReadModelAssembler,
|
||||
@ -53,11 +56,7 @@ export type ProformasInternalDeps = {
|
||||
}) => IssueProformaUseCase;
|
||||
updateProforma: () => UpdateProformaByIdUseCase;
|
||||
changeStatusProforma: () => ChangeStatusProformaUseCase;
|
||||
|
||||
/*
|
||||
deleteProforma: () => DeleteProformaUseCase;
|
||||
|
||||
*/
|
||||
deleteProforma: () => DeleteProformaByIdUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
@ -120,6 +119,9 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
paymentResolver: catalogResolvers.paymentResolver,
|
||||
targetInvoiceSeriesValidator,
|
||||
});
|
||||
const deleter = buildProformaDeleter({
|
||||
repository,
|
||||
});
|
||||
|
||||
const statusChanger = buildProformaStatusChanger({
|
||||
repository,
|
||||
@ -177,6 +179,12 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
deleteProforma: () =>
|
||||
buildDeleteProformaByIdUseCase({
|
||||
deleter,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
reportProformaPdf: () =>
|
||||
buildReportProformaPdfUseCase({
|
||||
finder,
|
||||
|
||||
@ -5,11 +5,11 @@ import {
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { DeleteProformaUseCase } from "../../../../application/index.ts";
|
||||
import type { DeleteProformaByIdUseCase } from "../../../../application/index.ts";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class DeleteProformaController extends ExpressController {
|
||||
public constructor(private readonly useCase: DeleteProformaUseCase) {
|
||||
public constructor(private readonly useCase: DeleteProformaByIdUseCase) {
|
||||
super();
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
@ -35,7 +35,7 @@ export class DeleteProformaController extends ExpressController {
|
||||
const result = await this.useCase.execute({ proforma_id, companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
() => this.noContent(),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//export * from "./change-status-proforma.controller";
|
||||
//export * from "./create-proforma.controller";
|
||||
//export * from "./delete-proforma.controller";
|
||||
export * from "./delete-proforma.controller";
|
||||
export * from "./get-proforma-by-id.controller";
|
||||
export * from "./issue-proforma.controller";
|
||||
export * from "./list-proformas.controller";
|
||||
|
||||
@ -90,8 +90,8 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isProformaCannotBeDeletedError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted."
|
||||
new ConflictApiError(
|
||||
(e as Error).message || "Proforma cannot be deleted."
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { requireIdentityTenant } from "@erp/identity/api";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import {
|
||||
DeleteProformaController,
|
||||
GetProformaByIdController,
|
||||
IssueProformaController,
|
||||
ListProformasController,
|
||||
@ -15,6 +16,7 @@ import {
|
||||
ChangeStatusProformaByIdParamsRequestSchema,
|
||||
ChangeStatusProformaByIdRequestSchema,
|
||||
CreateProformaRequestSchema,
|
||||
DeleteProformaByIdParamsRequestSchema,
|
||||
GetProformaByIdRequestSchema,
|
||||
IssueProformaByIdParamsRequestSchema,
|
||||
LegacyIssueProformaByIdParamsRequestSchema,
|
||||
@ -117,17 +119,15 @@ export const proformasRouter = (params: StartParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
/*router.delete(
|
||||
router.delete(
|
||||
"/:proforma_id",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(DeleteProformaByIdParamsRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.delete_proforma();
|
||||
const useCase = deps.useCases.deleteProforma();
|
||||
const controller = new DeleteProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);*/
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:proforma_id/status",
|
||||
|
||||
@ -140,6 +140,26 @@ export class SequelizeProformaRepositoryV2
|
||||
}
|
||||
}
|
||||
|
||||
async existsIssuedInvoiceBySourceProformaIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const count = await IssuedInvoiceModel.count({
|
||||
where: {
|
||||
company_id: companyId.toString(),
|
||||
source_proforma_id: id.toString(),
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
return Result.ok(count > 0);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatusByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
CustomerInvoiceItemModel,
|
||||
CustomerInvoiceModel,
|
||||
CustomerInvoiceTaxModel,
|
||||
IssuedInvoiceModel,
|
||||
} from "../../../../common";
|
||||
import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers";
|
||||
|
||||
@ -214,6 +215,26 @@ export class ProformaRepository
|
||||
}
|
||||
}
|
||||
|
||||
async existsIssuedInvoiceBySourceProformaIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const count = await IssuedInvoiceModel.count({
|
||||
where: {
|
||||
company_id: companyId.toString(),
|
||||
source_proforma_id: id.toString(),
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
return Result.ok(count > 0);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Actualiza el "status" de una proforma
|
||||
|
||||
@ -7,7 +7,7 @@ import { z } from "zod/v4";
|
||||
*/
|
||||
|
||||
export const DeleteProformaByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
proforma_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type DeleteProformaByIdRequestDTO = z.infer<typeof DeleteProformaByIdParamsRequestSchema>;
|
||||
|
||||
@ -110,16 +110,23 @@
|
||||
"unknown_error": "An unexpected error occurred"
|
||||
},
|
||||
"delete_proforma_dialog": {
|
||||
"single_title": "Delete proforma {{reference}}",
|
||||
"single_description": "Are you sure you want to delete this proforma? This action cannot be undone.",
|
||||
"second_confirm_single_title": "Additional confirmation",
|
||||
"second_confirm_single_description": "You are about to delete the proforma. This action cannot be undone. Are you sure you want to continue?",
|
||||
"multiple_description": "Are you sure you want to delete the {{total}} selected proformas? This action cannot be undone.",
|
||||
"single_title": "Delete proforma",
|
||||
"single_description": "This proforma will be deleted. Its reference will not be reused.",
|
||||
"multiple_title": "Delete proformas",
|
||||
"multiple_description": "{{count}} draft proformas will be deleted. Their references will not be reused.",
|
||||
"warning": "This action does not delete issued invoices and does not reuse numbering.",
|
||||
"confirm_delete": "Confirm deletion",
|
||||
"deleting": "Deleting...",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Delete",
|
||||
"list_item": "Proforma {{reference}}"
|
||||
"list_item": "Proforma {{reference}}",
|
||||
"success_title": "Proforma deleted",
|
||||
"success_single_message": "Proforma {{reference}} has been deleted successfully.",
|
||||
"success_multiple_message": "{{count}} proformas have been deleted successfully.",
|
||||
"error_title": "Unable to delete the proforma",
|
||||
"error_single_message": "The selected proforma could not be deleted.",
|
||||
"error_multiple_message": "{{count}} proformas could not be deleted.",
|
||||
"error_conflict_message": "Only draft proformas can be deleted.",
|
||||
"error_not_found_message": "The proforma no longer exists or is not available."
|
||||
},
|
||||
"change_proforma_status_dialog": {
|
||||
"title": "Change proforma status",
|
||||
@ -190,6 +197,9 @@
|
||||
"title": "Delete customer proforma",
|
||||
"description": "Delete the selected customer proforma"
|
||||
},
|
||||
"delete.error_title": "Unable to delete the proforma",
|
||||
"delete.error_unexpected_message": "An unexpected error occurred while deleting the proforma."
|
||||
,
|
||||
"view": {
|
||||
"title": "View customer proforma",
|
||||
"description": "View the details of the selected customer proforma"
|
||||
|
||||
@ -111,16 +111,23 @@
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"delete_proforma_dialog": {
|
||||
"single_title": "Eliminar proforma {{reference}}",
|
||||
"single_description": "¿Seguro que deseas eliminar esta proforma? Esta acción no se puede deshacer.",
|
||||
"second_confirm_single_title": "Confirmación adicional",
|
||||
"second_confirm_single_description": "Estás a punto de borrar la proforma. Esta acción masiva no se puede deshacer. ¿Seguro que quieres continuar?",
|
||||
"multiple_description": "¿Seguro que deseas eliminar las {{total}} proformas seleccionadas? Esta acción no se puede deshacer.",
|
||||
"single_title": "Eliminar proforma",
|
||||
"single_description": "Se eliminará la proforma. Su referencia no se reutilizará.",
|
||||
"multiple_title": "Eliminar proformas",
|
||||
"multiple_description": "Se eliminarán {{count}} proformas draft. Sus referencias no se reutilizarán.",
|
||||
"warning": "Esta acción no elimina facturas emitidas ni reutiliza numeración.",
|
||||
"confirm_delete": "Confirmar eliminación",
|
||||
"deleting": "Eliminando...",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Eliminar",
|
||||
"list_item": "Proforma {{reference}}"
|
||||
"list_item": "Proforma {{reference}}",
|
||||
"success_title": "Proforma eliminada",
|
||||
"success_single_message": "La proforma {{reference}} se ha eliminado correctamente.",
|
||||
"success_multiple_message": "Se han eliminado {{count}} proformas correctamente.",
|
||||
"error_title": "No se pudo eliminar la proforma",
|
||||
"error_single_message": "La proforma seleccionada no se ha podido eliminar.",
|
||||
"error_multiple_message": "{{count}} proformas no se han podido eliminar.",
|
||||
"error_conflict_message": "Solo se pueden eliminar proformas en borrador.",
|
||||
"error_not_found_message": "La proforma ya no existe o no está disponible."
|
||||
},
|
||||
"change_proforma_status_dialog": {
|
||||
"title": "Cambiar estado de la proforma",
|
||||
@ -191,6 +198,9 @@
|
||||
"title": "Eliminar proforma",
|
||||
"description": "Eliminar la proforma seleccionada"
|
||||
},
|
||||
"delete.error_title": "No se pudo eliminar la proforma",
|
||||
"delete.error_unexpected_message": "Ha ocurrido un error inesperado al eliminar la proforma."
|
||||
,
|
||||
"view": {
|
||||
"title": "Ver proforma",
|
||||
"description": "Ver los detalles de la proforma seleccionada"
|
||||
|
||||
@ -6,23 +6,20 @@ import { useTranslation } from "../../../i18n";
|
||||
import type { DeleteProformaByIdParams } from "../../shared";
|
||||
import { useProformaDeleteMutation } from "../../shared";
|
||||
import type { DeleteProformaTarget } from "../entities";
|
||||
|
||||
type ConfirmStep = "initial" | "second";
|
||||
type DeleteErrorWithStatus = Error & { status?: number };
|
||||
|
||||
interface DeleteProformaDialogState {
|
||||
open: boolean;
|
||||
targets: DeleteProformaTarget[];
|
||||
confirmStep: ConfirmStep;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: DeleteProformaDialogState = {
|
||||
open: false,
|
||||
targets: [],
|
||||
confirmStep: "initial",
|
||||
};
|
||||
|
||||
const buildDeleteParams = (target: DeleteProformaTarget): DeleteProformaByIdParams => ({
|
||||
id: target.id,
|
||||
proformaId: target.id,
|
||||
});
|
||||
|
||||
const getProformaLabel = (target: DeleteProformaTarget) => target.reference || `#${target.id}`;
|
||||
@ -39,7 +36,6 @@ export const useDeleteProformaDialogController = () => {
|
||||
setState({
|
||||
open: true,
|
||||
targets,
|
||||
confirmStep: "initial",
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -48,13 +44,6 @@ export const useDeleteProformaDialogController = () => {
|
||||
setState(INITIAL_STATE);
|
||||
}, [deleteMutation.isPending]);
|
||||
|
||||
const moveToSecondConfirmStep = React.useCallback(() => {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
confirmStep: "second",
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const notifySuccess = React.useCallback(
|
||||
(targets: DeleteProformaTarget[]) => {
|
||||
if (targets.length === 1) {
|
||||
@ -77,12 +66,38 @@ export const useDeleteProformaDialogController = () => {
|
||||
[t]
|
||||
);
|
||||
|
||||
const getDeleteErrorMessage = React.useCallback(
|
||||
(error: unknown) => {
|
||||
const status = (error as DeleteErrorWithStatus | undefined)?.status;
|
||||
|
||||
if (status === 404) {
|
||||
return t(
|
||||
"proformas.delete_proforma_dialog.error_not_found_message",
|
||||
"La proforma ya no existe o no está disponible."
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 409) {
|
||||
return t(
|
||||
"proformas.delete_proforma_dialog.error_conflict_message",
|
||||
"Solo se pueden eliminar proformas en borrador."
|
||||
);
|
||||
}
|
||||
|
||||
return t(
|
||||
"proformas.delete.error_unexpected_message",
|
||||
"Ha ocurrido un error inesperado al eliminar la proforma."
|
||||
);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const notifyPartialError = React.useCallback(
|
||||
(targets: DeleteProformaTarget[], errorCount: number) => {
|
||||
(targets: DeleteProformaTarget[], errorCount: number, firstError?: unknown) => {
|
||||
if (targets.length === 1) {
|
||||
showErrorToast(
|
||||
t("proformas.delete_proforma_dialog.error_title"),
|
||||
t("proformas.delete_proforma_dialog.error_single_message")
|
||||
getDeleteErrorMessage(firstError)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -94,7 +109,7 @@ export const useDeleteProformaDialogController = () => {
|
||||
})
|
||||
);
|
||||
},
|
||||
[t]
|
||||
[getDeleteErrorMessage, t]
|
||||
);
|
||||
|
||||
const submitDelete = React.useCallback(async () => {
|
||||
@ -106,6 +121,9 @@ export const useDeleteProformaDialogController = () => {
|
||||
|
||||
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||
const errorCount = results.length - successCount;
|
||||
const firstRejectedResult = results.find((result) => result.status === "rejected");
|
||||
const firstError =
|
||||
firstRejectedResult?.status === "rejected" ? firstRejectedResult.reason : undefined;
|
||||
|
||||
if (successCount > 0) {
|
||||
const fulfilledTargets = targets.filter((_, index) => results[index]?.status === "fulfilled");
|
||||
@ -116,13 +134,13 @@ export const useDeleteProformaDialogController = () => {
|
||||
}
|
||||
|
||||
if (rejectedTargets.length > 0) {
|
||||
notifyPartialError(targets, rejectedTargets.length);
|
||||
notifyPartialError(targets, rejectedTargets.length, firstError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
notifyPartialError(targets, errorCount);
|
||||
notifyPartialError(targets, errorCount, firstError);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -134,11 +152,6 @@ export const useDeleteProformaDialogController = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.confirmStep === "initial") {
|
||||
moveToSecondConfirmStep();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await submitDelete();
|
||||
} catch {
|
||||
@ -147,13 +160,12 @@ export const useDeleteProformaDialogController = () => {
|
||||
t("proformas.delete.error_unexpected_message")
|
||||
);
|
||||
}
|
||||
}, [deleteMutation.isPending, moveToSecondConfirmStep, state, submitDelete, t]);
|
||||
}, [deleteMutation.isPending, state, submitDelete, t]);
|
||||
|
||||
return {
|
||||
open: state.open,
|
||||
targets: state.targets,
|
||||
isSubmitting: deleteMutation.isPending,
|
||||
isSecondConfirmStep: state.confirmStep === "second",
|
||||
isBulkDelete: state.targets.length > 1,
|
||||
|
||||
openDialog,
|
||||
|
||||
@ -18,7 +18,6 @@ interface DeleteProformaDialogProps {
|
||||
targets: DeleteProformaTarget[];
|
||||
isSubmitting: boolean;
|
||||
onConfirm: () => void;
|
||||
isSecondConfirmStep: boolean;
|
||||
}
|
||||
|
||||
const getTargetLabel = (target: DeleteProformaTarget) => target.reference || `#${target.id}`;
|
||||
@ -29,7 +28,6 @@ export const DeleteProformaDialog = ({
|
||||
targets,
|
||||
isSubmitting,
|
||||
onConfirm,
|
||||
isSecondConfirmStep,
|
||||
}: DeleteProformaDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -37,15 +35,7 @@ export const DeleteProformaDialog = ({
|
||||
const isSingle = total === 1;
|
||||
const firstTarget = targets[0];
|
||||
|
||||
const title = isSecondConfirmStep
|
||||
? isSingle
|
||||
? t("proformas.delete_proforma_dialog.second_confirm_single_title", {
|
||||
reference: getTargetLabel(firstTarget),
|
||||
})
|
||||
: t("proformas.delete_proforma_dialog.second_confirm_multiple_title", {
|
||||
count: total,
|
||||
})
|
||||
: isSingle
|
||||
const title = isSingle
|
||||
? t("proformas.delete_proforma_dialog.single_title", {
|
||||
reference: getTargetLabel(firstTarget),
|
||||
})
|
||||
@ -53,13 +43,7 @@ export const DeleteProformaDialog = ({
|
||||
count: total,
|
||||
});
|
||||
|
||||
const description = isSecondConfirmStep
|
||||
? isSingle
|
||||
? t("proformas.delete_proforma_dialog.second_confirm_single_description")
|
||||
: t("proformas.delete_proforma_dialog.second_confirm_multiple_description", {
|
||||
count: total,
|
||||
})
|
||||
: isSingle
|
||||
const description = isSingle
|
||||
? t("proformas.delete_proforma_dialog.single_description")
|
||||
: t("proformas.delete_proforma_dialog.multiple_description", {
|
||||
count: total,
|
||||
@ -79,7 +63,11 @@ export const DeleteProformaDialog = ({
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{!isSecondConfirmStep && total > 1 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proformas.delete_proforma_dialog.warning")}
|
||||
</p>
|
||||
|
||||
{total > 1 ? (
|
||||
<div className="mt-4 max-h-48 overflow-y-auto rounded-md border p-3 text-sm">
|
||||
<ul className="space-y-1">
|
||||
{targets.map((target) => (
|
||||
@ -104,10 +92,8 @@ export const DeleteProformaDialog = ({
|
||||
<Spinner className="mr-2 size-4" />
|
||||
{t("proformas.delete_proforma_dialog.deleting")}
|
||||
</>
|
||||
) : isSecondConfirmStep ? (
|
||||
t("proformas.delete_proforma_dialog.confirm_delete")
|
||||
) : (
|
||||
t("proformas.delete_proforma_dialog.continue")
|
||||
t("proformas.delete_proforma_dialog.confirm_delete")
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@ -275,6 +275,7 @@ export function useProformasGridColumns(
|
||||
|
||||
cell: ({ row }) => {
|
||||
const proforma = row.original;
|
||||
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
|
||||
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
|
||||
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
|
||||
const availableTransitions =
|
||||
@ -396,7 +397,7 @@ export function useProformasGridColumns(
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Eliminar */}
|
||||
{!isIssued && actionHandlers.onDeleteClick && (
|
||||
{isDraft && actionHandlers.onDeleteClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
|
||||
@ -332,7 +332,6 @@ export const ListProformasPage = () => {
|
||||
|
||||
{/* Eliminar */}
|
||||
<DeleteProformaDialog
|
||||
isSecondConfirmStep={deleteDialogCtrl.isSecondConfirmStep}
|
||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@ -10,7 +10,7 @@ import type { IDataSource } from "@erp/core/client";
|
||||
*/
|
||||
|
||||
export interface DeleteProformaByIdParams {
|
||||
id: string;
|
||||
proformaId: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
@ -20,9 +20,9 @@ export const deleteProformaById = (
|
||||
dataSource: IDataSource,
|
||||
params: DeleteProformaByIdParams
|
||||
): Promise<DeleteProformaByIdResult> => {
|
||||
const { id, signal } = params;
|
||||
const { proformaId, signal } = params;
|
||||
|
||||
if (!id) throw new Error("proformaId is required");
|
||||
if (!proformaId) throw new Error("proformaId is required");
|
||||
|
||||
return dataSource.deleteOne("proformas", id, { signal });
|
||||
return dataSource.deleteOne("proformas", proformaId, { signal });
|
||||
};
|
||||
|
||||
@ -18,27 +18,32 @@ export const useProformaDeleteMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
|
||||
return useMutation<{ id: string }, DefaultError, DeleteProformaByIdParams, DeleteProformaContext>(
|
||||
return useMutation<
|
||||
{ proformaId: string },
|
||||
DefaultError,
|
||||
DeleteProformaByIdParams,
|
||||
DeleteProformaContext
|
||||
>(
|
||||
{
|
||||
mutationKey: DELETE_PROFORMA_MUTATION_KEY,
|
||||
mutationFn: async ({ id, signal }) => {
|
||||
if (!id) {
|
||||
mutationFn: async ({ proformaId, signal }) => {
|
||||
if (!proformaId) {
|
||||
throw new Error("proformaId is required");
|
||||
}
|
||||
|
||||
await deleteProformaById(dataSource, { id, signal });
|
||||
return { id };
|
||||
await deleteProformaById(dataSource, { proformaId, signal });
|
||||
return { proformaId };
|
||||
},
|
||||
onMutate: async ({ id }) => {
|
||||
return prepareDeleteProformaOptimisticUpdate(queryClient, id);
|
||||
onMutate: async ({ proformaId }) => {
|
||||
return prepareDeleteProformaOptimisticUpdate(queryClient, proformaId);
|
||||
},
|
||||
|
||||
onError: (_error, _params, context) => {
|
||||
rollbackDeleteProformaOptimisticUpdate(queryClient, context);
|
||||
},
|
||||
|
||||
onSettled: async (_data, _error, { id }) => {
|
||||
await finalizeDeletedProformaCaches(queryClient, id);
|
||||
onSettled: async (_data, _error, { proformaId }) => {
|
||||
await finalizeDeletedProformaCaches(queryClient, proformaId);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user