Files
Investidor-V2/resources/js/lib/iva-offer-preparations.ts
T

483 lines
19 KiB
TypeScript

import { inputText } from './input-text.ts';
import { acquisitionBook, acquisitionScope } from './iva-acquisitions.ts';
import {
importExemptionBook,
isImportService,
} from './iva-import-exemptions.ts';
import {
offerWorkflowApplied,
offerWorkflowReport,
type OfferWorkflow,
} from './iva-offer-workflow.ts';
type Choice = '' | 'yes' | 'no';
export const offerPreparationSource =
'CIVA,Lei13/2016,14(2)(a/b) e14(3),PDF13/p.1426(271). Procedimento operacional definido pela Tempo25/10/2026,distinto das fontes legais;correspondência específica do formulário ainda por demonstrar. Não concede isenção nem preenche dedução/campo11.';
export type OfferRequest = {
state: 'preparation' | 'presented' | 'granted' | 'denied';
reference: string;
presentedOn: string;
decisionRef: string;
decisionOn: string;
goods: string;
operation: '' | 'acquisition' | 'import';
periodFrom: string;
periodTo: string;
conditions: string;
operationScope?: string;
periodScope?: string;
};
export const offerRequestLabels = {
preparation: 'Em preparação',
presented: 'Apresentado',
granted: 'Deferido',
denied: 'Indeferido',
};
export type OfferPreparation = {
workflow?: OfferWorkflow;
id: string;
branch: 'P084' | 'P085';
operation: '' | 'acquisition' | 'import';
description: string;
period: string;
currency: string;
amount: string;
counterparty: string;
documentRef: string;
documentDate: string;
beneficiary: string;
purpose: string;
evidence: string;
nationalInstitution: Choice;
publicSocialInstitution: Choice;
goodsSuitable: Choice;
publicInterestUse: Choice;
disaster: string;
disasterKind: string;
mitigation: Choice;
reusedFrom?: { key: string; copiedAt: string };
humanitarianPurpose?: string;
availableDocuments?: string;
request?: OfferRequest;
requestRecords?: Array<{ recordedAt: string; request: OfferRequest }>;
normalVat?: string;
normalVatSource?: string;
conditionalAssumption?: Choice;
};
export type OfferPreparationBook = {
rows: OfferPreparation[];
history?: Array<{ savedAt: string; rows: OfferPreparation[] }>;
saved?: { savedAt: string; rows: OfferPreparation[] };
};
type Store = {
version: 1;
books: Record<string, unknown>;
offerPreparations?: Record<string, OfferPreparationBook>;
[key: string]: unknown;
};
export function newOfferPreparation(
id: string,
branch: OfferPreparation['branch'],
currency: string,
): OfferPreparation {
return {
id,
branch,
operation: branch === 'P085' ? 'acquisition' : '',
description: '',
period: '',
currency,
amount: '',
counterparty: '',
documentRef: '',
documentDate: '',
beneficiary: '',
purpose: '',
evidence: '',
nationalInstitution: '',
publicSocialInstitution: '',
goodsSuitable: '',
publicInterestUse: '',
disaster: '',
disasterKind: '',
mitigation: '',
};
}
function read(draft: Record<string, unknown>): Store {
const value: Store = draft.modelo_a_snapshot
? JSON.parse(inputText(draft.modelo_a_snapshot))
: { version: 1, books: {} };
if (
value?.version !== 1 ||
!value.books ||
typeof value.books !== 'object' ||
Array.isArray(value.books)
)
throw new Error(
'Dados fiscais não recuperáveis; originais preservados.',
);
if (
value.offerPreparations !== undefined &&
(!value.offerPreparations ||
typeof value.offerPreparations !== 'object' ||
Array.isArray(value.offerPreparations))
)
throw new Error(
'Rascunhos de ofertas não recuperáveis; sem substituição por lista vazia.',
);
for (const book of Object.values(value.offerPreparations ?? {})) {
if (
!book ||
!Array.isArray(book.rows) ||
(book.history !== undefined && !Array.isArray(book.history))
)
throw new Error(
'Formato de rascunhos de ofertas não recuperável; originais preservados.',
);
for (const row of book.rows) {
if (
!row ||
!['P084', 'P085'].includes(row.branch) ||
Object.keys(newOfferPreparation('', 'P084', '')).some(
(key) =>
typeof row[key as keyof OfferPreparation] !== 'string',
) ||
(row.branch === 'P085' && row.operation === 'import')
)
throw new Error(
'Rascunho de oferta não recuperável ou operação fora do ramo; dados conservados.',
);
if (
[
'humanitarianPurpose',
'availableDocuments',
'normalVat',
'normalVatSource',
'conditionalAssumption',
].some(
(key) =>
row[key as keyof OfferPreparation] !== undefined &&
typeof row[key as keyof OfferPreparation] !== 'string',
) ||
(row.requestRecords !== undefined &&
!Array.isArray(row.requestRecords))
)
throw new Error(
'Elementos do percurso de ofertas não recuperáveis; originais preservados.',
);
if (
row.workflow !== undefined &&
(!row.workflow ||
!Array.isArray(row.workflow.records) ||
[
'recipient',
'writtenRequest',
'decisionIssuer',
'scopeConfirmed',
'formName',
'formSource',
'formContent',
'submission',
'submissionReference',
].some(
(k) =>
typeof row.workflow?.[k as keyof OfferWorkflow] !==
'string',
))
)
throw new Error(
'Percurso pedido/decisão/formulário não recuperável;originais preservados.',
);
if (
row.request !== undefined &&
(!row.request ||
!Object.hasOwn(offerRequestLabels, row.request.state) ||
Object.entries(emptyOfferRequest()).some(
([key]) =>
typeof row.request?.[key as keyof OfferRequest] !==
'string',
))
)
throw new Error(
'Pedido de oferta não recuperável; decisão e originais conservados.',
);
}
}
return value;
}
export function offerPreparationBook(
draft: Record<string, unknown>,
): OfferPreparationBook {
return (
read(draft).offerPreparations?.[acquisitionScope(draft)] ?? { rows: [] }
);
}
export function saveOfferPreparationBook(
draft: Record<string, unknown>,
book: OfferPreparationBook,
): string {
const previous = read(draft);
const next = {
...previous,
offerPreparations: {
...previous.offerPreparations,
[acquisitionScope(draft)]: book,
},
};
const encoded = JSON.stringify(next);
read({ ...draft, modelo_a_snapshot: encoded });
return encoded;
}
export function recordOfferPreparations(
book: OfferPreparationBook,
): OfferPreparationBook {
if (
!book.rows.length ||
new Set(book.rows.map((r) => r.id)).size !== book.rows.length
)
throw new Error('Indique rascunhos com identificadores distintos.');
return {
...book,
history: [
...(book.history ?? []),
...(book.saved ? [structuredClone(book.saved)] : []),
],
saved: {
savedAt: new Date().toISOString(),
rows: structuredClone(book.rows),
},
};
}
export type OfferReusable = {
key: string;
label: string;
operation: 'acquisition' | 'import';
values: Partial<OfferPreparation>;
};
export function offerReusableData(
draft: Record<string, unknown>,
): OfferReusable[] {
const purchases = acquisitionBook(draft).rows.map((row) => ({
key: 'acquisition:' + row.id,
label: 'Aquisição: ' + (row.description || row.documentRef || row.id),
operation: 'acquisition' as const,
values: {
description: row.description,
period: row.period,
currency: row.sourceCurrency,
amount: row.net,
documentRef: row.documentRef,
documentDate: row.documentDate,
counterparty: row.supplier,
},
}));
const imports = importExemptionBook(draft)
.rows.filter((row) => !isImportService(row.treatment))
.map((row) => ({
key: 'import:' + row.id,
label:
'Importação: ' + (row.description || row.documentRef || row.id),
operation: 'import' as const,
values: {
description: row.description,
period: row.period,
currency: row.currency,
amount: row.declaredValue,
documentRef: row.documentRef,
documentDate: row.documentDate,
counterparty: row.importer,
},
}));
return [...purchases, ...imports];
}
export function reuseOfferData(
row: OfferPreparation,
source: OfferReusable,
): OfferPreparation {
if (row.branch === 'P085' && source.operation === 'import')
throw new Error(
'P085 abrange aquisição; esta referência de importação conserva-se no seu percurso.',
);
const next = { ...row };
if (
next.currency &&
source.values.currency &&
next.currency !== source.values.currency
)
throw new Error(
'Moeda da referência divergente; valores e edições conservados, sem conversão.',
);
// Only fill empty inputs. Deliberate edits and answers are never replaced.
if (!next.operation) next.operation = source.operation;
if (next.operation !== source.operation)
throw new Error(
'A operação já escolhida diverge da origem; rascunho conservado.',
);
for (const [key, value] of Object.entries(source.values))
if (!next[key as keyof OfferPreparation] && typeof value === 'string')
Object.assign(next, { [key]: value });
return {
...next,
reusedFrom: { key: source.key, copiedAt: new Date().toISOString() },
};
}
export function emptyOfferRequest(): OfferRequest {
return {
state: 'preparation',
reference: '',
presentedOn: '',
decisionRef: '',
decisionOn: '',
goods: '',
operation: '',
periodFrom: '',
periodTo: '',
conditions: '',
};
}
export function recordOfferRequest(row: OfferPreparation): OfferPreparation {
const request = row.request ?? emptyOfferRequest();
const validPeriod = (value: string) => {
if (/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) return true;
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const d = new Date(value + 'T00:00:00Z');
return (
Number.isFinite(d.getTime()) &&
d.toISOString().slice(0, 10) === value
);
};
const temporalScope =
!!request.periodScope?.trim() ||
(validPeriod(request.periodFrom) &&
validPeriod(request.periodTo) &&
request.periodFrom.length === request.periodTo.length &&
request.periodTo >= request.periodFrom);
if (
['granted', 'denied'].includes(request.state) &&
(!request.goods.trim() ||
(!['acquisition', 'import'].includes(request.operation) &&
!request.operationScope?.trim()) ||
!temporalScope ||
!request.conditions.trim())
)
throw new Error(
'Conserve o âmbito da decisão: bens, operação, período e condições. Pode usar datas, meses ou o âmbito temporal literal do documento, inclusive sem termo. Pode continuar a guardar o rascunho incompleto.',
);
return {
...row,
requestRecords: [
...(row.requestRecords ?? []),
{
recordedAt: new Date().toISOString(),
request: structuredClone(request),
},
],
};
}
export function offerScenarios(row: OfferPreparation): {
normalVat: string | null;
conditionalVat: string | null;
conditionalReason: string;
applied: boolean;
appliedVat?: string;
} {
const normalVat =
row.normalVatSource?.trim() &&
/^\d{1,15}(\.\d{1,6})?$/.test(row.normalVat ?? '')
? row.normalVat!
: null;
const result = {
normalVat,
conditionalVat: null as string | null,
conditionalReason:
'Hipótese de isenção não assumida; benefício não aplicado.',
applied: false as const,
};
const workflow = offerWorkflowApplied(row);
if (workflow.applied)
return {
...result,
applied: true,
appliedVat: '0.00',
conditionalReason: workflow.reason,
};
if (row.conditionalAssumption !== 'yes') return result;
const choices =
row.branch === 'P084'
? [
row.nationalInstitution,
row.publicSocialInstitution,
row.goodsSuitable,
row.publicInterestUse,
]
: [row.mitigation];
if (choices.includes('no'))
return {
...result,
conditionalReason:
'Condição material contrariada; este cenário de isenção não está demonstrado. Benefício não aplicado.',
};
if (
choices.some((value) => value !== 'yes') ||
!row.description.trim() ||
!row.beneficiary.trim() ||
!row.purpose.trim() ||
!row.operation ||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
row.currency !== 'MZN' ||
(row.branch === 'P085' &&
(!row.disaster.trim() || !row.humanitarianPurpose?.trim()))
)
return {
...result,
conditionalReason:
'Pressupostos, contexto ou condições materiais por indicar; sem IVA zero presumido. Benefício não aplicado.',
};
if (row.workflow)
return {
...result,
conditionalVat: '0.00',
conditionalReason:
'Cenário condicionado à isenção antes de despacho favorável e âmbito demonstrado;procedimento operacional fornecido pela Tempo.Não é benefício concedido nem imposto aplicado;apresentar pedido não concede isenção.',
};
return {
...result,
conditionalVat: '0.00',
conditionalReason:
'Cenário condicionado à isenção: hipótese expressa de cumprimento das condições e aplicação do procedimento, cuja correspondência permanece pendente. Não é benefício concedido nem imposto aplicado; apresentar pedido não concede isenção.',
};
}
export function offerPathReport(row: OfferPreparation): string {
const request = row.request ?? emptyOfferRequest(),
scenario = offerScenarios(row);
if (row.workflow)
return (
offerWorkflowReport(row) +
`;Cenário normal — IVA ${scenario.normalVat ?? 'Indeterminado'};Cenário condicionado à isenção — IVA ${scenario.conditionalVat ?? 'Indeterminado'};IVA do âmbito deferido:${scenario.appliedVat ?? 'Não aplicado'};${scenario.conditionalReason}`
);
const scope = (r: OfferRequest) =>
`bens ${r.goods || 'por indicar'}; operação ${r.operationScope || r.operation || 'por indicar'}; período ${r.periodScope || `${r.periodFrom || 'por indicar'} a ${r.periodTo || 'por indicar'}`}; condições ${r.conditions || 'por indicar'}; documento ${r.decisionRef || 'por indicar'},${r.decisionOn || 'data por indicar'}`;
return `${row.branch === 'P084' ? `Pedido ${offerRequestLabels[request.state]} (estado declarado, sem concessão pelo simulador); referência ${request.reference || 'por indicar'}, apresentação ${request.presentedOn || 'por indicar'}; âmbito em edição ${scope(request)}; ${(row.requestRecords ?? []).map((record) => `estado registado ${offerRequestLabels[record.request.state]} em ${record.recordedAt}, ${scope(record.request)}`).join(' | ') || 'sem estado/decisão registado'}; não estender decisão aos restantes bens, operações, períodos ou oportunidades; apresentar pedido não concede isenção` : `Emergência ${row.disaster || 'por indicar'}; finalidade humanitária ${row.humanitarianPurpose || 'por indicar'}; procedimento de emergência/doação por corresponder, sem aplicação automática`}; documentos disponíveis ${row.availableDocuments || row.documentRef || 'por indicar'}; Cenário normal — IVA ${scenario.normalVat ?? 'Indeterminado'} ${row.currency}, documento/pressuposto ${row.normalVatSource || 'por indicar'}; Cenário condicionado à isenção — IVA ${scenario.conditionalVat ?? 'Indeterminado'} ${row.currency}; ${scenario.conditionalReason}`;
}
export function offerPreparationReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
return offerPreparationBook(draft).rows.map((row) => [
'IVA mensal',
`Rascunho de oferta ${row.branch} — ${row.id}`,
`${row.description || 'Bens por indicar'}; ${offerWorkflowApplied(row).applied ? 'tratamento deferido no âmbito do despacho' : 'preparação, benefício não aplicado'}; ${row.amount || 'Valor não informado'} ${row.currency}; ${row.period || 'Período não informado'}`,
`${offerPreparationSource} Oportunidade ${acquisitionScope(draft)}; operação ${row.operation || 'por indicar'}; contraparte ${row.counterparty || 'por indicar'}; documento ${row.documentRef || 'por indicar'},${row.documentDate || 'data por indicar'}; beneficiário ${row.beneficiary || 'por indicar'}; finalidade ${row.purpose || 'por indicar'}; ${row.branch === 'P084' ? `instituição nacional ${row.nationalInstitution || 'por indicar'}, interesse público/relevantes fins sociais ${row.publicSocialInstitution || 'por indicar'}, bens adequados ${row.goodsSuitable || 'por indicar'}, utilização de interesse público ${row.publicInterestUse || 'por indicar'}` : `calamidade ${row.disaster || 'por indicar'}, natureza ${row.disasterKind || 'por indicar'}, atenuação dos efeitos ${row.mitigation || 'por indicar'}`}; fundamento ${row.evidence || 'por indicar'}; origem reutilizada ${row.reusedFrom?.key || 'entrada directa'}; ${offerPathReport(row)}; sem isenção automática ou dedução.`,
]);
} catch {
return [
[
'IVA mensal',
'Rascunhos de ofertas',
'Dados não recuperáveis',
'Originais preservados; sem substituição por zero.',
],
];
}
}