311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { compileFunction } from 'node:vm';
|
|
import ts from 'typescript';
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks) {
|
|
const code = ts.transpileModule(
|
|
fs.readFileSync(new URL(file, import.meta.url), 'utf8'),
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(code, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
function nodes(tree) {
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
const text = (tree) =>
|
|
typeof tree === 'string'
|
|
? tree
|
|
: Array.isArray(tree)
|
|
? tree.map(text).join('')
|
|
: tree?.props
|
|
? text(tree.props.children)
|
|
: typeof tree === 'number'
|
|
? String(tree)
|
|
: '';
|
|
const document = {
|
|
id: 'link-one',
|
|
document_id: 'original-one',
|
|
file_name: 'SYNTHETIC proof.txt',
|
|
version_no: 3,
|
|
source_reference: 'SYNTHETIC document source',
|
|
status: 'active',
|
|
malware_scan_status: 'clean',
|
|
};
|
|
const payment = {
|
|
id: 'payment-one',
|
|
reference: 'SYNTHETIC reference',
|
|
amount: '25.50',
|
|
currency: 'MZN',
|
|
source_reference: 'SYNTHETIC amount purpose',
|
|
document_id: null,
|
|
version: 1,
|
|
status: 'pending',
|
|
verified_by: null,
|
|
reason: null,
|
|
};
|
|
const shared = { card: '', input: '', button: '' };
|
|
function formsHarness(file, defaults, pickerMock = {}) {
|
|
const forms = [],
|
|
calls = [];
|
|
let cursor = 0,
|
|
serial = 0,
|
|
tree,
|
|
props = defaults;
|
|
const loaded = load(file, {
|
|
'@inertiajs/react': {
|
|
useForm(initial) {
|
|
const i = cursor++;
|
|
forms[i] ??= {
|
|
data: { ...initial },
|
|
errors: {},
|
|
processing: false,
|
|
setData(key, value) {
|
|
this.data[key] = value;
|
|
},
|
|
transform(callback) {
|
|
this.transformer = callback;
|
|
},
|
|
reset(...keys) {
|
|
for (const key of keys) this.data[key] = initial[key];
|
|
},
|
|
put(url, options) {
|
|
calls.push({
|
|
url,
|
|
method: 'PUT',
|
|
data: { ...this.data },
|
|
options,
|
|
});
|
|
},
|
|
post(url, options) {
|
|
calls.push({
|
|
url,
|
|
method: 'POST',
|
|
data: this.transformer
|
|
? this.transformer(this.data)
|
|
: { ...this.data },
|
|
options,
|
|
});
|
|
},
|
|
};
|
|
return forms[i];
|
|
},
|
|
},
|
|
'@/lib/operation-key': {
|
|
createOperationKey: () => `synthetic-key-${++serial}`,
|
|
},
|
|
'@/pages/investment/shared': shared,
|
|
'./desk-payment-document-form': pickerMock,
|
|
});
|
|
const Component = loaded.default ?? loaded.RequestOperations;
|
|
function render(patch = {}) {
|
|
props = { ...props, ...patch };
|
|
cursor = 0;
|
|
tree = Component(props);
|
|
}
|
|
render();
|
|
return {
|
|
forms,
|
|
calls,
|
|
render,
|
|
all: () => nodes(tree),
|
|
text: () => text(tree),
|
|
find: (type, label) =>
|
|
nodes(tree).find(
|
|
(n) => n.type === type && (!label || text(n).includes(label)),
|
|
),
|
|
submit() {
|
|
nodes(tree)
|
|
.find((n) => n.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
},
|
|
};
|
|
}
|
|
const proofHarness = () =>
|
|
formsHarness(
|
|
'../../resources/js/components/desk-payment-document-form.tsx',
|
|
{ requestId: 'request-one', payment, documents: [document] },
|
|
);
|
|
|
|
test('adding proof to a pending payment sends only new documentary facts and conserves financial declaration', () => {
|
|
const h = proofHarness();
|
|
h.find('select').props.onChange({ target: { value: document.id } });
|
|
h.render();
|
|
h.find('textarea').props.onChange({
|
|
target: { value: 'SYNTHETIC reason' },
|
|
});
|
|
h.render();
|
|
h.find('input').props.onChange({ target: { checked: true } });
|
|
h.render();
|
|
h.submit();
|
|
assert.match(h.calls[0].url, /pagamentos\/payment-one\/comprovativo$/);
|
|
assert.equal(h.calls[0].method, 'PUT');
|
|
assert.deepEqual(
|
|
Object.keys(h.calls[0].data).sort(),
|
|
[
|
|
'version',
|
|
'document_link_id',
|
|
'reason',
|
|
'consent',
|
|
'client_key',
|
|
].sort(),
|
|
);
|
|
assert.equal(h.calls[0].data.document_link_id, document.id);
|
|
assert.equal(h.calls[0].data.version, 1);
|
|
assert.equal(h.calls[0].data.consent, true);
|
|
assert.match(
|
|
h.text(),
|
|
/referência, o valor e a moeda já registados são conservados/,
|
|
);
|
|
assert.equal(h.all().filter((n) => n.type === 'input').length, 1);
|
|
});
|
|
|
|
test('failed retry keeps payload and operation key while editing deliberately starts a new operation', () => {
|
|
const h = proofHarness();
|
|
h.forms[0].setData('document_link_id', document.id);
|
|
h.forms[0].setData('reason', 'SYNTHETIC original reason');
|
|
h.forms[0].setData('consent', true);
|
|
h.submit();
|
|
h.submit();
|
|
assert.deepEqual(h.calls[0].data, h.calls[1].data);
|
|
const original = h.forms[0].data.client_key;
|
|
h.find('textarea').props.onChange({
|
|
target: { value: 'SYNTHETIC edited reason' },
|
|
});
|
|
h.render();
|
|
assert.notEqual(h.forms[0].data.client_key, original);
|
|
assert.equal(h.forms[0].data.consent, false);
|
|
assert.equal(h.forms[0].data.document_link_id, document.id);
|
|
h.forms[0].setData('consent', true);
|
|
h.submit();
|
|
const key = h.forms[0].data.client_key;
|
|
h.calls.at(-1).options.onSuccess();
|
|
h.render({ payment: { ...payment, version: 2 } });
|
|
assert.equal(h.forms[0].data.version, 2);
|
|
assert.equal(h.forms[0].data.reason, '');
|
|
assert.equal(h.forms[0].data.document_link_id, '');
|
|
assert.notEqual(h.forms[0].data.client_key, key);
|
|
});
|
|
|
|
test('new server version requires deliberate adoption and does not erase reason or selected proof', () => {
|
|
const h = proofHarness();
|
|
h.forms[0].setData('document_link_id', document.id);
|
|
h.forms[0].setData('reason', 'SYNTHETIC unsaved reason');
|
|
h.forms[0].setData('consent', true);
|
|
h.render({ payment: { ...payment, version: 3 } });
|
|
assert.equal(h.find('button', 'Guardar comprovativo').props.disabled, true);
|
|
h.submit();
|
|
assert.equal(h.calls.length, 0);
|
|
h.find('button', 'Usar versão actual').props.onClick();
|
|
h.render();
|
|
assert.equal(h.forms[0].data.version, 3);
|
|
assert.equal(h.forms[0].data.document_link_id, document.id);
|
|
assert.equal(h.forms[0].data.reason, 'SYNTHETIC unsaved reason');
|
|
assert.equal(h.forms[0].data.consent, false);
|
|
});
|
|
|
|
test('read-only operations preserve payment history without offering manual business forms', () => {
|
|
const Proof = () => null;
|
|
const h = formsHarness(
|
|
'../../resources/js/components/desk-internal-operations.tsx',
|
|
{
|
|
requestId: 'request-one',
|
|
owner: false,
|
|
reviewer: false,
|
|
documents: [document],
|
|
payments: [payment],
|
|
},
|
|
{ default: Proof },
|
|
);
|
|
assert.match(h.text(), /SYNTHETIC reference/);
|
|
assert.match(h.text(), /Comprovativo ainda não associado/);
|
|
assert.equal(h.all().filter((n) => n.type === 'form').length, 0);
|
|
assert.equal(h.all().filter((n) => n.type === Proof).length, 0);
|
|
h.render({ payments: [{ ...payment, document_id: document.document_id }] });
|
|
assert.match(h.text(), /metadados da associação não capturados à época/);
|
|
});
|
|
|
|
test('only pending or disputed payment offers proof correction and creation uses exact link without copying amount provenance', () => {
|
|
const Proof = () => null;
|
|
const h = formsHarness(
|
|
'../../resources/js/components/desk-internal-operations.tsx',
|
|
{
|
|
requestId: 'request-one',
|
|
owner: true,
|
|
reviewer: false,
|
|
documents: [document],
|
|
payments: [
|
|
payment,
|
|
{ ...payment, id: 'verified', status: 'internal_verified' },
|
|
{ ...payment, id: 'issue', status: 'internal_issue' },
|
|
],
|
|
},
|
|
{ default: Proof },
|
|
);
|
|
assert.equal(h.all().filter((n) => n.type === Proof).length, 2);
|
|
const selector = h
|
|
.all()
|
|
.find(
|
|
(n) =>
|
|
n.type === 'select' &&
|
|
n.props.value === h.forms[0].data.document_link_id,
|
|
);
|
|
assert.ok(
|
|
nodes(selector).some(
|
|
(n) => n.type === 'option' && n.props.value === document.id,
|
|
),
|
|
);
|
|
selector.props.onChange({ target: { value: document.id } });
|
|
h.render();
|
|
assert.equal(h.forms[0].data.document_link_id, document.id);
|
|
assert.equal(h.forms[0].data.source_reference, '');
|
|
assert.equal(h.forms[0].data.amount, '');
|
|
});
|
|
|
|
test('historical evidence shows the frozen file version and separates documentary reference from amount purpose', () => {
|
|
const h = formsHarness(
|
|
'../../resources/js/components/desk-internal-operations.tsx',
|
|
{
|
|
requestId: 'request-one',
|
|
owner: false,
|
|
reviewer: false,
|
|
documents: [],
|
|
payments: [
|
|
{
|
|
...payment,
|
|
document_id: document.document_id,
|
|
document_evidence: {
|
|
link_id: document.id,
|
|
file_name: document.file_name,
|
|
document_version: 3,
|
|
source_reference: document.source_reference,
|
|
valid_until: '2028-01-02',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{ default: () => null },
|
|
);
|
|
assert.match(h.text(), /Fonte do valor: SYNTHETIC amount purpose/);
|
|
assert.match(
|
|
h.text(),
|
|
/SYNTHETIC proof.txt.*versão 3.*SYNTHETIC document source.*2028-01-02/,
|
|
);
|
|
});
|