feat: create precious metals by grams and show optional purchase profit

This commit is contained in:
陈煜 committed 2026-10-03 22:43:59 +08:00
1 parent 027a8c1b6a
commit 0491f515fe
22 files changed
+570 -105

No files matched your search

+98
View File
@@ -294,3 +294,101 @@ test('inclusion preferences and precious metal settings, valuation and quotes re
await db.$disconnect();
}
});
test('gram-only creation, optional exact purchase cost, pending totals and backward-compatible backups', async () => {
const a = await fixture(),
b = await fixture();
try {
const input = {
name: 'gram asset',
currency: 'CNY',
metalType: 'gold',
metalGrams: '2.5',
metalPurity: '0.8',
autoValuation: true,
date: '2026-10-01T12:00',
metalCostPerGram: '12.8',
};
const created = await call('/metals/holdings', a.cookie, 'POST', input);
assert.equal(created.status, 201, JSON.stringify(created.data));
assert.equal(created.data.valuationAvailable, false);
const id = created.data.id;
let p = (await call('/positions/' + id, a.cookie)).data;
assert.equal(p.metalCost, '32.00000000');
assert.equal(p.metalProfit, null);
assert.equal(p.valuationAvailable, false);
assert.equal(await db.revision.count({ where: { positionId: id } }), 0);
const ov = (await call('/overview', a.cookie)).data;
assert.equal(ov.complete, false);
assert.deepEqual(ov.missingValuations, [{ id, name: input.name }]);
assert.equal((await call('/metals/' + id, b.cookie, 'PUT', input)).status, 400); // strict creation fields rejected
assert.equal(
(
await call('/metals/' + id, b.cookie, 'PUT', {
metalType: 'gold',
metalGrams: '2.5',
metalPurity: '0.8',
autoValuation: true,
})
).status,
404,
);
const price = await call('/metals/prices', a.cookie, 'POST', {
metalType: 'gold',
currency: 'CNY',
price: '20',
date: '2026-10-01',
});
assert.equal(price.status, 201, JSON.stringify(price.data));
p = (await call('/positions/' + id, a.cookie)).data;
assert.equal(p.amount, '40');
assert.equal(p.metalProfit, '8.00000000');
assert.equal(p.valuationAvailable, true);
const cfg = { metalType: 'gold', metalGrams: '2.5', metalPurity: '0.8', autoValuation: true };
assert.equal((await call('/metals/' + id, a.cookie, 'PUT', cfg)).status, 200);
assert.equal((await call('/positions/' + id, a.cookie)).data.metalCostPerGram, '12.8');
const controller = new (await import('../src/backup')).BackupBusinessService(db as any);
const backup = await (controller as any).data(a.id);
assert.equal(backup.positions[0].metalCostPerGram, '12.8');
assert.equal(
(await call('/backup/import', b.cookie, 'POST', { confirmed: true, backup })).status,
201,
);
const restored = (await call('/positions?kind=asset', b.cookie)).data[0];
assert.equal(restored.metalCostPerGram, '12.8');
assert.equal(restored.metalProfit, '8.00000000');
const invalidCost = structuredClone(backup);
Object.assign(invalidCost.positions[0], {
metalType: null,
metalGrams: null,
autoValuation: false,
});
assert.throws(() =>
(require('../src/backup') as typeof import('../src/backup')).validateBackup(invalidCost),
);
const old = structuredClone(backup);
delete old.positions[0].metalCostPerGram;
assert.doesNotThrow(() =>
(require('../src/backup') as typeof import('../src/backup')).validateBackup(old),
);
assert.equal(
(await call('/metals/' + id, a.cookie, 'PUT', { ...cfg, metalCostPerGram: null })).status,
200,
);
assert.equal((await call('/positions/' + id, a.cookie)).data.metalProfit, null);
const next = await call('/metals/holdings', a.cookie, 'POST', {
...input,
name: 'quoted',
metalCostPerGram: null,
});
assert.equal(next.status, 201);
assert.equal(next.data.valuationAvailable, true);
assert.equal((await call('/positions/' + next.data.id, a.cookie)).data.amount, '40');
assert.equal(
(await call('/metals/holdings', a.cookie, 'POST', { ...input, metalGrams: '0' })).status,
400,
);
} finally {
await db.user.deleteMany({ where: { id: { in: [a.id, b.id] } } });
await db.$disconnect();
}
});
+16 -1
View File
@@ -117,7 +117,7 @@ test('official SDK Streamable HTTP: real MySQL full business paths, drafts, isol
await write(a, 'metals_refresh');
await call(a, 'connection_info');
const discovered = await a.client.listTools();
assert.ok(discovered.tools.length >= 40);
assert.equal(discovered.tools.length, 49);
assert.equal(
discovered.tools.find((t) => t.name === 'positions_list')!.annotations!.readOnlyHint,
true,
@@ -322,6 +322,21 @@ test('official SDK Streamable HTTP: real MySQL full business paths, drafts, isol
});
assert.equal((await call(a, 'position_get', { id: metal })).amount, '21.75308642');
await write(a, 'metal_value', { id: metal });
const gram = (
await write(a, 'metal_holding_create', {
name: 'grams',
currency: 'CNY',
metalType: 'gold',
metalGrams: '2',
metalPurity: '1',
autoValuation: true,
metalCostPerGram: '9',
date: day,
})
).result.id;
const gramDetail = await call(a, 'position_get', { id: gram });
assert.equal(gramDetail.metalCost, '18.00000000');
assert.equal(gramDetail.metalProfit, '3.75308642');
await call(a, 'metals_prices');
await call(a, 'settings_get');
await call(a, 'overview_get', { limit: 1 });
+27
View File
@@ -81,3 +81,30 @@ test('public metal update preserves decimal values and manual quotes, skips clea
globalThis.fetch = original;
}
});
test('unpriced gram holdings make included totals incomplete and cost uses physical grams', async () => {
const p: Holding = {
id: 'pending',
name: 'pending',
kind: 'asset',
side: 'asset',
currency: 'CNY',
metalType: 'gold',
createdAt: new Date('2026-10-01'),
revisions: [],
};
const v = totals([p], [], 'CNY', '2026-10-03');
assert.equal(v.complete, false);
assert.equal(v.items[0].converted, null);
assert.equal(v.missingValuations[0].id, p.id);
assert.equal(totals([{ ...p, included: false }], [], 'CNY', '2026-10-03').complete, true);
assert.equal(totals([p], [], 'CNY', '2026-09-30').complete, true);
const { metalPerformance } = await import('../src/metals');
assert.deepEqual(
metalPerformance(
{ metalType: 'gold', metalGrams: '2.5', metalCostPerGram: '12.8' },
'40',
true,
),
{ valuationAvailable: true, metalCost: '32.00000000', metalProfit: '8.00000000' },
);
});