import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts'; export const serviceLocationVersion = 'service-territoriality/1'; export const serviceLocationSource = 'CIVA6(3),6(4)/(5)(a–d),6(6),Lei13/2016PDF7–8/p1426(265–266);AvisoBR89-I,08/06/2017,PDF1 rectifica6(5)c/d:actividades materialmente nacionais e transporte pela distância nacional;Lei10/2025PDF12mantém6(3–6),altera7–9digitais. Recorte2026:regra geral sem regras especiais aplicáveis;imóveis,trabalhos/peritagens em móveis e actividades presenciais pelos factos próprios. Sem taxa/IVA/isenção/dedução/campo11/totaisModeloA;digital e outros6(7)/(8)não classificados por estes percursos.'; export const serviceLocationTreatments = [ { id: 'general_service', label: 'P106 — Regra geral dos serviços', branch: 'P106', }, { id: 'immovable_service', label: 'P107 — Serviços relacionados com imóveis', branch: 'P107', }, { id: 'movable_work', label: 'P108 — Trabalhos/peritagens em bens móveis', branch: 'P108', }, { id: 'activity_service', label: 'P109 — Actividades presenciais e serviços acessórios', branch: 'P109', }, { id: 'transport_service', label: 'P110 — Transporte por distância e percurso', branch: 'P110', }, ] as const; export const serviceLocationConditions = [ [ 'serviceSupply', 'Está identificada uma prestação de serviços e a operação/período correspondentes', ], [ 'otherSpecialRulesExcluded', 'Foi conferida a natureza da operação: não está abrangida neste caso por regime digital ou regras6(7)/(8)que exijam outro percurso', ], ] as const; export const serviceOwnConditions = { general_service: [ [ 'noPlaceException', 'Não se aplica nenhuma excepção de localização6(4)/(5):imóveis,trabalhos/peritagens em móveis,actividades materiais ou transporte', ], [ 'providerEstablishment', 'Identificada a sede,estabelecimento estável ou domicílio do prestador a partir do qual este serviço é prestado', ], ], immovable_service: [ [ 'propertyRelation', 'O serviço relaciona-se com o imóvel identificado,incluindo preparação/coordenação de trabalhos imobiliários,peritos ou agentes imobiliários', ], ], movable_work: [ [ 'corporealMovable', 'São trabalhos em bens móveis corpóreos ou peritagens a eles referentes', ], [ 'totalEssential', 'O local indicado corresponde à execução total ou essencial dos trabalhos/peritagens', ], ], activity_service: [ [ 'activityNature', 'Serviço artístico,científico,desportivo,recreativo,de ensino ou similar,incluindo organizadores e serviços acessórios dessa actividade', ], [ 'materialActivity', 'A actividade ou serviço acessório tem materialmente lugar no local indicado', ], ], transport_service: [ [ 'transportNature', 'Prestação de transporte com percurso e distâncias identificados', ], [ 'routeCorresponds', 'Distâncias e pontos de partida/chegada correspondem a este trajecto;ida e volta são trajectos distintos6(6)', ], ], } as const; export function ownServiceConditions( treatment: string, ): ReadonlyArray { return ( serviceOwnConditions[treatment as keyof typeof serviceOwnConditions] ?? [] ); } type Choice = '' | 'yes' | 'no'; export type ServiceLocationRow = { id: string; treatment: string; description: string; period: string; origin: 'projected' | 'effective'; operationDate: string; operationRef: string; documentRef: string; documentDate: string; servicePlace: string; placeCountry: string; placeRef: string; totalDistance: string; nationalDistance: string; departure: string; arrival: string; routeRef: string; providerRef: string; providerPlace: string; providerCountry: string; subjectRef: string; scope: string; evidence: string; conditions: Partial>; }; type LineResult = { id: string; status: 'determined' | 'not_applicable' | 'indeterminate'; territory: 'national' | 'outside' | 'partial' | null; baseAmount: null; vat: null; rate: null; reason: string; }; export type ServiceLocationCalculation = { version: string; revision: number; calculatedAt: string; opportunity: string; location: string; sourceKey: string; source: string; inputs: ServiceLocationRow[]; rows: LineResult[]; }; export type ServiceLocationBook = { rows: ServiceLocationRow[]; result?: ServiceLocationCalculation; history?: ServiceLocationCalculation[]; }; type Store = ReturnType & { serviceLocations?: Record; }; export function newServiceLocation(id: string): ServiceLocationRow { return { id, treatment: '', description: '', period: '', origin: 'projected', operationDate: '', operationRef: '', documentRef: '', documentDate: '', servicePlace: '', placeCountry: '', placeRef: '', totalDistance: '', nationalDistance: '', departure: '', arrival: '', routeRef: '', providerRef: '', providerPlace: '', providerCountry: '', subjectRef: '', scope: '', evidence: '', conditions: {}, }; } export function switchServiceTreatment( row: ServiceLocationRow, treatment: string, ): ServiceLocationRow { return { ...row, treatment, conditions: Object.fromEntries( serviceLocationConditions.map(([key]) => [ key, row.conditions[key] ?? '', ]), ), }; } function readStore(draft: Record): Store { const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store; if ( store.serviceLocations !== undefined && (!store.serviceLocations || typeof store.serviceLocations !== 'object' || Array.isArray(store.serviceLocations)) ) throw new Error( 'Localizações de serviços não recuperáveis;originais preservados.', ); const fields = Object.keys(newServiceLocation('')).filter( (key) => key !== 'conditions', ); for (const book of Object.values(store.serviceLocations ?? {})) if ( !book || !Array.isArray(book.rows) || (book.history !== undefined && !Array.isArray(book.history)) || book.rows.some( (row) => !row || fields.some( (key) => typeof row[key as keyof ServiceLocationRow] !== 'string', ) || !row.conditions || typeof row.conditions !== 'object' || Array.isArray(row.conditions), ) ) throw new Error( 'Linhas de localização de serviços não recuperáveis;originais preservados,sem substituição por lista vazia.', ); return store; } export function serviceLocationBook( draft: Record, ): ServiceLocationBook { return ( readStore(draft).serviceLocations?.[acquisitionScope(draft)] ?? { rows: [], } ); } export function saveServiceLocationBook( draft: Record, book: ServiceLocationBook, ): string { const current = readStore(draft), encoded = JSON.stringify({ ...current, serviceLocations: { ...current.serviceLocations, [acquisitionScope(draft)]: book, }, }); readStore({ ...draft, modelo_a_snapshot: encoded }); return encoded; } function validDate(value: string): boolean { const d = new Date(value + 'T00:00:00Z'); return ( /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(d.getTime()) && d.toISOString().slice(0, 10) === value ); } export function evaluateServiceLocation(row: ServiceLocationRow): LineResult { const result: LineResult = { id: row.id, status: 'indeterminate', territory: null, baseAmount: null, vat: null, rate: null, reason: '', }; const stop = ( reason: string, status: LineResult['status'] = 'indeterminate', ) => ({ ...result, status, reason }); const rule = serviceLocationTreatments.find((t) => t.id === row.treatment); if (!rule) return stop( 'Identifique o percurso do serviço;desconhecido não determina território.', ); if ( !/^2026-(0[1-9]|1[0-2])$/.test(row.period) || !['projected', 'effective'].includes(row.origin) || !validDate(row.operationDate) || row.operationDate.slice(0, 7) !== row.period ) return stop( 'Identifique mês2026,data correspondente e natureza efectiva/projectada dos dados.', ); const conditions = [ ...serviceLocationConditions, ...ownServiceConditions(row.treatment), ]; if (conditions.some(([key]) => row.conditions[key] === 'no')) return stop( 'Este percurso não se aplica aos factos declarados;rever a regra própria,sem presumir fora do território,isenção ou IVAzero.', 'not_applicable', ); if (conditions.some(([key]) => row.conditions[key] !== 'yes')) return stop( 'Factos ou regra própria por esclarecer;localização indeterminada,não isenção ou IVAzero.', ); if ( !row.description.trim() || !row.operationRef.trim() || !row.documentRef.trim() || !validDate(row.documentDate) || !row.scope.trim() || !row.evidence.trim() ) return stop( 'Identifique serviço/operação,documento/data,âmbito e fundamento factual ou pressupostos expressos;reutilize dados sem certificado/aprovador novo.', ); if (row.treatment === 'transport_service') { const parse = (v: string): bigint | null => /^\d{1,18}(\.\d{1,6})?$/.test(v) ? BigInt(v.split('.')[0]) * 1000000n + BigInt((v.split('.')[1] ?? '').padEnd(6, '0')) : null; const total = parse(row.totalDistance), national = parse(row.nationalDistance); if ( total === null || total === 0n || national === null || national > total || !['MZ', 'foreign'].includes(row.departure) || !['MZ', 'foreign'].includes(row.arrival) || !row.routeRef.trim() ) return stop( 'Identifique percurso,pontos de partida/chegada e distâncias na mesma unidade,total positiva e nacional entre zero e total;desconhecido não é zero.', ); const deemed = row.departure === 'MZ' && row.arrival === 'MZ'; const covered = deemed ? total : national; const territory = covered === 0n ? 'outside' : covered === total ? 'national' : 'partial'; return { ...result, status: 'determined', territory, reason: `P110:artigo6(4)d/6(5)d rectificado pelo AvisoBR89-I,08/06/2017,PDF1,e6(6). ${deemed ? 'Partida e chegada no território nacional:inclui o percurso efectuado fora dos mesmos.' : `Parcela nacional:distância ${row.nationalDistance} de ${row.totalDistance},na mesma unidade,sem arredondamento ou apuramento monetário.`} Ida e volta são trajectos distintos:recolher uma linha por trajecto. ${row.origin === 'projected' ? 'Pressupostos expressos.' : 'Dados efectivos declarados.'} Fora do território não equivale a isenção/IVAzero;sem base,taxa,IVA,dedução/campo11 ou totaisModeloA.`, }; } const general = row.treatment === 'general_service'; const country = general ? row.providerCountry : row.placeCountry; if ( !['MZ', 'foreign'].includes(country) || !(general ? row.providerPlace : row.servicePlace).trim() || !(general ? row.providerRef : row.placeRef).trim() || (!general && !row.subjectRef.trim()) ) return stop( general ? 'Identifique sede/EP/domicílio do prestador a partir do qual o serviço é prestado,país e referência;endereço do cliente não substitui esse estabelecimento.' : 'Identifique local,país,referência e imóvel/bens/actividade a que respeita;endereço do cliente ou sede do prestador não substituem esses factos.', ); const territory = country === 'MZ' ? 'national' : 'outside'; const clause = general ? '6(3)' : `6(${territory === 'national' ? '5' : '4'})(${row.treatment === 'immovable_service' ? 'a' : row.treatment === 'movable_work' ? 'b' : 'c'})`; const fact = general ? 'estabelecimento do prestador a partir do qual o serviço é prestado' : row.treatment === 'immovable_service' ? 'local do imóvel' : row.treatment === 'movable_work' ? 'local de execução total ou essencial dos trabalhos/peritagens' : 'local material da actividade ou serviço acessório'; return { ...result, status: 'determined', territory, reason: `${rule.branch}:artigo${clause},${fact} ${territory === 'national' ? 'emMoçambique' : 'fora deMoçambique'},${row.origin === 'projected' ? 'sob pressupostos expressos' : 'segundo dados efectivos declarados'}. ${general ? 'Regras especiais conferidas como não aplicáveis.' : 'Regra territorial própria,independente da sede nacional ou estrangeira do prestador.'} Fora deste âmbito não equivale a isenção/IVAzero;não calcula base,taxa,IVA,dedução/campo11 ou totaisModeloA.`, }; } function key( draft: Record, book: ServiceLocationBook, ): string { return JSON.stringify({ version: serviceLocationVersion, scope: acquisitionScope(draft), location: draft.selection_labels ?? {}, rows: book.rows, }); } export function serviceLocationCurrent( draft: Record, book: ServiceLocationBook, ): boolean { return !!book.result && book.result.sourceKey === key(draft, book); } export function recordServiceLocations( draft: Record, book: ServiceLocationBook, ): ServiceLocationBook { if ( !book.rows.length || new Set(book.rows.map((row) => row.id)).size !== book.rows.length ) throw new Error( 'Indique operações com identificadores distintos;originais conservados.', ); const result: ServiceLocationCalculation = { version: serviceLocationVersion, revision: (book.result?.revision ?? 0) + 1, calculatedAt: new Date().toISOString(), opportunity: acquisitionScope(draft), location: JSON.stringify(draft.selection_labels ?? {}), sourceKey: key(draft, book), source: serviceLocationSource, inputs: structuredClone(book.rows), rows: book.rows.map((row) => evaluateServiceLocation(row)), }; const next = { ...book, result, history: [ ...(book.history ?? []), ...(book.result ? [book.result] : []), ], }; saveServiceLocationBook(draft, next); return next; } export function serviceLocationReportRows( draft: Record, ): Array<[string, string, string, string]> { try { const book = serviceLocationBook(draft), result = book.result; if (!result) return []; return result.inputs.map((row, index) => [ 'IVA mensal', `Localização ${serviceLocationTreatments.find((t) => t.id === row.treatment)?.branch ?? 'serviço'} — ${row.period} — ${row.id}`, `${row.description};classificação territorial: ${result.rows[index].territory === 'national' ? 'Moçambique' : result.rows[index].territory === 'outside' ? 'Fora do território nacional' : result.rows[index].territory === 'partial' ? 'Parcela nacional do transporte' : 'Indeterminada'};IVA: Indeterminado;${result.rows[index].reason}`, `${result.opportunity};${result.location};${serviceLocationCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin === 'projected' ? 'Projecção' : 'Dados efectivos declarados'};${serviceLocationTreatments.find((t) => t.id === row.treatment)?.label ?? 'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};${row.treatment === 'transport_service' ? `trajecto ${row.routeRef};partida ${row.departure};chegada ${row.arrival};distâncias nacional/total ${row.nationalDistance}/${row.totalDistance}` : row.treatment === 'general_service' ? `estabelecimento prestador ${row.providerPlace},${row.providerCountry};referência ${row.providerRef}` : `local ${row.servicePlace},${row.placeCountry};referência ${row.placeRef};imóvel/bens/actividade ${row.subjectRef}`};âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...serviceLocationConditions, ...ownServiceConditions(row.treatment)].map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento ${row.evidence};não determina isenção/IVA/campo11/totaisModeloA.`, ]); } catch { return [ [ 'IVA mensal', 'Localização de serviços', 'Dados não recuperáveis', 'Originais preservados;sem classificar por defeito.', ], ]; } }