mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-10 09:13:54 +01:00
feat: export confirmed payments as financial events
This commit is contained in:
@@ -161,11 +161,11 @@ describe('disposable clone migration execution', () => {
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('discovers the contiguous repository manifest through migration 40 dynamically', () => {
|
||||
test('discovers the contiguous repository manifest through migration 41 dynamically', () => {
|
||||
const manifest = discoverMigrationManifest();
|
||||
expect(manifest.latestOrdinal).toBeGreaterThanOrEqual(MINIMUM_RELEASE_MIGRATION);
|
||||
expect(manifest.latestFilename).toBe('040_paid_match_entropy_precommit.sql');
|
||||
expect(manifest.files).toHaveLength(40);
|
||||
expect(manifest.latestFilename).toBe('041_financial_event_outbox.sql');
|
||||
expect(manifest.files).toHaveLength(41);
|
||||
});
|
||||
|
||||
test('runs the normal ordered migration implementation and proves the exact ledger', async () => {
|
||||
@@ -185,9 +185,9 @@ describe('disposable clone migration execution', () => {
|
||||
});
|
||||
|
||||
expect(pool.transactions).toEqual(result.files);
|
||||
expect(result.latestFilename).toBe('040_paid_match_entropy_precommit.sql');
|
||||
expect(result.latestFilename).toBe('041_financial_event_outbox.sql');
|
||||
expect(output).toContain('clone_migration_status=ok');
|
||||
expect(output).toContain('latest_migration=040_paid_match_entropy_precommit.sql');
|
||||
expect(output).toContain('latest_migration=041_financial_event_outbox.sql');
|
||||
expect(output.join('\n')).not.toContain(secret);
|
||||
expect(pool.config).toEqual(expect.objectContaining({
|
||||
database: env.DB_NAME,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
const FinancialEventExporter = require('../src/services/financialEventExporter');
|
||||
const { buildFinancialEvent } = FinancialEventExporter;
|
||||
|
||||
const ENV = {
|
||||
NODE_ENV: 'production',
|
||||
CRYPTO_TYPE: 'WOW',
|
||||
MONERO_NETWORK: 'mainnet',
|
||||
FINANCIAL_EVENT_SINK_URL: 'https://accounting.example.test/api/events/wowngeon',
|
||||
FINANCIAL_EVENT_SINK_TOKEN: 'a'.repeat(64),
|
||||
FINANCIAL_EVENT_ACCOUNT_REF: 'wowngeon:receipts'
|
||||
};
|
||||
|
||||
const PAYMENT = {
|
||||
id: 22,
|
||||
payment_type: 'credits_package',
|
||||
expected_amount: '900000000000',
|
||||
received_amount: '900000000000',
|
||||
confirmed_at: '2026-07-22T14:30:00Z',
|
||||
provider_id: 'native-monero',
|
||||
product_id: 'small',
|
||||
receipts: [{ evidence_type: 'chain_output', evidence_id: `${'a'.repeat(64)}:${'b'.repeat(64)}` }]
|
||||
};
|
||||
|
||||
describe('financial event exporter', () => {
|
||||
test('builds a backend-neutral, unpriced WOW receipt without customer data', () => {
|
||||
const event = buildFinancialEvent(PAYMENT, ENV);
|
||||
|
||||
expect(event).toEqual(expect.objectContaining({
|
||||
schema: 'financial-event/v1',
|
||||
id: 'payment-22',
|
||||
producer: 'wowngeon',
|
||||
product: 'wowngeon',
|
||||
activity: 'customer-payment',
|
||||
account_refs: ['wowngeon:receipts'],
|
||||
amounts: {},
|
||||
attributes: expect.objectContaining({
|
||||
valuation_required: true,
|
||||
atomic_amount: '900000000000',
|
||||
atomic_decimals: 11
|
||||
})
|
||||
}));
|
||||
expect(event.legs[0]).toEqual(expect.objectContaining({
|
||||
asset: 'WOW', quantity: '9', direction: 'in', usd_value: '0'
|
||||
}));
|
||||
expect(JSON.stringify(event)).not.toMatch(/user|email|address_index/i);
|
||||
});
|
||||
|
||||
test('claims and delivers one durable outbox row', async () => {
|
||||
const db = {
|
||||
query: jest.fn(async (sql) => {
|
||||
if (/RETURNING id, aggregate_id, attempts/i.test(sql)) {
|
||||
return db.claimed ? { rows: [] } : (db.claimed = true, { rows: [{ id: 5, aggregate_id: 22, attempts: 1 }] });
|
||||
}
|
||||
if (/FROM payments p/i.test(sql)) return { rows: [PAYMENT] };
|
||||
return { rows: [] };
|
||||
})
|
||||
};
|
||||
const fetchFn = jest.fn(async () => ({ ok: true, status: 201 }));
|
||||
const exporter = new FinancialEventExporter({ db, env: ENV, fetchFn, logger: { warn: jest.fn() } });
|
||||
|
||||
await expect(exporter.runOnce()).resolves.toEqual({ skipped: false, delivered: 1, deferred: 0 });
|
||||
expect(fetchFn).toHaveBeenCalledWith(ENV.FINANCIAL_EVENT_SINK_URL, expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({ authorization: `Bearer ${ENV.FINANCIAL_EVENT_SINK_TOKEN}` })
|
||||
}));
|
||||
expect(db.query.mock.calls.some(([sql]) => /SET status = 'delivered'/i.test(sql))).toBe(true);
|
||||
});
|
||||
|
||||
test('refuses to export stagenet value into production accounting', () => {
|
||||
expect(() => new FinancialEventExporter({
|
||||
db: { query: jest.fn() },
|
||||
env: { ...ENV, CRYPTO_TYPE: 'XMR', MONERO_NETWORK: 'stagenet' },
|
||||
fetchFn: jest.fn()
|
||||
})).toThrow(/must not be exported/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user