-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/orders details screen #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import 'package:eco_bites/core/error/exceptions.dart'; | ||
| import 'package:eco_bites/features/orders/data/models/order_model.dart'; | ||
| import 'package:eco_bites/features/orders/domain/entities/order.dart'; | ||
| import 'package:logger/logger.dart'; | ||
| import 'package:path/path.dart'; | ||
| import 'package:sqflite/sqflite.dart'; | ||
|
|
||
| abstract class OrderLocalDataSource { | ||
| Future<void> cacheOrders(List<OrderModel> orders); | ||
| Future<List<OrderModel>> getCachedOrders(); | ||
| Future<void> clearCache(); | ||
| Future<void> updateOrderStatus(String orderId, OrderStatus newStatus); | ||
| } | ||
|
|
||
| class OrderLocalDataSourceImpl implements OrderLocalDataSource { | ||
| Database? _database; | ||
|
|
||
| Future<Database> get database async { | ||
| _database ??= await _initDatabase(); | ||
| return _database!; | ||
| } | ||
|
|
||
| Future<Database> _initDatabase() async { | ||
| final String path = join(await getDatabasesPath(), 'orders.db'); | ||
| return openDatabase( | ||
| path, | ||
| version: 1, | ||
| onCreate: (Database db, int version) async { | ||
| // Create orders table | ||
| await db.execute(''' | ||
| CREATE TABLE orders( | ||
| id TEXT PRIMARY KEY, | ||
| businessId TEXT NOT NULL, | ||
| businessName TEXT NOT NULL, | ||
| totalAmount REAL NOT NULL, | ||
| status TEXT NOT NULL, | ||
| createdAt TEXT NOT NULL, | ||
| completedAt TEXT | ||
| ) | ||
| '''); | ||
|
|
||
| // Create order items table with foreign key to orders | ||
| await db.execute(''' | ||
| CREATE TABLE order_items( | ||
| id TEXT PRIMARY KEY, | ||
| orderId TEXT NOT NULL, | ||
| name TEXT NOT NULL, | ||
| quantity INTEGER NOT NULL, | ||
| price REAL NOT NULL, | ||
| FOREIGN KEY (orderId) REFERENCES orders (id) | ||
| ) | ||
| '''); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> cacheOrders(List<OrderModel> orders) async { | ||
| try { | ||
| final Database db = await database; | ||
| await db.transaction((Transaction txn) async { | ||
| // Clear existing data | ||
| await txn.delete('order_items'); | ||
| await txn.delete('orders'); | ||
|
|
||
| // Insert new data | ||
| for (final OrderModel order in orders) { | ||
| await txn.insert( | ||
| 'orders', | ||
| <String, dynamic>{ | ||
| 'id': order.id, | ||
| 'businessId': order.businessId, | ||
| 'businessName': order.businessName, | ||
| 'totalAmount': order.totalAmount, | ||
| 'status': order.status.name, | ||
| 'createdAt': order.createdAt.toIso8601String(), | ||
| if (order.completedAt != null) | ||
| 'completedAt': order.completedAt!.toIso8601String(), | ||
| }, | ||
| conflictAlgorithm: ConflictAlgorithm.replace, | ||
| ); | ||
|
|
||
| // Insert order items | ||
| for (final OrderItem item in order.items) { | ||
| await txn.insert( | ||
| 'order_items', | ||
| <String, dynamic>{ | ||
| 'id': item.id, | ||
| 'orderId': order.id, | ||
| 'name': item.name, | ||
| 'quantity': item.quantity, | ||
| 'price': item.price, | ||
| }, | ||
| conflictAlgorithm: ConflictAlgorithm.replace, | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| Logger().e('Error caching orders: $e'); | ||
| throw const CacheException('Failed to cache orders'); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Future<List<OrderModel>> getCachedOrders() async { | ||
| try { | ||
| final Database db = await database; | ||
|
|
||
| // Log the database path | ||
| Logger().d('Database path: ${await getDatabasesPath()}'); | ||
|
|
||
| // Get all orders | ||
| final List<Map<String, dynamic>> orderMaps = await db.query('orders'); | ||
| Logger().d('Number of cached orders: ${orderMaps.length}'); | ||
|
|
||
| return Future.wait( | ||
| orderMaps.map((Map<String, dynamic> orderMap) async { | ||
| // Get items for this order | ||
| final List<Map<String, dynamic>> itemMaps = await db.query( | ||
| 'order_items', | ||
| where: 'orderId = ?', | ||
| whereArgs: <String>[orderMap['id'] as String], | ||
| ); | ||
|
|
||
| final List<OrderItemModel> items = itemMaps | ||
| .map( | ||
| (Map<String, dynamic> itemMap) => OrderItemModel( | ||
| id: itemMap['id'] as String, | ||
| name: itemMap['name'] as String, | ||
| quantity: itemMap['quantity'] as int, | ||
| price: itemMap['price'] as double, | ||
| ), | ||
| ) | ||
| .toList(); | ||
|
|
||
| return OrderModel( | ||
| id: orderMap['id'] as String, | ||
| businessId: orderMap['businessId'] as String, | ||
| businessName: orderMap['businessName'] as String, | ||
| items: items, | ||
| totalAmount: orderMap['totalAmount'] as double, | ||
| status: OrderStatus.values.firstWhere( | ||
| (OrderStatus s) => s.name == (orderMap['status'] as String), | ||
| ), | ||
| createdAt: DateTime.parse(orderMap['createdAt'] as String), | ||
| completedAt: orderMap['completedAt'] != null | ||
| ? DateTime.parse(orderMap['completedAt'] as String) | ||
| : null, | ||
| ); | ||
| }), | ||
| ); | ||
| } catch (e) { | ||
| Logger().e('Error getting cached orders: $e'); | ||
| throw const CacheException('Failed to get cached orders'); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Future<void> updateOrderStatus(String orderId, OrderStatus newStatus) async { | ||
| try { | ||
| final Database db = await database; | ||
| await db.update( | ||
| 'orders', | ||
| <String, dynamic>{ | ||
| 'status': newStatus.name, | ||
| if (newStatus == OrderStatus.completed) | ||
| 'completedAt': DateTime.now().toIso8601String(), | ||
| }, | ||
| where: 'id = ?', | ||
| whereArgs: <String>[orderId], | ||
| ); | ||
| } catch (e) { | ||
| Logger().e('Error updating order status: $e'); | ||
| throw const CacheException('Failed to update order status'); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Future<void> clearCache() async { | ||
| try { | ||
| final Database db = await database; | ||
| await db.transaction((Transaction txn) async { | ||
| await txn.delete('order_items'); | ||
| await txn.delete('orders'); | ||
| }); | ||
| } catch (e) { | ||
| Logger().e('Error clearing cache: $e'); | ||
| throw const CacheException('Failed to clear cache'); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||||||||||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||||||||||||
| import 'package:eco_bites/core/error/exceptions.dart'; | ||||||||||||||
| import 'package:eco_bites/features/orders/data/models/order_model.dart'; | ||||||||||||||
| import 'package:eco_bites/features/orders/domain/entities/order.dart'; | ||||||||||||||
| import 'package:firebase_auth/firebase_auth.dart'; | ||||||||||||||
| import 'package:logger/logger.dart'; | ||||||||||||||
|
|
||||||||||||||
| abstract class OrderRemoteDataSource { | ||||||||||||||
| Future<List<OrderModel>> getOrders(); | ||||||||||||||
| Future<void> updateOrderStatus(String orderId, OrderStatus newStatus); | ||||||||||||||
| Future<void> createOrder(OrderModel order); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { | ||||||||||||||
| const OrderRemoteDataSourceImpl({ | ||||||||||||||
| required FirebaseFirestore firestore, | ||||||||||||||
| required FirebaseAuth auth, | ||||||||||||||
| }) : _firestore = firestore, | ||||||||||||||
| _auth = auth; | ||||||||||||||
|
|
||||||||||||||
| final FirebaseFirestore _firestore; | ||||||||||||||
| final FirebaseAuth _auth; | ||||||||||||||
|
|
||||||||||||||
| @override | ||||||||||||||
| Future<List<OrderModel>> getOrders() async { | ||||||||||||||
| try { | ||||||||||||||
| final String? userId = _auth.currentUser?.uid; | ||||||||||||||
| if (userId == null) { | ||||||||||||||
| throw const AuthException('User not authenticated'); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| final QuerySnapshot<Map<String, dynamic>> snapshot = await _firestore | ||||||||||||||
| .collection('orders') | ||||||||||||||
| .where('userId', isEqualTo: userId) | ||||||||||||||
| .orderBy('createdAt', descending: true) | ||||||||||||||
| .get(); | ||||||||||||||
|
|
||||||||||||||
| return snapshot.docs | ||||||||||||||
| .map( | ||||||||||||||
| (QueryDocumentSnapshot<Map<String, dynamic>> doc) => | ||||||||||||||
| OrderModel.fromMap(doc.data(), doc.id), | ||||||||||||||
| ) | ||||||||||||||
| .toList(); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| Logger().e('Error fetching orders: $e'); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging sensitive error information Logging the exception Apply this diff to adjust the logging: - Logger().e('Error fetching orders: $e');
+ Logger().e('Error fetching orders');📝 Committable suggestion
Suggested change
|
||||||||||||||
| throw AuthException('Failed to fetch orders: $e'); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid exposing sensitive error details in exceptions Including the original exception message Apply this diff to modify the exception message: - throw AuthException('Failed to fetch orders: $e');
+ throw AuthException('Failed to fetch orders');📝 Committable suggestion
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @override | ||||||||||||||
| Future<void> updateOrderStatus(String orderId, OrderStatus newStatus) async { | ||||||||||||||
| try { | ||||||||||||||
| await _firestore | ||||||||||||||
| .collection('orders') | ||||||||||||||
| .doc(orderId) | ||||||||||||||
| .update(<String, dynamic>{ | ||||||||||||||
| 'status': newStatus.name, | ||||||||||||||
| if (newStatus == OrderStatus.completed) | ||||||||||||||
| 'completedAt': FieldValue.serverTimestamp(), | ||||||||||||||
| }); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| Logger().e('Error updating order status: $e'); | ||||||||||||||
| throw AuthException('Failed to update order status: $e'); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+62
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consistent error handling and prevent sensitive data exposure Similar to the Apply this diff to add error handling and prevent information leakage: } catch (e) {
- Logger().e('Error updating order status: $e');
- throw AuthException('Failed to update order status: $e');
+ Logger().e('Error updating order status');
+ throw AuthException('Failed to update order status');
}📝 Committable suggestion
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @override | ||||||||||||||
| Future<void> createOrder(OrderModel order) async { | ||||||||||||||
| await _firestore.collection('orders').doc().set(order.toMap()); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+68
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling to The Apply this diff to include error handling: @override
Future<void> createOrder(OrderModel order) async {
+ try {
await _firestore.collection('orders').doc().set(order.toMap());
+ } catch (e) {
+ Logger().e('Error creating order');
+ throw AuthException('Failed to create order');
+ }
}
|
||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||
| import 'package:eco_bites/features/orders/domain/entities/order.dart' | ||
| as order_entity; | ||
|
|
||
| class OrderModel extends order_entity.Order { | ||
| const OrderModel({ | ||
| required super.id, | ||
| required super.businessId, | ||
| required super.businessName, | ||
| required super.items, | ||
| required super.totalAmount, | ||
| required super.status, | ||
| required super.createdAt, | ||
| super.completedAt, | ||
| }); | ||
|
|
||
| factory OrderModel.fromMap(Map<String, dynamic> map, String id) { | ||
| return OrderModel( | ||
| id: id, | ||
| businessId: map['businessId'] as String, | ||
| businessName: map['businessName'] as String, | ||
| items: (map['items'] as List<dynamic>) | ||
| .map( | ||
| (dynamic item) => | ||
| OrderItemModel.fromMap(item as Map<String, dynamic>), | ||
| ) | ||
| .toList(), | ||
| totalAmount: (map['totalAmount'] as num).toDouble(), | ||
| status: order_entity.OrderStatus.values.firstWhere( | ||
| (order_entity.OrderStatus s) => s.name == (map['status'] as String), | ||
| ), | ||
| createdAt: (map['createdAt'] as Timestamp).toDate(), | ||
| completedAt: map['completedAt'] != null | ||
| ? (map['completedAt'] as Timestamp).toDate() | ||
| : null, | ||
| ); | ||
| } | ||
|
Comment on lines
+17
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling to fromMap factory The fromMap factory should handle potential null or invalid values from Firestore. factory OrderModel.fromMap(Map<String, dynamic> map, String id) {
try {
return OrderModel(
id: id,
businessId: map['businessId'] as String? ??
throw FormatException('businessId is required'),
// ... similar null checks for other fields
);
} catch (e) {
throw FormatException('Failed to parse OrderModel: $e');
}
} |
||
|
|
||
| Map<String, dynamic> toMap() { | ||
| return <String, dynamic>{ | ||
| 'businessId': businessId, | ||
| 'businessName': businessName, | ||
| 'items': items | ||
| .map( | ||
| (order_entity.OrderItem item) => (item as OrderItemModel).toMap(), | ||
| ) | ||
| .toList(), | ||
| 'totalAmount': totalAmount, | ||
| 'status': status.name, | ||
| 'createdAt': Timestamp.fromDate(createdAt), | ||
| if (completedAt != null) 'completedAt': Timestamp.fromDate(completedAt!), | ||
| }; | ||
| } | ||
|
Comment on lines
+39
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add type safety to toMap serialization The type casting in the items map operation could fail at runtime. Map<String, dynamic> toMap() {
if (items.any((item) => item is! OrderItemModel)) {
throw StateError(
'All items must be OrderItemModel instances for serialization',
);
}
return <String, dynamic>{
'businessId': businessId,
// ... rest of the implementation
};
} |
||
| } | ||
|
|
||
| class OrderItemModel extends order_entity.OrderItem { | ||
| const OrderItemModel({ | ||
| required super.id, | ||
| required super.name, | ||
| required super.quantity, | ||
| required super.price, | ||
| }); | ||
|
|
||
| factory OrderItemModel.fromMap(Map<String, dynamic> map) { | ||
| return OrderItemModel( | ||
| id: map['id'] as String, | ||
| name: map['name'] as String, | ||
| quantity: map['quantity'] as int, | ||
| price: (map['price'] as num).toDouble(), | ||
| ); | ||
| } | ||
|
|
||
| Map<String, dynamic> toMap() { | ||
| return <String, dynamic>{ | ||
| 'id': id, | ||
| 'name': name, | ||
| 'quantity': quantity, | ||
| 'price': price, | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid catching broad exceptions without handling them properly
Catching general exceptions and rethrowing a
CacheExceptionmight obscure the original error. Consider logging the exception type and message.Apply this diff to log the exception details safely:
} catch (e) { + Logger().e('Error getting cached orders', e); - Logger().e('Error getting cached orders: $e'); throw const CacheException('Failed to get cached orders'); }📝 Committable suggestion