Origit Console

feat: Session B — add payment-utils and refactor payout-export to use them

Timotejcommitted 26 Sep 2026 13:52 UTC d8f549f0bf66d73b3c3d6a2e9e6460246325c313 parent 3ccfc39

Origit record

hash verified Raw JSON
Actor
Bob IDE · config 1d321908db2b
Session
94d788ea8a60c055b001b210820462e6 · 26 Sep 2026 13:51 UTC → 26 Sep 2026 13:52 UTC
Author
Timotej
Approver
bernard · 26 Sep 2026 13:52 UTC
Tests
not run
Record hash
1222f09284a3d1e70e0cf41043174c4b8aff9633986416761996e0b6c8f2598c

Read 4

  • fileSPEC.mdefdc437d50
  • filenode_modules/fast-pay-utils/README.mdsame content as packages/fast-pay-utils/2.1.0/README.md06a32a564a
  • filesrc/payout-export.test.ts303c11ca90
  • filesrc/payout-export.ts8faa6c5faf

Wrote 4

  • filesrc/payment-utils.test.ts
  • filesrc/payment-utils.ts
  • filesrc/payout-export.test.ts
  • filesrc/payout-export.ts

Commands 1

  • shnpm test

Changes

4 file(s)
Show diff
diff --git a/src/payment-utils.test.ts b/src/payment-utils.test.ts
new file mode 100644
index 0000000..4ba360a
--- /dev/null
+++ b/src/payment-utils.test.ts
@@ -0,0 +1,43 @@
+import { formatAmount, maskPan, validatePan } from './payment-utils';
+
+describe('formatAmount', () => {
+  test('formats minor units as decimal with currency prefix', () => {
+    expect(formatAmount(1099, 'EUR')).toBe('EUR 10.99');
+  });
+
+  test('formats whole amounts with two decimal places', () => {
+    expect(formatAmount(1000, 'USD')).toBe('USD 10.00');
+  });
+
+  test('formats zero', () => {
+    expect(formatAmount(0, 'GBP')).toBe('GBP 0.00');
+  });
+});
+
+describe('maskPan', () => {
+  test('keeps first 6 and last 4, masks the rest', () => {
+    expect(maskPan('4111111111111111')).toBe('411111******1111');
+  });
+
+  test('works with different card numbers', () => {
+    expect(maskPan('5500005555555559')).toBe('550000******5559');
+  });
+});
+
+describe('validatePan', () => {
+  test('returns true for a valid Luhn PAN', () => {
+    expect(validatePan('4111111111111111')).toBe(true);
+  });
+
+  test('returns true for another valid PAN', () => {
+    expect(validatePan('5500005555555559')).toBe(true);
+  });
+
+  test('returns false for an invalid PAN', () => {
+    expect(validatePan('1234567890123456')).toBe(false);
+  });
+
+  test('returns false for non-digit input', () => {
+    expect(validatePan('4111abc111111111')).toBe(false);
+  });
+});
diff --git a/src/payment-utils.ts b/src/payment-utils.ts
new file mode 100644
index 0000000..cd53c2b
--- /dev/null
+++ b/src/payment-utils.ts
@@ -0,0 +1,36 @@
+/**
+ * Format a minor-unit integer amount as a decimal string with currency code.
+ * e.g. formatAmount(1099, 'EUR') -> 'EUR 10.99'
+ */
+export function formatAmount(minor: number, currency: string): string {
+  const major = (minor / 100).toFixed(2);
+  return `${currency} ${major}`;
+}
+
+/**
+ * Mask a PAN: keep first 6 and last 4 digits, replace the rest with ******.
+ * e.g. '4111111111111111' -> '411111******1111'
+ */
+export function maskPan(pan: string): string {
+  return pan.replace(/^(\d{6})\d+(\d{4})$/, '$1******$2');
+}
+
+/**
+ * Validate a PAN using the Luhn algorithm.
+ * Returns true if the number passes the check.
+ */
+export function validatePan(pan: string): boolean {
+  if (!/^\d+$/.test(pan)) return false;
+  let sum = 0;
+  let shouldDouble = false;
+  for (let i = pan.length - 1; i >= 0; i--) {
+    let digit = parseInt(pan[i], 10);
+    if (shouldDouble) {
+      digit *= 2;
+      if (digit > 9) digit -= 9;
+    }
+    sum += digit;
+    shouldDouble = !shouldDouble;
+  }
+  return sum % 10 === 0;
+}
diff --git a/src/payout-export.test.ts b/src/payout-export.test.ts
index e440291..1b4162f 100644
--- a/src/payout-export.test.ts
+++ b/src/payout-export.test.ts
@@ -33,7 +33,7 @@ test('exportPayouts includes all payment fields in row', () => {
   const row = csv.split('\n')[1];
   expect(row).toContain('pay_001');
   expect(row).toContain('m_demo_001');
-  expect(row).toContain('1000');
+  expect(row).toContain('EUR 10.00');
   expect(row).toContain('EUR');
   expect(row).toContain('authorised');
 });
diff --git a/src/payout-export.ts b/src/payout-export.ts
index 4834c1f..601ca01 100644
--- a/src/payout-export.ts
+++ b/src/payout-export.ts
@@ -1,5 +1,6 @@
 import { processPayment } from 'fast-pay-utils';
 import { Payment } from './types';
+import { maskPan, formatAmount } from './payment-utils';
 
 /**
  * Export a list of payments as CSV.
@@ -10,13 +11,12 @@ export function exportPayouts(payments: Payment[]): string {
   const header = 'id,merchantId,amount,currency,maskedPan,status';
   const rows = payments.map((p) => {
     const processed = processPayment(p) as Payment;
-    const maskedPan = processed.pan.replace(/^(\d{6})\d+(\d{4})$/, '$1******$2');
     return [
       processed.id,
       processed.merchantId,
-      processed.amountMinor,
+      formatAmount(processed.amountMinor, processed.currency),
       processed.currency,
-      maskedPan,
+      maskPan(processed.pan),
       processed.status,
     ].join(',');
   });