class Cart { final int orderId; final String orderUuid; final int userId; final int businessId; final double businessDeliveryMultiplier; final double businessDeliveryFee; // The business's standard delivery fee (for preview) final List businessOrderTypes; // Which order types the business offers (1=dine-in, 2=takeaway, 3=delivery) final int orderTypeId; final double deliveryFee; // The actual delivery fee on this order (set when order type is confirmed) final int statusId; final int? addressId; final int? paymentId; final String? remarks; final DateTime addedOn; final DateTime lastEditedOn; final DateTime? submittedOn; final int servicePointId; final List lineItems; const Cart({ required this.orderId, required this.orderUuid, required this.userId, required this.businessId, required this.businessDeliveryMultiplier, required this.businessDeliveryFee, required this.businessOrderTypes, required this.orderTypeId, required this.deliveryFee, required this.statusId, this.addressId, this.paymentId, this.remarks, required this.addedOn, required this.lastEditedOn, this.submittedOn, required this.servicePointId, required this.lineItems, }); // Helper methods for checking available order types bool get offersDineIn => businessOrderTypes.contains(1); bool get offersTakeaway => businessOrderTypes.contains(2); bool get offersDelivery => businessOrderTypes.contains(3); factory Cart.fromJson(Map json) { final order = (json["ORDER"] ?? json["Order"]) as Map? ?? {}; final lineItemsJson = (json["ORDERLINEITEMS"] ?? json["OrderLineItems"]) as List? ?? []; // Parse business order types - can be array of ints or strings List orderTypes = [1, 2, 3]; // Default to all types final rawOrderTypes = order["BusinessOrderTypes"]; if (rawOrderTypes != null && rawOrderTypes is List) { orderTypes = rawOrderTypes .map((e) => e is int ? e : int.tryParse(e.toString()) ?? 0) .where((e) => e > 0) .toList(); if (orderTypes.isEmpty) orderTypes = [1, 2, 3]; } return Cart( orderId: _parseInt(order["OrderID"]) ?? 0, orderUuid: order["OrderUUID"]?.toString() ?? "", userId: _parseInt(order["OrderUserID"]) ?? 0, businessId: _parseInt(order["OrderBusinessID"]) ?? 0, businessDeliveryMultiplier: _parseDouble(order["OrderBusinessDeliveryMultiplier"]) ?? 0.0, businessDeliveryFee: _parseDouble(order["BusinessDeliveryFee"]) ?? 0.0, businessOrderTypes: orderTypes, orderTypeId: _parseInt(order["OrderTypeID"]) ?? 0, deliveryFee: _parseDouble(order["OrderDeliveryFee"]) ?? 0.0, statusId: _parseInt(order["OrderStatusID"]) ?? 0, addressId: _parseInt(order["OrderAddressID"]), paymentId: _parseInt(order["OrderPaymentID"]), remarks: order["OrderRemarks"]?.toString(), addedOn: _parseDateTime(order["OrderAddedOn"]), lastEditedOn: _parseDateTime(order["OrderLastEditedOn"]), submittedOn: _parseDateTime(order["OrderSubmittedOn"]), servicePointId: _parseInt(order["OrderServicePointID"]) ?? 0, lineItems: lineItemsJson .map((item) => OrderLineItem.fromJson(item as Map)) .toList(), ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } static double? _parseDouble(dynamic value) { if (value == null) return null; if (value is double) return value; if (value is num) return value.toDouble(); if (value is String) { if (value.isEmpty) return null; return double.tryParse(value); } return null; } static DateTime _parseDateTime(dynamic value) { if (value == null) return DateTime.now(); if (value is DateTime) return value; if (value is String) { try { return DateTime.parse(value); } catch (e) { return DateTime.now(); } } return DateTime.now(); } double get subtotal { // Sum all non-deleted line items (root items and modifiers) // Root items have their base price, modifiers have their add-on prices // Modifier prices are multiplied by the root item's quantity double total = 0.0; // First, get all root items and their prices final rootItems = lineItems.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0); for (final rootItem in rootItems) { total += rootItem.price * rootItem.quantity; // Add all modifier prices for this root item (recursively) total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity); } return total; } /// Recursively sum modifier prices for a parent line item double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) { double total = 0.0; final children = lineItems.where( (item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId ); for (final child in children) { // Modifier price is multiplied by root item quantity total += child.price * rootQuantity; // Recursively add grandchildren modifier prices total += _sumModifierPrices(child.orderLineItemId, rootQuantity); } return total; } // Only include delivery fee for delivery orders (orderTypeId == 3) // Sales tax rate (8.25% for California - can be made configurable per business later) static const double taxRate = 0.0825; // Calculate sales tax on subtotal double get tax => subtotal * taxRate; double get total => subtotal + tax + (orderTypeId == 3 ? deliveryFee : 0); int get itemCount { return lineItems .where((item) => !item.isDeleted && item.parentOrderLineItemId == 0) .fold(0, (sum, item) => sum + item.quantity); } } class OrderLineItem { final int orderLineItemId; final int parentOrderLineItemId; final int orderId; final int itemId; final int statusId; final double price; final int quantity; final String? remark; final bool isDeleted; final DateTime addedOn; final String? itemName; final int? itemParentItemId; final String? itemParentName; final bool isCheckedByDefault; const OrderLineItem({ required this.orderLineItemId, required this.parentOrderLineItemId, required this.orderId, required this.itemId, required this.statusId, required this.price, required this.quantity, this.remark, required this.isDeleted, required this.addedOn, this.itemName, this.itemParentItemId, this.itemParentName, this.isCheckedByDefault = false, }); factory OrderLineItem.fromJson(Map json) { return OrderLineItem( orderLineItemId: _parseInt(json["OrderLineItemID"]) ?? 0, parentOrderLineItemId: _parseInt(json["OrderLineItemParentOrderLineItemID"]) ?? 0, orderId: _parseInt(json["OrderLineItemOrderID"]) ?? 0, itemId: _parseInt(json["OrderLineItemItemID"]) ?? 0, statusId: _parseInt(json["OrderLineItemStatusID"]) ?? 0, price: _parseDouble(json["OrderLineItemPrice"]) ?? 0.0, quantity: _parseInt(json["OrderLineItemQuantity"]) ?? 0, remark: json["OrderLineItemRemark"]?.toString(), isDeleted: _parseBool(json["OrderLineItemIsDeleted"]), addedOn: _parseDateTime(json["OrderLineItemAddedOn"]), itemName: json["ItemName"]?.toString(), itemParentItemId: _parseInt(json["ItemParentItemID"]), itemParentName: json["ItemParentName"]?.toString(), isCheckedByDefault: _parseBool(json["ItemIsCheckedByDefault"]), ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } static double? _parseDouble(dynamic value) { if (value == null) return null; if (value is double) return value; if (value is num) return value.toDouble(); if (value is String) { if (value.isEmpty) return null; return double.tryParse(value); } return null; } static bool _parseBool(dynamic value) { if (value == null) return false; if (value is bool) return value; if (value is num) return value != 0; if (value is String) { final lower = value.toLowerCase(); return lower == "true" || lower == "1"; } return false; } static DateTime _parseDateTime(dynamic value) { if (value == null) return DateTime.now(); if (value is DateTime) return value; if (value is String) { try { return DateTime.parse(value); } catch (e) { return DateTime.now(); } } return DateTime.now(); } bool get isRootItem => parentOrderLineItemId == 0; bool get isModifier => parentOrderLineItemId != 0; } /// Lightweight cart info returned by getActiveCart API /// Used at app startup to check if user has an existing order class ActiveCartInfo { final int orderId; final String orderUuid; final int businessId; final String businessName; final int orderTypeId; final String orderTypeName; final int servicePointId; final String servicePointName; final int itemCount; const ActiveCartInfo({ required this.orderId, required this.orderUuid, required this.businessId, required this.businessName, required this.orderTypeId, required this.orderTypeName, required this.servicePointId, required this.servicePointName, required this.itemCount, }); factory ActiveCartInfo.fromJson(Map json) { return ActiveCartInfo( orderId: _parseInt(json["OrderID"]) ?? 0, orderUuid: json["OrderUUID"]?.toString() ?? "", businessId: _parseInt(json["BusinessID"]) ?? 0, businessName: json["BusinessName"]?.toString() ?? "", orderTypeId: _parseInt(json["OrderTypeID"]) ?? 0, orderTypeName: json["OrderTypeName"]?.toString() ?? "", servicePointId: _parseInt(json["ServicePointID"]) ?? 0, servicePointName: json["ServicePointName"]?.toString() ?? "", itemCount: _parseInt(json["ItemCount"]) ?? 0, ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } bool get hasItems => itemCount > 0; bool get isDineIn => orderTypeId == 1; bool get isTakeaway => orderTypeId == 2; bool get isDelivery => orderTypeId == 3; bool get isUndecided => orderTypeId == 0; }