From bade0f112fca1e9611575a7034ba89d246f200f3 Mon Sep 17 00:00:00 2001 From: asif Date: Thu, 26 Feb 2026 12:52:20 +0530 Subject: [PATCH] Positive Pay fixed, yojna accounts created --- lib/api/services/cheque_service.dart | 26 + lib/data/models/beneficiary.dart | 3 + .../screens/beneficiary_details_screen.dart | 1 + .../cheque/screens/positive_pay_screen.dart | 133 ++++- .../service/screens/service_screen.dart | 48 +- lib/features/yojna/screens/apy_screen.dart | 0 .../yojna/screens/pm_create_screen.dart | 0 .../yojna/screens/pm_main_screen.dart | 147 ++++++ lib/features/yojna/screens/pmsby_screen.dart | 0 lib/main.dart | 4 +- .../account_statement_screen_test.dart | 499 ++++++++++++++++++ 11 files changed, 830 insertions(+), 31 deletions(-) create mode 100644 lib/features/yojna/screens/apy_screen.dart create mode 100644 lib/features/yojna/screens/pm_create_screen.dart create mode 100644 lib/features/yojna/screens/pm_main_screen.dart create mode 100644 lib/features/yojna/screens/pmsby_screen.dart create mode 100644 test/features/accounts/screens/account_statement_screen_test.dart diff --git a/lib/api/services/cheque_service.dart b/lib/api/services/cheque_service.dart index dc3cc1c..fda0ba5 100644 --- a/lib/api/services/cheque_service.dart +++ b/lib/api/services/cheque_service.dart @@ -162,4 +162,30 @@ class ChequeService { ); return response.toString(); } + + Future registerPPS({ + required String cheque_no, + required String account_number, + String? issue_date, + String? amount, + String? payee_name, + required String tpin, + }) async { + final response = await _dio.post( + '/api/pps', + options: Options( + validateStatus: (int? status) => true, + receiveDataWhenStatusError: true, + ), + data: { + 'cheque_no': cheque_no, + 'account_number': account_number, + 'issue_date': issue_date, + 'amount': amount, + 'payee_name': payee_name, + 'tpin': tpin, + }, + ); + return response.toString(); + } } diff --git a/lib/data/models/beneficiary.dart b/lib/data/models/beneficiary.dart index 4521bf0..8389b0a 100644 --- a/lib/data/models/beneficiary.dart +++ b/lib/data/models/beneficiary.dart @@ -6,6 +6,7 @@ class Beneficiary { final String ifscCode; final String? bankName; final String? branchName; + final String? transactionLimit; final String? tpin; Beneficiary({ @@ -16,6 +17,7 @@ class Beneficiary { required this.ifscCode, this.bankName, this.branchName, + this.transactionLimit, this.tpin, }); @@ -30,6 +32,7 @@ class Beneficiary { ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '', bankName: json['bank_name'] ?? json['bankName'] ?? '', branchName: json['branch_name'] ?? json['branchName'] ?? '', + transactionLimit: json['transactionLimit'] ?? '', ); } diff --git a/lib/features/beneficiaries/screens/beneficiary_details_screen.dart b/lib/features/beneficiaries/screens/beneficiary_details_screen.dart index 3a4fc0c..e592a12 100644 --- a/lib/features/beneficiaries/screens/beneficiary_details_screen.dart +++ b/lib/features/beneficiaries/screens/beneficiary_details_screen.dart @@ -119,6 +119,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget { beneficiary.ifscCode), _buildDetailRow('${AppLocalizations.of(context).branchName} ', beneficiary.branchName ?? 'N/A'), + _buildDetailRow("Beneficiary Transactional Limit", beneficiary.transactionLimit ?? 'N/A'), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/features/cheque/screens/positive_pay_screen.dart b/lib/features/cheque/screens/positive_pay_screen.dart index f0c1989..cb45a31 100644 --- a/lib/features/cheque/screens/positive_pay_screen.dart +++ b/lib/features/cheque/screens/positive_pay_screen.dart @@ -1,5 +1,11 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:kmobile/api/services/cheque_service.dart'; import 'package:kmobile/data/models/user.dart'; +import 'package:kmobile/di/injection.dart'; +import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; import 'package:kmobile/l10n/app_localizations.dart'; class PositivePayScreen extends StatefulWidget { @@ -16,25 +22,41 @@ class PositivePayScreen extends StatefulWidget { } class _PositivePayScreenState extends State { + User? _selectedAccount; + List _filteredUsers = []; final _formKey = GlobalKey(); - final _accountNumberController = TextEditingController(); final _chequeNumberController = TextEditingController(); final _chequeDateController = TextEditingController(); final _amountController = TextEditingController(); final _payeeController = TextEditingController(); + final _chequeService = getIt(); @override void initState() { super.initState(); + _filteredUsers = widget.users + .where((user) => ['SA', 'SB', 'CA', 'CC'].contains(user.accountType)) + .toList(); // Pre-fill the account number if possible - if (widget.users.isNotEmpty) { - _accountNumberController.text = widget.users[widget.selectedIndex].accountNo!; + if (widget.users.isNotEmpty && widget.selectedIndex < widget.users.length) { + if (_filteredUsers.isNotEmpty) { + if (_filteredUsers.contains(widget.users[widget.selectedIndex])) { + _selectedAccount = widget.users[widget.selectedIndex]; + } else { + _selectedAccount = _filteredUsers.first; + } + } else { + _selectedAccount = widget.users[widget.selectedIndex]; + } + } else { + if (_filteredUsers.isNotEmpty) { + _selectedAccount = _filteredUsers.first; + } } } @override void dispose() { - _accountNumberController.dispose(); _chequeNumberController.dispose(); _chequeDateController.dispose(); _amountController.dispose(); @@ -42,6 +64,33 @@ class _PositivePayScreenState extends State { super.dispose(); } +Future _showResponseDialog(String title, String message) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text(message), + ], + ), + ), + actions: [ + TextButton( + child: Text(AppLocalizations.of(context).closeButton), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -59,16 +108,26 @@ class _PositivePayScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16.0), - TextFormField( - controller: _accountNumberController, + DropdownButtonFormField( + value: _selectedAccount, decoration: InputDecoration( labelText: AppLocalizations.of(context).accountNumber, border: const OutlineInputBorder(), ), - keyboardType: TextInputType.number, + items: _filteredUsers.map((user) { + return DropdownMenuItem( + value: user, + child: Text(user.accountNo.toString()), + ); + }).toList(), + onChanged: (User? newUser) { + setState(() { + _selectedAccount = newUser; + }); + }, validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter account number'; + if (value == null) { + return AppLocalizations.of(context).accountNumberRequired; } return null; }, @@ -106,7 +165,7 @@ class _PositivePayScreenState extends State { ); if (pickedDate != null) { // Format the date as you wish - String formattedDate = "${pickedDate.day}-${pickedDate.month}-${pickedDate.year}"; + String formattedDate = "${pickedDate.year}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.day.toString().padLeft(2, '0')}"; _chequeDateController.text = formattedDate; } }, @@ -146,8 +205,58 @@ class _PositivePayScreenState extends State { onPressed: () { if (_formKey.currentState!.validate()) { // Process data - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).processingData)), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionPinScreen( + onPinCompleted: (ctx, pin) async { + Navigator.pop(context); + try { + final response = await _chequeService.registerPPS( + cheque_no: _chequeNumberController.text, + account_number: + _selectedAccount!.accountNo!, + issue_date: + _chequeDateController.text, + amount: _amountController.text, + payee_name: _payeeController.text, + tpin: pin, + ); + if (!mounted) return; + String responseString = response.toString(); + if(responseString == 'PPS Registered Successfully'){ + _showResponseDialog('REGISTRATION SUCCESFUL', + 'Your Positive Pay Request has been registered succesfully'); + } + if(responseString.contains('Cheque already registered')){ + _showResponseDialog('ERROR', + 'Your Request for the selected cheque number has already been registered'); + } + if(responseString.contains('INCORRECT_TPIN')){ + _showResponseDialog('Invalid TPIN', + 'The TPIN you entered is incorrect. Please try again.'); + } + } on DioException catch (e) { + try { + final errorBodyString = + e.toString().split('Exception: ')[1]; + final errorBody = jsonDecode(errorBodyString); + if (errorBody.containsKey('error') && + errorBody['error'] == 'INCORRECT_TPIN') { + _showResponseDialog('Invalid TPIN', + 'The TPIN you entered is incorrect. Please try again.'); + } else { + _showResponseDialog( + 'Error', 'Internal Server Error'); + } + } catch (_) { + _showResponseDialog( + 'Error', 'Internal Server Error'); + } + } + }, + ), + ), ); } }, diff --git a/lib/features/service/screens/service_screen.dart b/lib/features/service/screens/service_screen.dart index e72379b..2783143 100644 --- a/lib/features/service/screens/service_screen.dart +++ b/lib/features/service/screens/service_screen.dart @@ -1,6 +1,7 @@ import 'package:kmobile/features/account_opening/screens/account_opening_screen.dart'; import 'package:kmobile/features/card/screens/card_management_screen.dart'; import 'package:kmobile/features/service/screens/atm_locator_screen.dart'; +import 'package:kmobile/features/yojna/screens/pm_main_screen.dart'; import '../../../l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; @@ -71,28 +72,41 @@ class _ServiceScreen extends State { disabled: false, ), ), + // Expanded( + // child: ServiceManagementTile( + // icon: Symbols.box, + // label: "Account Opening", + // onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const AccountOpeningScreen())); + // }, + // disabled: false, + // ), + // ), + // Expanded( + // child: ServiceManagementTile( + // icon: Symbols.credit_card, + // label: AppLocalizations.of(context).cardManagement, + // onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const CardManagementScreen())); + // }, + // disabled: false, + // ), + // ), Expanded( child: ServiceManagementTile( - icon: Symbols.box, - label: "Account Opening", + icon: Symbols.family_group, + label: "Pradhan Mantri Yojna", onTap: () { Navigator.push( - context, + context, MaterialPageRoute( - builder: (context) => const AccountOpeningScreen())); - }, - disabled: false, - ), - ), - Expanded( - child: ServiceManagementTile( - icon: Symbols.credit_card, - label: AppLocalizations.of(context).cardManagement, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CardManagementScreen())); + builder: (context) => const PMMainScreen())); }, disabled: false, ), diff --git a/lib/features/yojna/screens/apy_screen.dart b/lib/features/yojna/screens/apy_screen.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/yojna/screens/pm_create_screen.dart b/lib/features/yojna/screens/pm_create_screen.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/yojna/screens/pm_main_screen.dart b/lib/features/yojna/screens/pm_main_screen.dart new file mode 100644 index 0000000..0788007 --- /dev/null +++ b/lib/features/yojna/screens/pm_main_screen.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:kmobile/l10n/app_localizations.dart'; + +class PMMainScreen extends StatefulWidget { + const PMMainScreen({super.key}); + + @override + State createState() => _PMMainScreenState(); +} + +class _PMMainScreenState extends State { + final TextEditingController _accountController = TextEditingController(); + String? _selectedScheme; + + final List _schemes = [ + 'Pradhan Mantri Jeevan Jyoti Bima Yojana (PMJJBY)', + 'Pradhan Mantri Suraksha Bima Yojana (PMSBY)', + ]; + + @override + void dispose() { + _accountController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Pradhan Mantri Yojana'), + centerTitle: false, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + "Create and Enquire about your Pradhan Mantri Jeevan Jyoti Bima Yojana (PMJJBY) and Pradhan Mantri Suraksha Bima Yojana(PMSBY) schemes.", + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + const SizedBox(height: 16), + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _accountController, + decoration: const InputDecoration( + labelText: 'Account Number', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: _selectedScheme, + decoration: const InputDecoration( + labelText: 'Scheme', + border: OutlineInputBorder(), + ), + items: _schemes.map((String scheme) { + return DropdownMenuItem( + value: scheme, + child: Text( + scheme, + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedScheme = newValue; + }); + }, + isExpanded: true, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + // Action for Create button + }, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(vertical: 16), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Create", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + // Action for Enquiry button + }, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(vertical: 16), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + AppLocalizations.of(context).enquiry, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/yojna/screens/pmsby_screen.dart b/lib/features/yojna/screens/pmsby_screen.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/main.dart b/lib/main.dart index a4e7953..6209eb2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,7 @@ void main() async { ]); // Check for device compromise - // final compromisedMessage = await SecurityService.deviceCompromisedMessage; + // final compromisedMessage = await SecurityService.deviceCompromisedMessage; // if (compromisedMessage != null) { // runApp(MaterialApp( // home: SecurityErrorScreen(message: compromisedMessage), @@ -24,4 +24,4 @@ void main() async { // } await setupDependencies(); runApp(const KMobile()); -} +} \ No newline at end of file diff --git a/test/features/accounts/screens/account_statement_screen_test.dart b/test/features/accounts/screens/account_statement_screen_test.dart new file mode 100644 index 0000000..064a428 --- /dev/null +++ b/test/features/accounts/screens/account_statement_screen_test.dart @@ -0,0 +1,499 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:kmobile/data/models/transaction.dart'; +import 'package:kmobile/data/models/user.dart'; +import 'package:kmobile/data/repositories/transaction_repository.dart'; +import 'package:kmobile/features/accounts/screens/account_statement_screen.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:kmobile/l10n/app_localizations.dart'; + +// ─── Mock TransactionRepository ────────────────────────────────────────────── + +class MockTransactionRepository implements TransactionRepository { + List mockTransactions = []; + bool shouldThrow = false; + String? lastAccountNo; + DateTime? lastFromDate; + DateTime? lastToDate; + int callCount = 0; + + @override + Future> fetchTransactions( + String accountNo, { + DateTime? fromDate, + DateTime? toDate, + }) async { + callCount++; + lastAccountNo = accountNo; + lastFromDate = fromDate; + lastToDate = toDate; + + if (shouldThrow) { + throw Exception('Network error'); + } + return mockTransactions; + } +} + +// ─── Test Helpers ──────────────────────────────────────────────────────────── + +final getIt = GetIt.instance; + +User _createTestUser({ + String accountNo = '1234567890', + String name = 'Test User', + String availableBalance = '50000', + String branchId = 'BR001', + String cifNumber = 'CIF123', + String address = '123 Main Street', +}) { + return User( + accountNo: accountNo, + accountType: 'SB', + branchId: branchId, + currency: 'INR', + availableBalance: availableBalance, + currentBalance: availableBalance, + name: name, + mobileNo: '9876543210', + address: address, + picode: '176215', + cifNumber: cifNumber, + ); +} + +Transaction _createTestTransaction({ + String id = '1', + String name = 'UPI Payment', + String date = '01-01-2026', + String amount = '1000', + String type = 'DR', + String balance = '49000', + String balanceType = 'CR', +}) { + return Transaction( + id: id, + name: name, + date: date, + amount: amount, + type: type, + balance: balance, + balanceType: balanceType, + ); +} + +/// Wraps the widget with MaterialApp + localizations for testing +Widget _buildTestApp({ + required List users, + int selectedIndex = 0, +}) { + return MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('en')], + locale: const Locale('en'), + home: AccountStatementScreen( + users: users, + selectedIndex: selectedIndex, + ), + ); +} + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +void main() { + late MockTransactionRepository mockRepo; + late User testUser; + late User testUser2; + + setUp(() { + // Reset GetIt before each test + getIt.reset(); + + mockRepo = MockTransactionRepository(); + getIt.registerSingleton(mockRepo); + + testUser = _createTestUser(); + testUser2 = _createTestUser( + accountNo: '9876543210', + name: 'Second User', + availableBalance: '75000', + ); + }); + + tearDown(() { + getIt.reset(); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 1: Screen Rendering + // ═══════════════════════════════════════════════════════════════════════════ + group('Screen Rendering', () { + testWidgets('renders AppBar with "Account Statement" title', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('Account Statement'), findsOneWidget); + }); + + testWidgets('renders account number card with label', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('Account Number'), findsOneWidget); + }); + + testWidgets('renders available balance card', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + // The balance text shows ' ₹ 50000' + expect(find.textContaining('50000'), findsOneWidget); + }); + + testWidgets('renders From Date and To Date filter boxes', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('From Date'), findsOneWidget); + expect(find.text('To Date'), findsOneWidget); + }); + + testWidgets('renders Search button', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('Search'), findsOneWidget); + }); + + testWidgets('renders floating action button for PDF download', + (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.byType(FloatingActionButton), findsOneWidget); + expect(find.byIcon(Icons.download), findsOneWidget); + }); + + testWidgets('renders watermark logo with low opacity', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + // There should be an Opacity widget wrapping the logo + final opacityFinder = find.byWidgetPredicate( + (widget) => widget is Opacity && widget.opacity == 0.07, + ); + expect(opacityFinder, findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 2: Account Dropdown + // ═══════════════════════════════════════════════════════════════════════════ + group('Account Dropdown', () { + testWidgets('displays selected user account number in dropdown', + (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); + await tester.pumpAndSettle(); + + expect(find.text('1234567890'), findsOneWidget); + }); + + testWidgets('dropdown shows all user accounts when tapped', + (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); + await tester.pumpAndSettle(); + + // Tap the dropdown + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + + // Both accounts should be visible in the dropdown menu + expect(find.text('1234567890'), findsWidgets); + expect(find.text('9876543210'), findsWidgets); + }); + + testWidgets('selecting another account triggers transaction reload', + (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); + await tester.pumpAndSettle(); + + final initialCallCount = mockRepo.callCount; + + // Open dropdown and select second user + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + + // Tap the second account (appears in the overlay) + await tester.tap(find.text('9876543210').last); + await tester.pumpAndSettle(); + + // Verify repository was called again with the new account + expect(mockRepo.callCount, greaterThan(initialCallCount)); + expect(mockRepo.lastAccountNo, '9876543210'); + }); + + testWidgets('balance updates when user is switched', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); + await tester.pumpAndSettle(); + + expect(find.textContaining('50000'), findsOneWidget); + + // Switch to second user + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('9876543210').last); + await tester.pumpAndSettle(); + + // Balance should now show second user's balance + expect(find.textContaining('75000'), findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 3: Loading State + // ═══════════════════════════════════════════════════════════════════════════ + group('Loading State', () { + testWidgets('shows shimmer loading effect initially', (tester) async { + // Make the repo slow so loading state is visible + mockRepo = MockTransactionRepository(); + getIt.allowReassignment = true; + getIt.registerSingleton(mockRepo); + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + // Don't call pumpAndSettle — we want to see the loading state + await tester.pump(); + + // Check for shimmer placeholders (CircleAvatar is used in loading tiles) + expect(find.byType(CircleAvatar), findsWidgets); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 4: Empty State + // ═══════════════════════════════════════════════════════════════════════════ + group('Empty State', () { + testWidgets('shows "No Transactions" when list is empty', (tester) async { + mockRepo.mockTransactions = []; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('No Transactions'), findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 5: Transaction List + // ═══════════════════════════════════════════════════════════════════════════ + group('Transaction List', () { + testWidgets('displays transaction list when data is loaded', + (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction( + name: 'UPI Payment', + date: '01-01-2026', + amount: '1000', + type: 'DR', + balance: '49000', + ), + _createTestTransaction( + id: '2', + name: 'Salary Credit', + date: '02-01-2026', + amount: '50000', + type: 'CR', + balance: '99000', + ), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('01-01-2026'), findsOneWidget); + expect(find.text('02-01-2026'), findsOneWidget); + expect(find.text('UPI Payment'), findsOneWidget); + expect(find.text('Salary Credit'), findsOneWidget); + }); + + testWidgets('displays amounts with rupee symbol', (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction(amount: '1500', balance: '48500'), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('₹1500'), findsOneWidget); + expect(find.text('Bal: ₹48500'), findsOneWidget); + }); + + testWidgets('truncates long transaction names to 22 characters', + (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction( + name: 'This is a very long transaction name exceeding limit', + ), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + // Only first 22 characters should be displayed + expect(find.text('This is a very long tr'), findsOneWidget); + }); + + testWidgets('shows "Last 10 Transactions" label when no date filter set', + (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction(), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.text('Last 10 Transactions'), findsOneWidget); + }); + + testWidgets('credit transactions show green icon', (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction(type: 'CR'), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + // Find the Icon for credit transactions (call_received) + final iconFinder = find.byWidgetPredicate( + (widget) => + widget is Icon && widget.color == const Color(0xFF10BB10), + ); + expect(iconFinder, findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 6: Error State + // ═══════════════════════════════════════════════════════════════════════════ + group('Error State', () { + testWidgets('shows snackbar on transaction load failure', (tester) async { + mockRepo.shouldThrow = true; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + expect(find.byType(SnackBar), findsOneWidget); + expect(find.textContaining('Failed to load transactions'), findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 7: Date Filters + // ═══════════════════════════════════════════════════════════════════════════ + group('Date Filters', () { + testWidgets('tapping "From Date" opens a date picker', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + await tester.tap(find.text('From Date')); + await tester.pumpAndSettle(); + + // The DatePicker dialog should be open + expect(find.byType(DatePickerDialog), findsOneWidget); + }); + + testWidgets('tapping "To Date" without From Date shows error snackbar', + (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + await tester.tap(find.text('To Date')); + await tester.pumpAndSettle(); + + // Should show snackbar asking to select From Date first + expect(find.byType(SnackBar), findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 8: Search Button + // ═══════════════════════════════════════════════════════════════════════════ + group('Search Button', () { + testWidgets('pressing Search reloads transactions', (tester) async { + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + final callCountBefore = mockRepo.callCount; + + await tester.tap(find.text('Search')); + await tester.pumpAndSettle(); + + expect(mockRepo.callCount, greaterThan(callCountBefore)); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 9: PDF Export + // ═══════════════════════════════════════════════════════════════════════════ + group('PDF Export', () { + testWidgets( + 'pressing download FAB with no transactions shows snackbar', + (tester) async { + mockRepo.mockTransactions = []; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + expect(find.text('No transactions to export.'), findsOneWidget); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 10: Selected Index + // ═══════════════════════════════════════════════════════════════════════════ + group('Selected Index', () { + testWidgets('uses selectedIndex to pick initial user', (tester) async { + await tester.pumpWidget( + _buildTestApp(users: [testUser, testUser2], selectedIndex: 1), + ); + await tester.pumpAndSettle(); + + // Second user's balance should be shown + expect(find.textContaining('75000'), findsOneWidget); + + // Repository should have been called with second user's account + expect(mockRepo.lastAccountNo, '9876543210'); + }); + }); + + // ═══════════════════════════════════════════════════════════════════════════ + // GROUP 11: Navigation + // ═══════════════════════════════════════════════════════════════════════════ + group('Navigation', () { + testWidgets('tapping a transaction navigates to details screen', + (tester) async { + mockRepo.mockTransactions = [ + _createTestTransaction(name: 'Test Payment'), + ]; + + await tester.pumpWidget(_buildTestApp(users: [testUser])); + await tester.pumpAndSettle(); + + // Tap on the transaction ListTile + await tester.tap(find.text('Test Payment')); + await tester.pumpAndSettle(); + + // We should have navigated away from the statement screen + // The TransactionDetailsScreen should now be visible + expect(find.byType(AccountStatementScreen), findsNothing); + }); + }); +}