Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/core/utils/date_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:intl/intl.dart';

/// Utility class for date related operations.
class DateUtils {

DateUtils._();

/// Formats the given date into a string in the format "MMM d, y".
Expand All @@ -15,7 +14,10 @@ class DateUtils {
/// Formats the given date to show only the day and month.
/// Example output: "04/27"
static String formatDayMonth(DateTime date) {
final DateFormat formatter = DateFormat('MM/dd');
return formatter.format(date);
return DateFormat('d MMM').format(date);
}

static String formatDateTime(DateTime dateTime) {
return DateFormat('d MMM y, HH:mm').format(dateTime);
}
}
192 changes: 192 additions & 0 deletions lib/features/orders/data/datasources/order_local_data_source.dart
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');
}
Comment on lines +154 to +156
Copy link

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 CacheException might 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Logger().e('Error getting cached orders: $e');
throw const CacheException('Failed to get cached orders');
}
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');
}
}
}
71 changes: 71 additions & 0 deletions lib/features/orders/data/datasources/order_remote_data_source.dart
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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid logging sensitive error information

Logging the exception $e can reveal sensitive details in the logs. Consider logging only the error type or a generic message.

Apply this diff to adjust the logging:

- Logger().e('Error fetching orders: $e');
+ Logger().e('Error fetching orders');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Logger().e('Error fetching orders: $e');
Logger().e('Error fetching orders');

throw AuthException('Failed to fetch orders: $e');
}
Comment on lines +45 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid exposing sensitive error details in exceptions

Including the original exception message $e in the thrown AuthException might expose sensitive information. It's better to provide a generic error message to prevent potential information leakage.

Apply this diff to modify the exception message:

- throw AuthException('Failed to fetch orders: $e');
+ throw AuthException('Failed to fetch orders');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Logger().e('Error fetching orders: $e');
throw AuthException('Failed to fetch orders: $e');
}
Logger().e('Error fetching orders: $e');
throw AuthException('Failed to fetch orders');
}

}

@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
Copy link

Choose a reason for hiding this comment

The 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 getOrders method, include error handling in updateOrderStatus and avoid exposing sensitive error details in both logging and exceptions.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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');
}

}

@override
Future<void> createOrder(OrderModel order) async {
await _firestore.collection('orders').doc().set(order.toMap());
}
Comment on lines +68 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to createOrder method

The createOrder method lacks error handling, which can lead to unhandled exceptions. Wrap the method's body in a try-catch block to handle potential errors and maintain consistency with other methods.

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');
+ }
}

Committable suggestion skipped: line range outside the PR's diff.

}
81 changes: 81 additions & 0 deletions lib/features/orders/data/models/order_model.dart
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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,
};
}
}
Loading
Loading