483 lines
19 KiB
TypeScript
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.',
|
|
],
|
|
];
|
|
}
|
|
}
|