From 18487e7a2e9a4ce51eae4a624cc165e04a3a9d05 Mon Sep 17 00:00:00 2001 From: orangdeso Date: Thu, 3 Apr 2025 18:41:49 +0700 Subject: [PATCH] Feat: done post data for transaction --- .dart_tool/package_config.json | 38 +- .dart_tool/package_config_subset | 24 ++ .../transaction_repository_impl.dart | 407 ++++++++++++++++++ lib/domain/bindings/transaction_binding.dart | 25 ++ lib/domain/models/transaction_model.dart | 73 ++++ .../repositories/transaction_repository.dart | 40 ++ lib/domain/usecases/transaction_usecase.dart | 81 ++++ lib/main.dart | 6 + .../controllers/transaction_controller.dart | 172 ++++++++ .../pages/ticket_booking_step4_screen.dart | 155 ++++++- .../screens/routes/app_rountes.dart | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 48 +++ pubspec.yaml | 2 + 14 files changed, 1054 insertions(+), 21 deletions(-) create mode 100644 lib/data/repositories/transaction_repository_impl.dart create mode 100644 lib/domain/bindings/transaction_binding.dart create mode 100644 lib/domain/models/transaction_model.dart create mode 100644 lib/domain/repositories/transaction_repository.dart create mode 100644 lib/domain/usecases/transaction_usecase.dart create mode 100644 lib/presentation/controllers/transaction_controller.dart diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 4c61743..8aa3312 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -181,6 +181,24 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "firebase_database", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database-11.3.5", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "firebase_database_platform_interface", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_platform_interface-0.2.6+5", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "firebase_database_web", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_web-0.2.6+11", + "packageUri": "lib/", + "languageVersion": "3.4" + }, { "name": "firebase_storage", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage-12.4.5", @@ -199,6 +217,12 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "fixnum", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/fixnum-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, { "name": "flutter", "rootUri": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0/packages/flutter", @@ -487,6 +511,12 @@ "packageUri": "lib/", "languageVersion": "2.18" }, + { + "name": "sprintf", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/sprintf-7.0.0", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "stack_trace", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1", @@ -523,6 +553,12 @@ "packageUri": "lib/", "languageVersion": "3.5" }, + { + "name": "uuid", + "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/uuid-4.5.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, { "name": "vector_graphics", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18", @@ -596,7 +632,7 @@ "languageVersion": "3.4" } ], - "generated": "2025-03-31T15:19:30.948302Z", + "generated": "2025-04-01T21:50:03.833642Z", "generator": "pub", "generatorVersion": "3.5.0", "flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0", diff --git a/.dart_tool/package_config_subset b/.dart_tool/package_config_subset index 63478b5..a002857 100644 --- a/.dart_tool/package_config_subset +++ b/.dart_tool/package_config_subset @@ -118,6 +118,18 @@ firebase_core_web 3.4 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core_web-2.22.0/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core_web-2.22.0/lib/ +firebase_database +3.2 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database-11.3.5/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database-11.3.5/lib/ +firebase_database_platform_interface +3.2 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_platform_interface-0.2.6+5/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_platform_interface-0.2.6+5/lib/ +firebase_database_web +3.4 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_web-0.2.6+11/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_database_web-0.2.6+11/lib/ firebase_storage 3.2 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage-12.4.5/ @@ -130,6 +142,10 @@ firebase_storage_web 3.4 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage_web-3.10.12/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage_web-3.10.12/lib/ +fixnum +3.1 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/fixnum-1.1.1/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/fixnum-1.1.1/lib/ flutter_picker 2.12 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_picker-2.1.0/ @@ -306,6 +322,10 @@ source_span 2.18 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.0/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.0/lib/ +sprintf +2.12 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/sprintf-7.0.0/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/sprintf-7.0.0/lib/ stack_trace 2.18 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1/ @@ -330,6 +350,10 @@ typed_data 3.5 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0/lib/ +uuid +3.0 +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/uuid-4.5.1/ +file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/uuid-4.5.1/lib/ vector_graphics 3.4 file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18/ diff --git a/lib/data/repositories/transaction_repository_impl.dart b/lib/data/repositories/transaction_repository_impl.dart new file mode 100644 index 0000000..857a4cb --- /dev/null +++ b/lib/data/repositories/transaction_repository_impl.dart @@ -0,0 +1,407 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:e_porter/domain/models/transaction_model.dart'; +import 'package:e_porter/domain/repositories/transaction_repository.dart'; +import 'package:path/path.dart' as path; + +class TransactionRepositoryImpl implements TransactionRepository { + final FirebaseFirestore _firestore; + final FirebaseStorage _storage; + + TransactionRepositoryImpl({ + FirebaseFirestore? firestore, + FirebaseStorage? storage, + }) : _firestore = firestore ?? FirebaseFirestore.instance, + _storage = storage ?? FirebaseStorage.instance; + + // Method untuk generate ID unik + String _generateUniqueId() { + return DateTime.now().millisecondsSinceEpoch.toString() + + '_' + + (100000 + (DateTime.now().microsecond % 900000)).toString(); + } + + @override + Future createTransaction({ + required String ticketId, + required String flightId, + required double amount, + required String method, + required DateTime expiryTime, + required Map flightDetails, + required Map bandaraDetails, + Map? porterServiceDetails, + required Map userDetails, + required int passenger, + required List> passengerDetails, + required List numberSeat, + }) async { + try { + final transactionId = _generateUniqueId(); + final now = DateTime.now(); + + final transactionData = { + 'id': transactionId, + 'ticketId': ticketId, + 'flightId': flightId, + 'amount': amount, + 'method': method, + 'status': 'pending', + 'createdAt': now, + 'expiryTime': expiryTime, + 'flightDetails': flightDetails, + 'bandaraDetails': bandaraDetails, + 'porterServiceDetails': porterServiceDetails, + 'userDetails': userDetails, + 'passenger': passenger, + 'passengerDetails': passengerDetails, + 'numberSeat': numberSeat, + }; + + await _firestore + .collection('tickets') + .doc(ticketId) + .collection('payments') + .doc(transactionId) + .set(transactionData); + + DocumentSnapshot seatData = + await _firestore.collection('tickets').doc(ticketId).collection('flights').doc(flightId).get(); + + if (seatData.exists) { + Map? data = seatData.data() as Map?; + + // Fungsi untuk mendapatkan kelas seat dari nomor kursi (misalnya "A2" -> "a") + String getSeatClass(String seatNumber) { + // Ambil huruf pertama dan ubah ke lowercase + if (seatNumber.isNotEmpty) { + return seatNumber[0].toLowerCase(); + } + return "a"; // Default ke "a" jika format tidak sesuai + } + + // Fungsi untuk mendapatkan indeks seat dari nomor kursi (misalnya "A2" -> 2) + int getSeatIndex(String seatNumber) { + if (seatNumber.length > 1) { + try { + return int.parse(seatNumber.substring(1)) - 1; // Konversi ke berbasis-0 + } catch (e) { + log("Error parsing seat index: $e"); + } + } + return 0; // Default ke 0 jika format tidak sesuai + } + + // Perbarui status kursi untuk setiap seat yang dipilih + for (String seatNumber in numberSeat) { + String seatClass = getSeatClass(seatNumber); + int seatIndex = getSeatIndex(seatNumber); + + log("Processing seat: $seatNumber, class: $seatClass, index: $seatIndex"); + + // Pastikan struktur data seat tersedia + if (data != null && data.containsKey('seat')) { + Map seatData = Map.from(data['seat']); + + // Pastikan kelas kursi (a-f) tersedia + if (seatData.containsKey(seatClass)) { + Map classSeatData = Map.from(seatData[seatClass]); + + // Pastikan array isTaken tersedia + if (classSeatData.containsKey('isTaken')) { + List isTaken = List.from(classSeatData['isTaken'] ?? []); + + // Perbarui array isTaken + while (isTaken.length <= seatIndex) { + isTaken.add(false); // Tambahkan seat yang belum ada dengan false + } + isTaken[seatIndex] = true; // Set kursi yang dipilih sebagai 'taken' + + // Update Firestore untuk kelas kursi tertentu + await _firestore + .collection('tickets') + .doc(ticketId) + .collection('flights') + .doc(flightId) + .update({'seat.$seatClass.isTaken': isTaken}); + + log("Successfully updated seat $seatNumber in class $seatClass at index $seatIndex"); + } else { + // Jika 'isTaken' tidak ada, buat array baru + List isTaken = List.filled(seatIndex + 1, false); + isTaken[seatIndex] = true; + + await _firestore + .collection('tickets') + .doc(ticketId) + .collection('flights') + .doc(flightId) + .update({'seat.$seatClass.isTaken': isTaken}); + + log("Created new isTaken array for seat $seatNumber in class $seatClass"); + } + } else { + // Jika kelas kursi tidak ada, buat struktur baru + List isTaken = List.filled(seatIndex + 1, false); + isTaken[seatIndex] = true; + + await _firestore.collection('tickets').doc(ticketId).collection('flights').doc(flightId).update({ + 'seat.$seatClass': {'isTaken': isTaken} + }); + + log("Created new seat class $seatClass for seat $seatNumber"); + } + } else { + // Jika struktur 'seat' tidak ada, buat struktur lengkap + List isTaken = List.filled(seatIndex + 1, false); + isTaken[seatIndex] = true; + + Map newSeatData = { + 'seat': { + seatClass: {'isTaken': isTaken} + } + }; + + await _firestore + .collection('tickets') + .doc(ticketId) + .collection('flights') + .doc(flightId) + .set(newSeatData, SetOptions(merge: true)); + + log("Created complete new seat structure for seat $seatNumber"); + } + } + } else { + log("Flight document not found"); + throw Exception("Flight document not found"); + } + + final databaseRef = FirebaseDatabase.instance.ref(); + final userId = userDetails['uid']; + await databaseRef.child('transactions/$userId/$ticketId/$transactionId').set({ + 'payment': { + 'id': transactionId, + 'status': 'pending', + 'amount': amount, + 'method': method, + 'createdAt': now.millisecondsSinceEpoch, + 'expiryTime': expiryTime.millisecondsSinceEpoch, + }, + 'flight': flightDetails, + 'bandara': bandaraDetails, + 'porterService': porterServiceDetails, + 'user': userDetails, + 'passenger': passenger, + 'passengerDetails': passengerDetails, + 'numberSeat': numberSeat, + }); + + return transactionId; + } catch (e) { + throw Exception('Failed to create transaction: $e'); + } + } + + @override + Future updateTransactionStatus({ + required String ticketId, + required String transactionId, + required String status, + required String userId, + }) async { + try { + final now = DateTime.now(); + + await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({ + 'status': status, + 'updatedAt': now, + }); + + // Update status dokumen tiket utama + await _firestore.collection('tickets').doc(ticketId).update({ + 'status': status == 'paid' ? 'awaiting_verification' : status, + 'lastUpdated': now, + }); + + // Update status di Realtime Database + final databaseRef = FirebaseDatabase.instance.ref(); + await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({ + 'status': status, + 'updatedAt': now.millisecondsSinceEpoch, + }); + } catch (e) { + throw Exception('Failed to update transaction status: $e'); + } + } + + @override + Future uploadPaymentProof( + {required String ticketId, required String transactionId, required File proofImage}) async { + try { + final fileName = + 'payment_proof_${transactionId}_${DateTime.now().millisecondsSinceEpoch}${path.extension(proofImage.path)}'; + final storageRef = _storage.ref().child('payment/$fileName'); + + // Upload file ke Firebase Storage + final uploadTask = await storageRef.putFile(proofImage); + final downloadUrl = await uploadTask.ref.getDownloadURL(); + + // Update di Firestore dengan URL bukti pembayaran + await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({ + 'proofUrl': downloadUrl, + 'status': 'paid', + 'paidAt': DateTime.now(), + }); + + // Update status dokumen tiket utama + await _firestore.collection('tickets').doc(ticketId).update({ + 'status': 'awaiting_verification', + 'lastUpdated': DateTime.now(), + }); + + // Update di Realtime Database + final databaseRef = FirebaseDatabase.instance.ref(); + await databaseRef.child('transactions/$ticketId/payment').update({ + 'proofUrl': downloadUrl, + 'status': 'paid', + 'paidAt': DateTime.now().millisecondsSinceEpoch, + }); + } catch (e) { + throw Exception('Failed to upload payment proof: $e'); + } + } + + @override + Future> getTransactionsByUserId(String userId) async { + try { + // Mencari tiket berdasarkan userId + final ticketQuerySnapshot = await _firestore.collection('tickets').where('userId', isEqualTo: userId).get(); + + final List transactions = []; + + // Untuk setiap tiket, ambil data payment + for (var ticketDoc in ticketQuerySnapshot.docs) { + final ticketId = ticketDoc.id; + final paymentsSnapshot = await _firestore.collection('tickets').doc(ticketId).collection('payments').get(); + + for (var paymentDoc in paymentsSnapshot.docs) { + final data = paymentDoc.data(); + data['id'] = paymentDoc.id; + transactions.add(TransactionModel.fromJson(data)); + } + } + + return transactions; + } catch (e) { + throw Exception('Failed to get transactions: $e'); + } + } + + @override + Future getTransactionById({required String ticketId, required String transactionId}) async { + try { + final docSnapshot = + await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).get(); + + if (docSnapshot.exists) { + final data = docSnapshot.data()!; + data['id'] = docSnapshot.id; + return TransactionModel.fromJson(data); + } + + return null; + } catch (e) { + throw Exception('Failed to get transaction: $e'); + } + } + + @override + Future syncTransactionToRealtimeDB({required String ticketId, required String transactionId}) async { + try { + final transaction = await getTransactionById(ticketId: ticketId, transactionId: transactionId); + + if (transaction != null) { + final databaseRef = FirebaseDatabase.instance.ref(); + await databaseRef.child('transactions/$ticketId').set({ + 'payment': { + 'id': transaction.id, + 'status': transaction.status, + 'amount': transaction.amount, + 'method': transaction.method, + 'proofUrl': transaction.proofUrl, + 'createdAt': transaction.createdAt.millisecondsSinceEpoch, + 'expiryTime': transaction.expiryTime.millisecondsSinceEpoch, + }, + 'flight': transaction.flightDetails, + 'bandara': transaction.bandaraDetails, + 'porterService': transaction.porterServiceDetails, + 'user': transaction.userDetails, + 'passenger': transaction.passenger, + }); + } + } catch (e) { + throw Exception('Failed to sync transaction to Realtime DB: $e'); + } + } + + @override + Stream watchTransaction({required String ticketId, required String transactionId}) { + return _firestore + .collection('tickets') + .doc(ticketId) + .collection('payments') + .doc(transactionId) + .snapshots() + .map((snapshot) { + if (snapshot.exists) { + final data = snapshot.data()!; + data['id'] = snapshot.id; + return TransactionModel.fromJson(data); + } + return null; + }); + } + + Future checkAndCancelExpiredTransactions() async { + try { + final now = DateTime.now(); + final pendingTransactionsSnapshot = + await _firestore.collectionGroup('payments').where('status', isEqualTo: 'pending').get(); + + for (var doc in pendingTransactionsSnapshot.docs) { + final data = doc.data(); + final expiryTime = (data['expiryTime'] as Timestamp).toDate(); + + if (expiryTime.isBefore(now)) { + final ticketId = data['ticketId']; + final transactionId = doc.id; + final userId = data['userDetails']['uid']; + + await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({ + 'status': 'cancelled', + 'updatedAt': now, + }); + + await _firestore.collection('tickets').doc(ticketId).update({ + 'status': 'cancelled', + 'lastUpdated': now, + }); + + final databaseRef = FirebaseDatabase.instance.ref(); + await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({ + 'status': 'cancelled', + 'updatedAt': now.millisecondsSinceEpoch, + }); + + log('Transaction $transactionId cancelled due to expiry'); + } + } + } catch (e) { + log('Error checking expired transactions: $e'); + } + } +} diff --git a/lib/domain/bindings/transaction_binding.dart b/lib/domain/bindings/transaction_binding.dart new file mode 100644 index 0000000..abf7d14 --- /dev/null +++ b/lib/domain/bindings/transaction_binding.dart @@ -0,0 +1,25 @@ +import 'package:get/get.dart'; +import 'package:e_porter/domain/repositories/transaction_repository.dart'; +import 'package:e_porter/data/repositories/transaction_repository_impl.dart'; +import 'package:e_porter/domain/usecases/transaction_usecase.dart'; +import 'package:e_porter/presentation/controllers/transaction_controller.dart'; + +class TransactionBinding extends Bindings { + @override + void dependencies() { + // Repository + Get.lazyPut( + () => TransactionRepositoryImpl(), + ); + + // UseCase + Get.lazyPut( + () => TransactionUseCase(Get.find()), + ); + + // Controller + Get.lazyPut( + () => TransactionController(Get.find()), + ); + } +} \ No newline at end of file diff --git a/lib/domain/models/transaction_model.dart b/lib/domain/models/transaction_model.dart new file mode 100644 index 0000000..00397fd --- /dev/null +++ b/lib/domain/models/transaction_model.dart @@ -0,0 +1,73 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class TransactionModel { + final String id; + final String ticketId; + final String flightId; + final double amount; + final String method; + final String status; + final String? proofUrl; + final DateTime createdAt; + final DateTime expiryTime; + final Map flightDetails; + final Map bandaraDetails; + final Map? porterServiceDetails; + final Map userDetails; + final int passenger; + + TransactionModel({ + required this.id, + required this.ticketId, + required this.flightId, + required this.amount, + required this.method, + required this.status, + this.proofUrl, + required this.createdAt, + required this.expiryTime, + required this.flightDetails, + required this.bandaraDetails, + this.porterServiceDetails, + required this.userDetails, + required this.passenger, + }); + + factory TransactionModel.fromJson(Map json) { + return TransactionModel( + id: json['id'] ?? '', + ticketId: json['ticketId'] ?? '', + flightId: json['flightId'] ?? '', + amount: (json['amount'] ?? 0.0).toDouble(), + method: json['method'] ?? '', + status: json['status'] ?? 'pending', + proofUrl: json['proofUrl'], + createdAt: (json['createdAt'] as Timestamp).toDate(), + expiryTime: (json['expiryTime'] as Timestamp).toDate(), + flightDetails: json['flightDetails'] ?? {}, + bandaraDetails: json['bandaraDetails'] ?? {}, + porterServiceDetails: json['porterServiceDetails'], + userDetails: json['userDetails'] ?? {}, + passenger: json['passenger'] ?? 0, + ); + } + + Map toJson() { + return { + 'id': id, + 'ticketId': ticketId, + 'flightId': flightId, + 'amount': amount, + 'method': method, + 'status': status, + 'proofUrl': proofUrl, + 'createdAt': createdAt, + 'expiryTime': expiryTime, + 'flightDetails': flightDetails, + 'bandaraDetails': bandaraDetails, + 'porterServiceDetails': porterServiceDetails, + 'userDetails': userDetails, + 'passenger': passenger, + }; + } +} \ No newline at end of file diff --git a/lib/domain/repositories/transaction_repository.dart b/lib/domain/repositories/transaction_repository.dart new file mode 100644 index 0000000..93465f7 --- /dev/null +++ b/lib/domain/repositories/transaction_repository.dart @@ -0,0 +1,40 @@ +import 'dart:io'; +import 'package:e_porter/domain/models/transaction_model.dart'; + +abstract class TransactionRepository { + Future createTransaction({ + required String ticketId, + required String flightId, + required double amount, + required String method, + required DateTime expiryTime, + required Map flightDetails, + required Map bandaraDetails, + Map? porterServiceDetails, + required Map userDetails, + required int passenger, + required List> passengerDetails, + required List numberSeat, + }); + + Future updateTransactionStatus({ + required String ticketId, + required String transactionId, + required String status, + required String userId, + }); + + Future uploadPaymentProof({ + required String ticketId, + required String transactionId, + required File proofImage, + }); + + Future> getTransactionsByUserId(String userId); + + Future getTransactionById({required String ticketId, required String transactionId}); + + Future syncTransactionToRealtimeDB({required String ticketId, required String transactionId}); + + Stream watchTransaction({required String ticketId, required String transactionId}); +} diff --git a/lib/domain/usecases/transaction_usecase.dart b/lib/domain/usecases/transaction_usecase.dart new file mode 100644 index 0000000..2340b09 --- /dev/null +++ b/lib/domain/usecases/transaction_usecase.dart @@ -0,0 +1,81 @@ +import 'dart:io'; +import 'package:e_porter/domain/models/transaction_model.dart'; +import 'package:e_porter/domain/repositories/transaction_repository.dart'; + +class TransactionUseCase { + final TransactionRepository _repository; + + TransactionUseCase(this._repository); + + Future createTransaction({ + required String ticketId, + required String flightId, + required double amount, + required String method, + required DateTime expiryTime, + required Map flightDetails, + required Map bandaraDetails, + Map? porterServiceDetails, + required Map userDetails, + required int passenger, + required List> passengerDetails, // Tambah parameter ini + required List numberSeat, // Tambah parameter ini + }) { + return _repository.createTransaction( + ticketId: ticketId, + flightId: flightId, + amount: amount, + method: method, + expiryTime: expiryTime, + flightDetails: flightDetails, + bandaraDetails: bandaraDetails, + porterServiceDetails: porterServiceDetails, + userDetails: userDetails, + passenger: passenger, + passengerDetails: passengerDetails, // Tambahkan parameter ini + numberSeat: numberSeat, // Tambahkan parameter ini + ); + } + + Future updateTransactionStatus({ + required String ticketId, + required String transactionId, + required String status, + required String userId, + }) { + return _repository.updateTransactionStatus( + ticketId: ticketId, + transactionId: transactionId, + status: status, + userId: userId, + ); + } + + Future uploadPaymentProof({ + required String ticketId, + required String transactionId, + required File proofImage, + }) { + return _repository.uploadPaymentProof( + ticketId: ticketId, + transactionId: transactionId, + proofImage: proofImage, + ); + } + + Future> getTransactionsByUserId(String userId) { + return _repository.getTransactionsByUserId(userId); + } + + Future getTransactionById({required String ticketId, required String transactionId}) { + return _repository.getTransactionById(ticketId: ticketId, transactionId: transactionId); + } + + Future syncTransactionToRealtimeDB({required String ticketId, required String transactionId}) { + return _repository.syncTransactionToRealtimeDB(ticketId: ticketId, transactionId: transactionId); + } + + Stream watchTransaction({required String ticketId, required String transactionId}) { + return _repository.watchTransaction(ticketId: ticketId, transactionId: transactionId); + } +} diff --git a/lib/main.dart b/lib/main.dart index c5f7bf6..0d92bc0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,19 @@ import 'dart:developer'; +import 'package:e_porter/data/repositories/transaction_repository_impl.dart'; import 'package:e_porter/presentation/screens/routes/app_rountes.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import '_core/service/transaction_expiry_service.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); + + final transactionRepo = Get.put(TransactionRepositoryImpl()); + TransactionExpiryService().initialize(transactionRepo); log("Firebase Initialized Successfully!"); runApp(MyApp(initialRoute: Routes.SPLASH)); diff --git a/lib/presentation/controllers/transaction_controller.dart b/lib/presentation/controllers/transaction_controller.dart new file mode 100644 index 0000000..923cdc1 --- /dev/null +++ b/lib/presentation/controllers/transaction_controller.dart @@ -0,0 +1,172 @@ +import 'dart:io'; +import 'package:get/get.dart'; +import 'package:e_porter/domain/models/transaction_model.dart'; +import 'package:e_porter/domain/usecases/transaction_usecase.dart'; +import 'package:e_porter/_core/service/logger_service.dart'; + +class TransactionController extends GetxController { + final TransactionUseCase _transactionUseCase; + + TransactionController(this._transactionUseCase); + + final Rx currentTransaction = Rx(null); + final RxBool isLoading = false.obs; + final RxString error = ''.obs; + + Future createTransaction({ + required String ticketId, + required String flightId, + required double amount, + required String method, + required DateTime expiryTime, + required Map flightDetails, + required Map bandaraDetails, + Map? porterServiceDetails, + required Map userDetails, + required int passenger, + required List> passengerDetails, // Tambah parameter ini + required List numberSeat, // Tambah parameter ini + }) async { + try { + isLoading.value = true; + error.value = ''; + + final transactionId = await _transactionUseCase.createTransaction( + ticketId: ticketId, + flightId: flightId, + amount: amount, + method: method, + expiryTime: expiryTime, + flightDetails: flightDetails, + bandaraDetails: bandaraDetails, + porterServiceDetails: porterServiceDetails, + userDetails: userDetails, + passenger: passenger, + passengerDetails: passengerDetails, + numberSeat: numberSeat, + ); + + // Ambil data transaksi setelah berhasil dibuat + final transaction = await _transactionUseCase.getTransactionById( + ticketId: ticketId, + transactionId: transactionId, + ); + + currentTransaction.value = transaction; + return transactionId; + } catch (e) { + logger.e('Gagal membuat transaksi: $e'); + error.value = 'Gagal membuat transaksi: $e'; + throw Exception('Gagal membuat transaksi: $e'); + } finally { + isLoading.value = false; + } + } + + Future uploadPaymentProof( + {required String ticketId, required String transactionId, required File proofImage}) async { + try { + isLoading.value = true; + error.value = ''; + + await _transactionUseCase.uploadPaymentProof( + ticketId: ticketId, transactionId: transactionId, proofImage: proofImage); + + // Refresh data transaksi setelah bukti pembayaran diunggah + final transaction = + await _transactionUseCase.getTransactionById(ticketId: ticketId, transactionId: transactionId); + + currentTransaction.value = transaction; + } catch (e) { + logger.e('Gagal mengunggah bukti pembayaran: $e'); + error.value = 'Gagal mengunggah bukti pembayaran: $e'; + throw Exception('Gagal mengunggah bukti pembayaran: $e'); + } finally { + isLoading.value = false; + } + } + + Future> getTransactionsByUserId(String userId) async { + try { + isLoading.value = true; + error.value = ''; + + final transactions = await _transactionUseCase.getTransactionsByUserId(userId); + return transactions; + } catch (e) { + logger.e('Gagal mendapatkan daftar transaksi: $e'); + error.value = 'Gagal mendapatkan daftar transaksi: $e'; + throw Exception('Gagal mendapatkan daftar transaksi: $e'); + } finally { + isLoading.value = false; + } + } + + Future getTransactionById({required String ticketId, required String transactionId}) async { + try { + isLoading.value = true; + error.value = ''; + + final transaction = + await _transactionUseCase.getTransactionById(ticketId: ticketId, transactionId: transactionId); + + currentTransaction.value = transaction; + } catch (e) { + logger.e('Gagal mendapatkan detail transaksi: $e'); + error.value = 'Gagal mendapatkan detail transaksi: $e'; + throw Exception('Gagal mendapatkan detail transaksi: $e'); + } finally { + isLoading.value = false; + } + } + + void watchTransaction({required String ticketId, required String transactionId}) { + try { + _transactionUseCase.watchTransaction(ticketId: ticketId, transactionId: transactionId).listen( + (transaction) { + currentTransaction.value = transaction; + }, + onError: (e) { + logger.e('Error mendengarkan perubahan transaksi: $e'); + error.value = 'Error mendengarkan perubahan transaksi: $e'; + }, + ); + } catch (e) { + logger.e('Gagal memantau transaksi: $e'); + error.value = 'Gagal memantau transaksi: $e'; + } + } + + Future updateTransactionStatus({ + required String ticketId, + required String transactionId, + required String status, + required String userId, + }) async { + try { + isLoading.value = true; + error.value = ''; + + await _transactionUseCase.updateTransactionStatus( + ticketId: ticketId, + transactionId: transactionId, + status: status, + userId: userId, + ); + + // Refresh data transaksi setelah status diupdate + final transaction = await _transactionUseCase.getTransactionById( + ticketId: ticketId, + transactionId: transactionId, + ); + + currentTransaction.value = transaction; + } catch (e) { + logger.e('Gagal mengupdate status transaksi: $e'); + error.value = 'Gagal mengupdate status transaksi: $e'; + throw Exception('Gagal mengupdate status transaksi: $e'); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart b/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart index 4d08ef3..9282605 100644 --- a/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart +++ b/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart @@ -13,10 +13,13 @@ import 'package:intl/intl.dart'; import '../../../../_core/component/appbar/appbar_component.dart'; import '../../../../_core/component/icons/icons_library.dart'; import '../../../../_core/service/logger_service.dart'; +import '../../../../_core/service/preferences_service.dart'; +import '../../../../_core/utils/snackbar/snackbar_helper.dart'; import '../../../../domain/models/porter_service_model.dart'; import '../../../../domain/models/ticket_model.dart'; import '../../../../domain/models/user_entity.dart'; import '../../../controllers/ticket_controller.dart'; +import '../../../controllers/transaction_controller.dart'; import '../component/card_flight_information.dart'; class TicketBookingStep4Screen extends StatefulWidget { @@ -41,6 +44,7 @@ class _TicketBookingStep4ScreenState extends State { final double serviceCharge = 10000.0; final TicketController ticketController = Get.find(); + final TransactionController transactionController = Get.find(); FlightModel? flightData; String? departureTime; String? arrivalTime; @@ -218,26 +222,137 @@ class _TicketBookingStep4ScreenState extends State { price: "Rp ${NumberFormat.decimalPattern('id_ID').format(totalAll())}", labelButton: "Buat Pesanan", iconButton: CustomeIcons.ProtectOutline(color: Colors.white), - onTap: () { - final DateTime currentTime = DateTime.now(); - final DateTime expiryTime = currentTime.add(Duration(hours: 1)); - final String formattedExpiryTime = - "${DateFormat('dd MMMM yyyy', 'en_US').format(expiryTime)}, ${DateFormat.Hm().format(expiryTime)}"; - final argument = { - 'ticketId': ticketId, - 'flightId': flightId, - 'date': ticketDate, - 'passenger': passenger, - 'selectedPassenger': selectedPassengers, - 'numberSeat': numberSeat, - 'totalPrice': totalPrice, - 'grandTotal': grandTotal, - 'selectedServiceLabels': selectedServiceLabels, - 'selectedPorterServices': selectedPorterServices, - 'totalAll': totalAll(), - 'expiryTime': formattedExpiryTime, - }; - Get.toNamed(Routes.PAYMENT, arguments: argument); + onTap: () async { + try { + Get.dialog( + Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + + final userData = await PreferencesService.getUserData(); + if (userData == null) { + Get.back(); // close dialog + SnackbarHelper.showError('Error', 'Data pengguna tidak ditemukan'); + return; + } + + // Persiapkan data expiry time + final DateTime currentTime = DateTime.now(); + final DateTime expiryTime = currentTime.add(Duration(seconds: 5)); + + // Persiapkan data bandara + final bandaraData = { + 'departure': { + 'code': flightData?.codeDeparture, + 'city': flightData?.cityDeparture, + }, + 'arrival': { + 'code': flightData?.codeArrival, + 'city': flightData?.cityArrival, + }, + }; + + // Persiapkan data flight + final flightDataMap = { + 'airLines': flightData?.airLines, + 'code': flightData?.code, + 'cityDeparture': flightData?.cityDeparture, + 'cityArrival': flightData?.cityArrival, + 'codeDeparture': flightData?.codeDeparture, + 'codeArrival': flightData?.codeArrival, + 'departureTime': departureTime, + 'arrivalTime': arrivalTime, + 'flightClass': flightData?.flightClass, + 'transitAirplane': flightData?.transitAirplane, + 'stop': flightData?.stop, + 'airlineLogo': flightData?.airlineLogo, + 'price': flightData?.price, + }; + + // Persiapkan data porter service jika ada + Map? porterServiceData; + if (selectedPorterServices.isNotEmpty) { + porterServiceData = {}; + selectedPorterServices.forEach((key, value) { + if (value != null) { + porterServiceData![key] = { + 'name': value.name, + 'price': value.price, + 'description': value.description, + }; + } + }); + } + + // Persiapkan data user + final userDetailData = { + 'uid': userData.uid, + 'name': userData.name, + 'email': userData.email, + 'phone': userData.phone, + }; + + final List> passengerDetailsList = []; + for (var passenger in selectedPassengers) { + if (passenger != null) { + passengerDetailsList.add({ + 'name': passenger.name, + 'typeId': passenger.typeId, + 'noId': passenger.noId, + 'gender': passenger.gender, + }); + } + } + + // Buat transaksi + final transactionId = await transactionController.createTransaction( + ticketId: ticketId, + flightId: flightId, + amount: totalAll(), + method: 'QRIS', + expiryTime: expiryTime, + flightDetails: flightDataMap, + bandaraDetails: bandaraData, + porterServiceDetails: porterServiceData, + userDetails: userDetailData, + passenger: passenger, + passengerDetails: passengerDetailsList, + numberSeat: numberSeat, + ); + + // Tutup dialog loading + Get.back(); + + // Format expiry time untuk tampilan + final formattedExpiryTime = + "${DateFormat('dd MMMM yyyy', 'en_US').format(expiryTime)}, ${DateFormat.Hm().format(expiryTime)}"; + + // Navigasi ke halaman pembayaran + final argument = { + 'ticketId': ticketId, + 'flightId': flightId, + 'transactionId': transactionId, + 'date': ticketDate, + 'passenger': passenger, + 'selectedPassenger': selectedPassengers, + 'numberSeat': numberSeat, + 'totalPrice': totalPrice, + 'grandTotal': grandTotal, + 'selectedServiceLabels': selectedServiceLabels, + 'selectedPorterServices': selectedPorterServices, + 'totalAll': totalAll(), + 'expiryTime': formattedExpiryTime, + }; + + Get.toNamed(Routes.PAYMENT, arguments: argument); + } catch (e) { + // Tutup dialog loading jika terjadi error + if (Get.isDialogOpen ?? false) { + Get.back(); + } + + SnackbarHelper.showError('Error', 'Gagal membuat pesanan: $e'); + } }, )); } diff --git a/lib/presentation/screens/routes/app_rountes.dart b/lib/presentation/screens/routes/app_rountes.dart index 808227a..8ec29a3 100644 --- a/lib/presentation/screens/routes/app_rountes.dart +++ b/lib/presentation/screens/routes/app_rountes.dart @@ -4,6 +4,7 @@ import 'package:e_porter/domain/bindings/porter_service_binding.dart'; import 'package:e_porter/domain/bindings/profil_binding.dart'; import 'package:e_porter/domain/bindings/search_flight_binding.dart'; import 'package:e_porter/domain/bindings/ticket_binding.dart'; +import 'package:e_porter/domain/bindings/transaction_binding.dart'; import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.dart'; import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart'; import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart'; @@ -112,6 +113,7 @@ class AppRoutes { GetPage( name: Routes.TICKETBOOKINGSTEP4, page: () => TicketBookingStep4Screen(), + binding: TransactionBinding(), ), GetPage( name: Routes.CHOOSECHAIR, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3a00b9d..316f5a4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import device_info_plus import file_picker import firebase_auth import firebase_core +import firebase_database import firebase_storage import path_provider_foundation import shared_preferences_foundation @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b45003c..cb8c58c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,6 +241,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.22.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "182ce4713d47ffc5f19a5a7b934867d1fae9c33081febcec8c062cb89fc14652" + url: "https://pub.dev" + source: hosted + version: "11.3.5" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: b65f416dd2c8ac2d5322241e5411a24ed3da43d0f38aaf9ab6c211d72e52261b + url: "https://pub.dev" + source: hosted + version: "0.2.6+5" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "5203141fe00a1edfaed5f8e0444b8e4ef807a8ec6eca925621b1cab69b6c06e4" + url: "https://pub.dev" + source: hosted + version: "0.2.6+11" firebase_storage: dependency: "direct main" description: @@ -265,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.10.12" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -637,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -685,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b31c926..6452147 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,8 @@ dependencies: cloud_firestore: ^5.6.4 firebase_auth: ^5.5.1 firebase_storage: ^12.4.4 + uuid: ^4.5.1 + firebase_database: ^11.3.5 shared_preferences: ^2.4.6 logger: ^2.5.0 dropdown_button2: ^2.3.9