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

264 lines
11 KiB
TypeScript

import type {
OfferPreparation,
OfferRequest,
} from './iva-offer-preparations.ts';
export const offerWorkflowSource =
'Procedimento operacional fornecido pela Tempo em25/10/2026:pedido escrito ao Ministério das Finanças/AT → pedido apresentado → decisão recebida e despacho anexo → preparação do formulário aplicável com despacho. Origem distinta do CIVA14(2)a/b e14(3) e do cotejo documental do ModeloI.1;sem nova certificação ou aprovação criada pelo portal.';
export type OfferAttachment = {
id: string;
draftId: string;
name: string;
sha256: string;
scan: string;
url: string;
};
export type OfferWorkflow = {
recipient: string;
writtenRequest: string;
requestAttachment?: OfferAttachment;
decisionIssuer: string;
decisionAttachment?: OfferAttachment;
scopeConfirmed: '' | 'yes' | 'no';
scopeKey?: string;
formName: string;
formSource: string;
formContent: string;
formAttachment?: OfferAttachment;
formDecisionId?: string;
formDecisionHash?: string;
formPreparedAt?: string;
submission: 'outside';
submissionReference: string;
records: Array<{
at: string;
stage: string;
request: OfferRequest;
workflow: Omit<OfferWorkflow, 'records'>;
}>;
};
export function emptyOfferWorkflow(): OfferWorkflow {
return {
recipient: 'Ministério das Finanças / Autoridade Tributária',
writtenRequest: '',
decisionIssuer: '',
scopeConfirmed: '',
formName: '',
formSource: '',
formContent: '',
submission: 'outside',
submissionReference: '',
records: [],
};
}
export function offerScopeKey(row: OfferPreparation): string {
return JSON.stringify({
branch: row.branch,
description: row.description,
beneficiary: row.beneficiary,
operation: row.operation,
period: row.period,
currency: row.currency,
amount: row.amount,
purpose: row.purpose,
evidence: row.evidence,
national: row.nationalInstitution,
social: row.publicSocialInstitution,
suitable: row.goodsSuitable,
use: row.publicInterestUse,
disaster: row.disaster,
mitigation: row.mitigation,
humanitarian: row.humanitarianPurpose,
request: row.request,
issuer: row.workflow?.decisionIssuer,
document: row.workflow?.decisionAttachment?.id,
hash: row.workflow?.decisionAttachment?.sha256,
});
}
export function offerAttachmentValid(
a: OfferAttachment | undefined,
): a is OfferAttachment {
return (
!!a &&
/^[0-9a-f-]{36}$/i.test(a.id) &&
/^[0-9a-f-]{36}$/i.test(a.draftId) &&
!!a.name &&
/^[0-9a-f]{64}$/.test(a.sha256) &&
a.url ===
`/api/v1/simulation-drafts/${a.draftId}/offer-documents/${a.id}`
);
}
export function offerWrittenRequest(
row: OfferPreparation,
w: OfferWorkflow,
): string {
return `Ao ${w.recipient}\nAssunto:Pedido de isenção — ${row.branch}\nEntidade/destinatário:${row.beneficiary}\nFinalidade:${row.purpose}\n${row.branch === 'P084' ? 'Justificação do interesse público/fins sociais:' + row.evidence : 'Calamidade:' + row.disaster + ';finalidade humanitária:' + row.humanitarianPurpose}\nBens:${row.description}\nOperação:${row.operation};período:${row.period};valor de referência:${row.amount} ${row.currency}\nDocumentos:${row.availableDocuments || row.documentRef}\n${w.writtenRequest}\nPreparação InvesteMoz;a apresentação ocorre fora do portal. Não constitui concessão.`;
}
export function offerWorkflowApplied(row: OfferPreparation): {
applied: boolean;
reason: string;
} {
const w = row.workflow,
r = row.request;
if (!w || !r || r.state !== 'granted')
return {
applied: false,
reason: 'Sem despacho de deferimento registado;mantém-se hipótese condicionada.',
};
if (
!r.decisionRef.trim() ||
!validDate(r.decisionOn) ||
!w.decisionIssuer.trim() ||
!offerAttachmentValid(w.decisionAttachment)
)
return {
applied: false,
reason: 'Despacho recebido incompleto:número,data,emitente e anexo por registar.',
};
if (w.scopeConfirmed !== 'yes' || w.scopeKey !== offerScopeKey(row))
return {
applied: false,
reason: 'Correspondência desta linha ao âmbito do despacho não confirmada ou dados alterados;sem extensão automática.',
};
if (
(!r.operationScope?.trim() &&
r.operation &&
r.operation !== row.operation) ||
(!r.periodScope?.trim() &&
/^\d{4}-\d{2}$/.test(row.period) &&
(row.period < r.periodFrom.slice(0, 7) ||
row.period > r.periodTo.slice(0, 7)))
)
return {
applied: false,
reason: 'A operação ou o período desta linha está fora do âmbito expresso do despacho;sem extensão automática.',
};
const conditions =
row.branch === 'P084'
? [
row.nationalInstitution,
row.publicSocialInstitution,
row.goodsSuitable,
row.publicInterestUse,
]
: [row.mitigation];
if (
conditions.some((v) => v !== 'yes') ||
!row.description ||
!row.beneficiary ||
!row.purpose ||
!row.operation ||
(row.branch === 'P085' &&
(row.operation !== 'acquisition' ||
!row.disaster ||
!row.humanitarianPurpose))
)
return {
applied: false,
reason: 'Condições próprias do tratamento por demonstrar;despacho não alarga o fundamento legal.',
};
return {
applied: true,
reason: `Tratamento deferido apenas nesta linha:${r.goods};${r.operationScope || r.operation};${r.periodScope || r.periodFrom + ' a ' + r.periodTo};condições:${r.conditions}. Despacho${r.decisionRef},${r.decisionOn},${w.decisionIssuer}. Sem extensão às restantes linhas,períodos ou oportunidades;sem dedução automática.`,
};
}
function validDate(v: string) {
const d = new Date(v + 'T00:00:00Z');
return (
/^\d{4}-\d{2}-\d{2}$/.test(v) &&
Number.isFinite(d.getTime()) &&
d.toISOString().slice(0, 10) === v
);
}
export function recordOfferWorkflow(
row: OfferPreparation,
stage: 'request' | 'presented' | 'decision' | 'form',
): OfferPreparation {
const w = structuredClone(row.workflow ?? emptyOfferWorkflow()),
r = structuredClone(row.request);
if (!r) throw Error('Prepare primeiro o pedido.');
if (
!w.recipient.trim() ||
!row.beneficiary.trim() ||
!row.description.trim() ||
!row.purpose.trim() ||
!row.operation
)
throw Error(
'Indique entidade,finalidade,bens,operação e destinatário do pedido;rascunho incompleto pode ser guardado.',
);
if (
stage === 'presented' &&
(!r.reference.trim() ||
!validDate(r.presentedOn) ||
r.state !== 'presented')
)
throw Error(
'Registe referência,data e estado apresentado;submissão fora do InvesteMoz.',
);
if (stage === 'decision' || stage === 'form') {
if (
!['granted', 'denied'].includes(r.state) ||
!r.decisionRef.trim() ||
!validDate(r.decisionOn) ||
!w.decisionIssuer.trim() ||
!offerAttachmentValid(w.decisionAttachment)
)
throw Error(
'Decisão recebida exige número,data,emitente e despacho anexo.',
);
const dates =
validDate(r.periodFrom) &&
validDate(r.periodTo) &&
r.periodFrom <= r.periodTo;
const months =
/^\d{4}-(0[1-9]|1[0-2])$/.test(r.periodFrom) &&
/^\d{4}-(0[1-9]|1[0-2])$/.test(r.periodTo) &&
r.periodFrom <= r.periodTo;
if (
!r.goods.trim() ||
(!r.operation && !r.operationScope?.trim()) ||
(!r.periodScope?.trim() && !dates && !months) ||
!r.conditions.trim()
)
throw Error(
'Conserve o âmbito literal:bens,operação,período e condições da decisão.',
);
if (stage === 'decision')
w.scopeKey =
w.scopeConfirmed === 'yes' ? offerScopeKey(row) : undefined;
}
if (stage === 'form') {
if (r.state !== 'granted')
throw Error(
'Não preparar formulário de isenção como deferido após indeferimento.',
);
if (!offerWorkflowApplied({ ...row, workflow: w }).applied)
throw Error(
'Confira explicitamente a correspondência desta linha ao despacho antes de preparar o formulário;alterações não renovam a confirmação automaticamente.',
);
if (
!w.formName.trim() ||
!w.formSource.trim() ||
(!w.formContent.trim() && !offerAttachmentValid(w.formAttachment))
)
throw Error(
'Identifique formulário aplicável e referência da sua aplicabilidade;prepare conteúdo ou anexe o exemplar preenchido.Sem presumir que ModeloI.1 abrange todos os casos.',
);
w.formDecisionId = w.decisionAttachment!.id;
w.formDecisionHash = w.decisionAttachment!.sha256;
w.formPreparedAt = new Date().toISOString();
}
const { records, ...snapshot } = w;
w.records = [
...records,
{ at: new Date().toISOString(), stage, request: r, workflow: snapshot },
];
return { ...row, workflow: w };
}
export function offerWorkflowReport(row: OfferPreparation): string {
const w = row.workflow;
if (!w) return '';
const state = offerWorkflowApplied(row);
return `${offerWorkflowSource} Pedido:${row.request?.reference || 'por indicar'};apresentado em:${row.request?.presentedOn || 'por indicar'};estado:${row.request?.state || 'preparation'};decisão:${row.request?.decisionRef || 'por indicar'},${row.request?.decisionOn || 'por indicar'};Pedido escrito:${offerWrittenRequest(row, w)};decisão emitente:${w.decisionIssuer || 'por indicar'};anexo:${w.decisionAttachment?.name || 'por anexar'},SHA256:${w.decisionAttachment?.sha256 || 'por indicar'};${state.reason};formulário:${w.formName || 'por identificar'},fonte/aplicabilidade:${w.formSource || 'por indicar'};conteúdo:${w.formContent || 'por preparar'};despacho ligado ao formulário:${w.formDecisionId || 'por preparar'}/${w.formDecisionHash || ''};submissão fora do InvesteMoz:${w.submissionReference || 'não registada'};histórico:${w.records.map((e) => e.stage + '@' + e.at + ':' + e.request.state + ':' + e.request.decisionRef).join(' | ')}`;
}