Feat: done post data for transaction

This commit is contained in:
orangdeso 2025-04-03 18:41:49 +07:00
parent afc667838b
commit 18487e7a2e
14 changed files with 1054 additions and 21 deletions

View File

@ -181,6 +181,24 @@
"packageUri": "lib/", "packageUri": "lib/",
"languageVersion": "3.4" "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", "name": "firebase_storage",
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage-12.4.5", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage-12.4.5",
@ -199,6 +217,12 @@
"packageUri": "lib/", "packageUri": "lib/",
"languageVersion": "3.4" "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", "name": "flutter",
"rootUri": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0/packages/flutter", "rootUri": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0/packages/flutter",
@ -487,6 +511,12 @@
"packageUri": "lib/", "packageUri": "lib/",
"languageVersion": "2.18" "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", "name": "stack_trace",
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1",
@ -523,6 +553,12 @@
"packageUri": "lib/", "packageUri": "lib/",
"languageVersion": "3.5" "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", "name": "vector_graphics",
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18", "rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18",
@ -596,7 +632,7 @@
"languageVersion": "3.4" "languageVersion": "3.4"
} }
], ],
"generated": "2025-03-31T15:19:30.948302Z", "generated": "2025-04-01T21:50:03.833642Z",
"generator": "pub", "generator": "pub",
"generatorVersion": "3.5.0", "generatorVersion": "3.5.0",
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0", "flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",

View File

@ -118,6 +118,18 @@ firebase_core_web
3.4 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/
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core_web-2.22.0/lib/ 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 firebase_storage
3.2 3.2
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage-12.4.5/ 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 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/
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_storage_web-3.10.12/lib/ 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 flutter_picker
2.12 2.12
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_picker-2.1.0/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_picker-2.1.0/
@ -306,6 +322,10 @@ source_span
2.18 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/
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.0/lib/ 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 stack_trace
2.18 2.18
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.11.1/
@ -330,6 +350,10 @@ typed_data
3.5 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/
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0/lib/ 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 vector_graphics
3.4 3.4
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18/ file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/vector_graphics-1.1.18/

View File

@ -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<String> createTransaction({
required String ticketId,
required String flightId,
required double amount,
required String method,
required DateTime expiryTime,
required Map<String, dynamic> flightDetails,
required Map<String, dynamic> bandaraDetails,
Map<String, dynamic>? porterServiceDetails,
required Map<String, dynamic> userDetails,
required int passenger,
required List<Map<String, dynamic>> passengerDetails,
required List<String> 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<String, dynamic>? data = seatData.data() as Map<String, dynamic>?;
// 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<String, dynamic> seatData = Map<String, dynamic>.from(data['seat']);
// Pastikan kelas kursi (a-f) tersedia
if (seatData.containsKey(seatClass)) {
Map<String, dynamic> classSeatData = Map<String, dynamic>.from(seatData[seatClass]);
// Pastikan array isTaken tersedia
if (classSeatData.containsKey('isTaken')) {
List<dynamic> isTaken = List<dynamic>.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<dynamic> 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<dynamic> 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<dynamic> isTaken = List.filled(seatIndex + 1, false);
isTaken[seatIndex] = true;
Map<String, dynamic> 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<void> 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<void> 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<List<TransactionModel>> getTransactionsByUserId(String userId) async {
try {
// Mencari tiket berdasarkan userId
final ticketQuerySnapshot = await _firestore.collection('tickets').where('userId', isEqualTo: userId).get();
final List<TransactionModel> 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<TransactionModel?> 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<void> 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<TransactionModel?> 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<void> 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');
}
}
}

View File

@ -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<TransactionRepository>(
() => TransactionRepositoryImpl(),
);
// UseCase
Get.lazyPut<TransactionUseCase>(
() => TransactionUseCase(Get.find<TransactionRepository>()),
);
// Controller
Get.lazyPut<TransactionController>(
() => TransactionController(Get.find<TransactionUseCase>()),
);
}
}

View File

@ -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<String, dynamic> flightDetails;
final Map<String, dynamic> bandaraDetails;
final Map<String, dynamic>? porterServiceDetails;
final Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
};
}
}

View File

