264 lines
11 KiB
TypeScript
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(' | ')}`;
|
|
}
|