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(',');
});