diff --git a/__tests__/BatchInvoiceForm.test.tsx b/__tests__/BatchInvoiceForm.test.tsx
index d0b0224..2b7ed66 100644
--- a/__tests__/BatchInvoiceForm.test.tsx
+++ b/__tests__/BatchInvoiceForm.test.tsx
@@ -182,4 +182,32 @@ GTEST123,1000.00,USDC,3.50,2024-12-31`;
// Should show total amount in summary
expect(screen.getByText('Total Amount')).toBeInTheDocument();
});
+
+ it('rejects/truncates 51-row CSV client-side with clear message matching 50-row contract limit', async () => {
+ render();
+
+ // Build a CSV with 51 rows
+ const header = 'payer,amount,token,discount_rate,due_date';
+ const rows = Array.from({ length: 51 }, (_, i) =>
+ `GTEST${i}EXAMPLE456DEF789GHI012JKL345MNO678PQR901STU234VWX567YZ,1000.00,USDC,3.50,2024-12-31`
+ );
+ const csvContent = [header, ...rows].join('\n');
+
+ const file = new File([csvContent], 'test_51.csv', { type: 'text/csv' });
+ const input = screen.getByLabelText('Click to upload CSV file').querySelector('input');
+
+ if (input) {
+ fireEvent.change(input, { target: { files: [file] } });
+ }
+
+ await waitFor(() => {
+ expect(mockAddToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'warning',
+ title: 'Rows Truncated',
+ message: 'Only the first 50 rows were loaded.',
+ })
+ );
+ });
+ });
});
diff --git a/src/components/BatchInvoiceForm.tsx b/src/components/BatchInvoiceForm.tsx
index 768701e..39d93ef 100644
--- a/src/components/BatchInvoiceForm.tsx
+++ b/src/components/BatchInvoiceForm.tsx
@@ -16,6 +16,7 @@ import {
import { submitInvoicesBatch } from '@/utils/soroban';
import TokenSelector from './TokenSelector';
+// Enforces maximum 50 rows per batch, matching the contract's submit_invoices_batch() instruction (source of truth)
const MAX_BATCH_SIZE = 50;
export interface BatchInvoiceRow extends InvoiceFormValues {
diff --git a/src/components/__tests__/BatchInvoiceForm.test.tsx b/src/components/__tests__/BatchInvoiceForm.test.tsx
index 48c6006..75a900b 100644
--- a/src/components/__tests__/BatchInvoiceForm.test.tsx
+++ b/src/components/__tests__/BatchInvoiceForm.test.tsx
@@ -113,4 +113,26 @@ describe('BatchInvoiceForm', () => {
render();
expect(screen.queryByText(/batch/i)).toBeInTheDocument();
});
+
+ it('rejects 51-row CSV file exceeding contract batch limit with a clear notification', async () => {
+ render();
+ const header = 'payer,amount,token,discount_rate,due_date';
+ const rows = Array.from({ length: 51 }, (_, i) =>
+ `GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC${i % 10},100.00,USDC,3.00,2025-12-31`
+ );
+ const file = new File([[header, ...rows].join('\n')], 'over_limit.csv', { type: 'text/csv' });
+ const input = document.getElementById('csv-upload') as HTMLInputElement | null;
+ if (input) {
+ fireEvent.change(input, { target: { files: [file] } });
+ await waitFor(() => {
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'warning',
+ title: 'Rows Truncated',
+ message: 'Only the first 50 rows were loaded.',
+ })
+ );
+ });
+ }
+ });
});