feat: create precious metals by grams and show optional purchase profit
This commit is contained in:
1 parent
027a8c1b6a
commit
0491f515fe
22 files changed
+570
-105
No files matched your search
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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 });
|
||||
|
||||
@@ -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' },
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user