@ -0,0 +1,40 @@
import 'dart:io';
import 'package:e_porter/domain/models/transaction_model.dart';
abstract class TransactionRepository {
Future<String> createTransaction({
required String ticketId,
required String flightId,
required double amount,
required String method,
required DateTime expiryTime,
required Map<String, dynamic> flightDetails,
required Map<String, dynamic> bandaraDetails,
Map<String, dynamic>? porterServiceDetails,
required Map<String, dynamic> userDetails,
required int passenger,
required List<Map<String, dynamic>> passengerDetails,
required List<String> numberSeat,
});
Future<void> updateTransactionStatus({
required String ticketId,
required String transactionId,
required String status,
required String userId,
});
Future<void> uploadPaymentProof({
required String ticketId,
required String transactionId,
required File proofImage,
});
Future<List<TransactionModel>> getTransactionsByUserId(String userId);
Future<TransactionModel?> getTransactionById({required String ticketId, required String transactionId});
Future<void> syncTransactionToRealtimeDB({required String ticketId, required String transactionId});
Stream<TransactionModel?> watchTransaction({required String ticketId, required String transactionId});
}

View File

@ -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<String> createTransaction({
required String ticketId,
required String flightId,
required double amount,
required String method,
required DateTime expiryTime,
required Map<String, dynamic> flightDetails,
required Map<String, dynamic> bandaraDetails,
Map<String, dynamic>? porterServiceDetails,
required Map<String, dynamic> userDetails,
required int passenger,
required List<Map<String, dynamic>> passengerDetails, // Tambah parameter ini
required List<String> 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<void> 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<void> uploadPaymentProof({
required String ticketId,
required String transactionId,
required File proofImage,
}) {
return _repository.uploadPaymentProof(
ticketId: ticketId,
transactionId: transactionId,
proofImage: proofImage,
);
}
Future<List<TransactionModel>> getTransactionsByUserId(String userId) {
return _repository.getTransactionsByUserId(userId);
}
Future<TransactionModel?> getTransactionById({required String ticketId, required String transactionId}) {
return _repository.getTransactionById(ticketId: ticketId, transactionId: transactionId);
}
Future<void> syncTransactionToRealtimeDB({required String ticketId, required String transactionId}) {
return _repository.syncTransactionToRealtimeDB(ticketId: ticketId, transactionId: transactionId);
}
Stream<TransactionModel?> watchTransaction({required String ticketId, required String transactionId}) {
return _repository.watchTransaction(ticketId: ticketId, transactionId: transactionId);
}
}

View File

@ -1,13 +1,19 @@
import 'dart:developer'; 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:e_porter/presentation/screens/routes/app_rountes.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '_core/service/transaction_expiry_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); await Firebase.initializeApp();
final transactionRepo = Get.put(TransactionRepositoryImpl());
TransactionExpiryService().initialize(transactionRepo);
log("Firebase Initialized Successfully!"); log("Firebase Initialized Successfully!");
runApp(MyApp(initialRoute: Routes.SPLASH)); runApp(MyApp(initialRoute: Routes.SPLASH));

View File

@ -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<TransactionModel?> currentTransaction = Rx<TransactionModel?>(null);
final RxBool isLoading = false.obs;
final RxString error = ''.obs;
Future<String> createTransaction({
required String ticketId,
required String flightId,
required double amount,
required String method,
required DateTime expiryTime,
required Map<String, dynamic> flightDetails,
required Map<String, dynamic> bandaraDetails,
Map<String, dynamic>? porterServiceDetails,
required Map<String, dynamic> userDetails,
required int passenger,
required List<Map<String, dynamic>> passengerDetails, // Tambah parameter ini
required List<String> 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<void> 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<List<TransactionModel>> 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<void> 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<void> 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;
}
}
}

View File

