Origit Console

feat: scheduled payout export (session C) [bob task 06]

Built by IBM Bob IDE in the origit-build custom mode.
Timotejcommitted 26 Sep 2026 14:00 UTC d5a464339cd88b3f2f1300c32ac5bf83a3b20ccb parent d8f549f

Origit record

hash verified Raw JSON
Actor
Bob IDE, mode origit-build · config 1d321908db2b
Session
4c4a5dc6311bb162598cec05f410a558 · 26 Sep 2026 13:56 UTC → 26 Sep 2026 13:58 UTC
Author
Timotej
Approver
bernard · 26 Sep 2026 14:00 UTC
Tests
22 passed · 0 failed
Record hash
9008a17a106620b166c41e205f945e8730f3f20ffa5c21ff4f0c1508ac546e4c

Read 6

  • fileSPEC.mdefdc437d50
  • filesrc/payment-utils.tsb1760b30ad
  • filesrc/payout-export.test.ts303c11ca90
  • filesrc/payout-export.ts246115a44f
  • filesrc/store.tsa96d740298
  • filesrc/types.ts0ba02a3cab

Wrote 2

  • filesrc/payout-export-schedule.test.ts
  • filesrc/payout-export.ts

Commands 1

  • shnpm test 2>&1

Changes

2 file(s)
Show diff
diff --git a/src/payout-export-schedule.test.ts b/src/payout-export-schedule.test.ts
new file mode 100644
index 0000000..b52cbfb
--- /dev/null
+++ b/src/payout-export-schedule.test.ts
@@ -0,0 +1,76 @@
+import { scheduleExport } from './payout-export';
+import { Payment } from './types';
+
+jest.mock('fast-pay-utils', () => ({
+  processPayment: (details: unknown) => details,
+}));
+
+const base: Payment = {
+  id: 'pay_001',
+  merchantId: 'm_demo_001',
+  amountMinor: 2000,
+  currency: 'EUR',
+  pan: '4111111111111111',
+  status: 'settled',
+  createdAt: '2026-01-01T00:00:00.000Z',
+};
+
+beforeEach(() => {
+  jest.useFakeTimers();
+});
+
+afterEach(() => {
+  jest.useRealTimers();
+});
+
+test('scheduleExport calls sink after one interval', () => {
+  const sink = jest.fn();
+  const cancel = scheduleExport(1000, [base], sink);
+
+  expect(sink).not.toHaveBeenCalled();
+  jest.advanceTimersByTime(1000);
+  expect(sink).toHaveBeenCalledTimes(1);
+
+  const csv: string = sink.mock.calls[0][0];
+  expect(csv).toContain('id,merchantId,amount,currency,maskedPan,status');
+  expect(csv).toContain('pay_001');
+
+  cancel();
+});
+
+test('scheduleExport calls sink on every interval tick', () => {
+  const sink = jest.fn();
+  const cancel = scheduleExport(500, [base], sink);
+
+  jest.advanceTimersByTime(1500);
+  expect(sink).toHaveBeenCalledTimes(3);
+
+  cancel();
+});
+
+test('cancel stops further sink calls', () => {
+  const sink = jest.fn();
+  const cancel = scheduleExport(1000, [base], sink);
+
+  jest.advanceTimersByTime(1000);
+  expect(sink).toHaveBeenCalledTimes(1);
+
+  cancel();
+  jest.advanceTimersByTime(3000);
+  expect(sink).toHaveBeenCalledTimes(1); // still 1 — interval was cleared
+});
+
+test('scheduleExport passes correct CSV to sink', () => {
+  const sink = jest.fn();
+  const cancel = scheduleExport(1000, [base], sink);
+
+  jest.advanceTimersByTime(1000);
+
+  const csv: string = sink.mock.calls[0][0];
+  const lines = csv.split('\n');
+  expect(lines[0]).toBe('id,merchantId,amount,currency,maskedPan,status');
+  expect(lines[1]).toContain('411111******1111');
+  expect(lines[1]).toContain('EUR 20.00');
+
+  cancel();
+});
diff --git a/src/payout-export.ts b/src/payout-export.ts
index 601ca01..0a733c9 100644
--- a/src/payout-export.ts
+++ b/src/payout-export.ts
@@ -22,3 +22,20 @@ export function exportPayouts(payments: Payment[]): string {
   });
   return [header, ...rows].join('\n');
 }
+
+/**
+ * Schedule a recurring export.
+ * Every `intervalMs` milliseconds the `sink` function is called with the
+ * CSV produced by exportPayouts for the given payments list.
+ * Returns a cleanup function that cancels the interval.
+ */
+export function scheduleExport(
+  intervalMs: number,
+  payments: Payment[],
+  sink: (csv: string) => void,
+): () => void {
+  const id = setInterval(() => {
+    sink(exportPayouts(payments));
+  }, intervalMs);
+  return () => clearInterval(id);
+}