Files
Investidor-V2/resources/js/lib/organization-country-reuse.ts
T

326 lines
11 KiB
TypeScript

import { router } from '@inertiajs/react';
import { useEffect, useRef, useState } from 'react';
import { createOperationKey } from './operation-key';
import { portalRequest, PortalApiError } from './portal-api';
import type {
useInvestmentProfileEditor,
OrganizationRecord,
} from './investment-profile-editor';
export type CountryReusePreview = {
organizationId: string;
organizationName: string;
organizationVersion: number;
country: string;
reference: string;
companyVersion: number;
companyCountry: string;
};
type Attempt = {
version: number;
organization_id: string;
organization_version: number;
operation_key: string;
country: string;
};
type Editor = ReturnType<typeof useInvestmentProfileEditor>;
const object = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
const version = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
export function useOrganizationCountryReuse(
companyId: string,
actorId: number,
editor: Editor,
reportDirty: (id: string, dirty: boolean) => void,
organizations: OrganizationRecord[],
) {
const [preview, setPreview] = useState<CountryReusePreview | null>(null),
[busy, setBusy] = useState(false),
[reading, setReading] = useState(false),
[uncertain, setUncertain] = useState(false),
[blocked, setBlocked] = useState(false),
[error, setError] = useState(''),
[notice, setNotice] = useState('');
const pending = useRef<Attempt | null>(null),
active = useRef(false),
alive = useRef(true),
liveEditor = useRef(editor),
sources = useRef(organizations),
origin = useRef({ companyId, actorId }),
current = useRef({ companyId, actorId });
current.current = { companyId, actorId };
liveEditor.current = editor;
sources.current = organizations;
const valid = () =>
alive.current &&
Number.isSafeInteger(actorId) &&
actorId > 0 &&
origin.current.actorId === current.current.actorId &&
origin.current.companyId === current.current.companyId;
const changedSource = () => {
if (!preview || pending.current) return false;
const source = sources.current.find(
(item) => item.id === preview.organizationId,
);
return (
!source ||
source.version > preview.organizationVersion ||
(source.version === preview.organizationVersion &&
source.country !== preview.country)
);
};
useEffect(() => {
alive.current = true;
return () => {
alive.current = false;
};
}, []);
useEffect(() => {
reportDirty(
'country-reuse-' + companyId,
!!preview || busy || uncertain,
);
}, [preview, busy, uncertain, companyId, reportDirty]);
useEffect(
() => () => reportDirty('country-reuse-' + companyId, false),
[companyId, reportDirty],
);
async function inspect(organizationId: string) {
if (active.current || !valid()) return;
if (
!pending.current &&
(liveEditor.current.dirty ||
liveEditor.current.blocked ||
liveEditor.current.stale)
) {
setError(
'Guarde ou confirme a preparação desta declaração antes de reutilizar o país.',
);
return;
}
active.current = true;
setReading(true);
setError('');
setNotice('');
try {
const data = await portalRequest<unknown>(
'/api/v1/investment/profile',
);
if (!valid()) return;
if (
!object(data) ||
data.actorId !== actorId ||
!Array.isArray(data.companies) ||
!Array.isArray(data.organizations)
)
throw new Error('Invalid context');
const company = data.companies.find(
(item) => object(item) && item.id === companyId,
),
source = data.organizations.find(
(item) => object(item) && item.id === organizationId,
);
if (
!object(company) ||
!version(company.version) ||
!object(company.payload) ||
!object(source) ||
!version(source.version) ||
typeof source.name !== 'string'
) {
setBlocked(true);
throw new Error('Source unavailable');
}
if (pending.current) {
setBlocked(false);
setNotice(
'Estado consultado: declaração na versão ' +
company.version +
'. Confirme o pedido sem resposta antes de preparar outra reutilização.',
);
return;
}
if (
company.version !== liveEditor.current.base.version ||
liveEditor.current.dirty
) {
setPreview(null);
setError(
'A declaração ou a preparação mudou. Consulte e compare o registo no formulário antes de reutilizar.',
);
router.reload({ only: ['companies', 'organizations'] });
return;
}
if (
typeof source.country !== 'string' ||
!/^[A-Z]{2}$/.test(source.country)
) {
setPreview(null);
setError(
'O país de constituição ainda não está disponível nesta organização.',
);
return;
}
const reference =
object(source.provenance) &&
typeof source.provenance.reference === 'string'
? source.provenance.reference
: '';
setPreview({
organizationId,
organizationName: source.name,
organizationVersion: source.version,
country: source.country,
reference,
companyVersion: company.version,
companyCountry:
typeof company.payload.incorporation_country_code ===
'string'
? company.payload.incorporation_country_code
: '',
});
setBlocked(false);
} catch {
if (valid())
setError(
'Não foi possível consultar a declaração e a organização nesta sessão. A preparação foi conservada.',
);
} finally {
if (valid()) {
active.current = false;
setReading(false);
}
}
}
async function confirm() {
if (active.current || blocked || !valid()) return;
if (!pending.current) {
if (changedSource()) {
setError(
'A organização mudou. Consulte novamente o país antes de confirmar.',
);
return;
}
if (
!preview ||
preview.companyVersion !== liveEditor.current.base.version
) {
setError('Volte a consultar os valores antes de confirmar.');
return;
}
let key: string;
try {
key = createOperationKey();
} catch (failure) {
setError(
failure instanceof Error
? failure.message
: 'Não foi possível preparar o pedido.',
);
return;
}
if (!liveEditor.current.beginCountryReuse()) {
setError(
'Guarde ou confirme a preparação desta declaração antes de reutilizar o país.',
);
return;
}
pending.current = {
version: preview.companyVersion,
organization_id: preview.organizationId,
organization_version: preview.organizationVersion,
operation_key: key,
country: preview.country,
};
}
const attempt = pending.current;
active.current = true;
setBusy(true);
setError('');
setNotice('');
try {
const { country: expectedCountry, ...command } = attempt;
const result = await portalRequest<unknown>(
'/api/v1/investment/companies/' +
companyId +
'/organization-country',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(command),
},
);
if (!valid() || pending.current !== attempt) return;
if (
!object(result) ||
!version(result.version) ||
result.version !== attempt.version + 1 ||
result.country !== expectedCountry
)
throw new Error('Unconfirmed result');
liveEditor.current.endCountryReuse({
version: result.version,
country: expectedCountry,
});
pending.current = null;
setPreview(null);
setUncertain(false);
setBlocked(false);
setNotice('País reutilizado e gravação confirmada.');
router.reload({ only: ['companies', 'organizations'] });
} catch (failure) {
if (!valid() || pending.current !== attempt) return;
if (
failure instanceof PortalApiError &&
[409, 422].includes(failure.status)
) {
pending.current = null;
liveEditor.current.endCountryReuse();
setUncertain(false);
setPreview(null);
setError(
'A declaração ou a fonte já não corresponde ao pedido. Consulte os valores guardados antes de preparar outra reutilização.',
);
router.reload({ only: ['companies', 'organizations'] });
} else {
setUncertain(true);
setBlocked(
failure instanceof PortalApiError &&
[401, 403, 404, 419].includes(failure.status),
);
setError(
'Não foi possível confirmar a reutilização. O pedido original foi conservado. Consulte o estado ou repita o mesmo pedido antes de editar.',
);
}
} finally {
if (valid()) {
active.current = false;
setBusy(false);
}
}
}
function cancel() {
if (pending.current || active.current || !valid()) return;
setPreview(null);
setError('');
setNotice('');
}
return {
preview,
busy,
reading,
uncertain,
blocked,
error,
notice,
inspect,
confirm,
cancel,
sourceChanged: changedSource(),
pendingOrganizationId: pending.current?.organization_id,
};
}