@ -13,10 +13,13 @@ import 'package:intl/intl.dart';
import '../../../../_core/component/appbar/appbar_component.dart'; import '../../../../_core/component/appbar/appbar_component.dart';
import '../../../../_core/component/icons/icons_library.dart'; import '../../../../_core/component/icons/icons_library.dart';
import '../../../../_core/service/logger_service.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/porter_service_model.dart';
import '../../../../domain/models/ticket_model.dart'; import '../../../../domain/models/ticket_model.dart';
import '../../../../domain/models/user_entity.dart'; import '../../../../domain/models/user_entity.dart';
import '../../../controllers/ticket_controller.dart'; import '../../../controllers/ticket_controller.dart';
import '../../../controllers/transaction_controller.dart';
import '../component/card_flight_information.dart'; import '../component/card_flight_information.dart';
class TicketBookingStep4Screen extends StatefulWidget { class TicketBookingStep4Screen extends StatefulWidget {
@ -41,6 +44,7 @@ class _TicketBookingStep4ScreenState extends State<TicketBookingStep4Screen> {
final double serviceCharge = 10000.0; final double serviceCharge = 10000.0;
final TicketController ticketController = Get.find<TicketController>(); final TicketController ticketController = Get.find<TicketController>();
final TransactionController transactionController = Get.find<TransactionController>();
FlightModel? flightData; FlightModel? flightData;
String? departureTime; String? departureTime;
String? arrivalTime; String? arrivalTime;
@ -218,26 +222,137 @@ class _TicketBookingStep4ScreenState extends State<TicketBookingStep4Screen> {
price: "Rp ${NumberFormat.decimalPattern('id_ID').format(totalAll())}", price: "Rp ${NumberFormat.decimalPattern('id_ID').format(totalAll())}",
labelButton: "Buat Pesanan", labelButton: "Buat Pesanan",
iconButton: CustomeIcons.ProtectOutline(color: Colors.white), iconButton: CustomeIcons.ProtectOutline(color: Colors.white),
onTap: () { onTap: () async {
final DateTime currentTime = DateTime.now(); try {
final DateTime expiryTime = currentTime.add(Duration(hours: 1)); Get.dialog(
final String formattedExpiryTime = Center(child: CircularProgressIndicator()),
"${DateFormat('dd MMMM yyyy', 'en_US').format(expiryTime)}, ${DateFormat.Hm().format(expiryTime)}"; barrierDismissible: false,
final argument = { );
'ticketId': ticketId,
'flightId': flightId, final userData = await PreferencesService.getUserData();
'date': ticketDate, if (userData == null) {
'passenger': passenger, Get.back(); // close dialog
'selectedPassenger': selectedPassengers, SnackbarHelper.showError('Error', 'Data pengguna tidak ditemukan');
'numberSeat': numberSeat, return;
'totalPrice': totalPrice, }
'grandTotal': grandTotal,
'selectedServiceLabels': selectedServiceLabels, // Persiapkan data expiry time
'selectedPorterServices': selectedPorterServices, final DateTime currentTime = DateTime.now();
'totalAll': totalAll(), final DateTime expiryTime = currentTime.add(Duration(seconds: 5));
'expiryTime': formattedExpiryTime,
}; // Persiapkan data bandara
Get.toNamed(Routes.PAYMENT, arguments: argument); 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<String, dynamic>? 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<Map<String, dynamic>> 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');
}
}, },
)); ));
} }

View File

@ -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/profil_binding.dart';
import 'package:e_porter/domain/bindings/search_flight_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/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/forget_password_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart'; import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart'; import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
@ -112,6 +113,7 @@ class AppRoutes {
GetPage( GetPage(
name: Routes.TICKETBOOKINGSTEP4, name: Routes.TICKETBOOKINGSTEP4,
page: () => TicketBookingStep4Screen(), page: () => TicketBookingStep4Screen(),
binding: TransactionBinding(),
), ),
GetPage( GetPage(
name: Routes.CHOOSECHAIR, name: Routes.CHOOSECHAIR,

View File

@ -10,6 +10,7 @@ import device_info_plus
import file_picker import file_picker
import firebase_auth import firebase_auth
import firebase_core import firebase_core
import firebase_database
import firebase_storage import firebase_storage
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@ -241,6 +241,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.22.0" 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: firebase_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -265,6 +289,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.12" version: "3.10.12"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -637,6 +669,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -685,6 +725,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: vector_graphics:
dependency: transitive dependency: transitive
description: description:

View File

@ -47,6 +47,8 @@ dependencies:
cloud_firestore: ^5.6.4 cloud_firestore: ^5.6.4
firebase_auth: ^5.5.1 firebase_auth: ^5.5.1
firebase_storage: ^12.4.4 firebase_storage: ^12.4.4
uuid: ^4.5.1
firebase_database: ^11.3.5
shared_preferences: ^2.4.6 shared_preferences: ^2.4.6
logger: ^2.5.0 logger: ^2.5.0
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9