Files
apskel-pos-flutter-v2/lib/domain/order/entities/order_entity.dart
T
efrilmandClaude Opus 5 1505a83dd3 feat: dukung penjualan produk per timbangan
Produk kini punya cara jual `unit` (cacah) atau `weight` (timbangan),
mengikuti kontrak backend di docs/integration-weight-based-products.md.

- tipe SellBy, domain/infrastructure unit baru, dan endpoint /api/v1/units
- dialog input berat saat produk timbangan ditambahkan ke keranjang
- tiap penimbangan jadi baris keranjang sendiri; total baris dihitung
  berat x harga satuan, sedangkan produk cacah tetap digabung
- request ke backend mengirim quantity 1 dan membawa berat
- migrasi DB v3: kolom sell_by, unit_id, unit_name, unit_abbreviation
  di tabel products supaya satuan tetap tampil saat offline
- tampilan keranjang, struk, split bill, void, dan refund memakai berat
- test/weight_based_products_test.dart menutup perilaku di atas (9 test)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 23:03:31 +07:00

320 lines
8.8 KiB
Dart

part of '../order.dart';
@freezed
class ListOrder with _$ListOrder {
const factory ListOrder({
required List<Order> orders,
required int totalCount,
required int page,
required int limit,
required int totalPages,
}) = _ListOrder;
factory ListOrder.empty() => const ListOrder(
orders: [],
totalCount: 0,
page: 0,
limit: 0,
totalPages: 0,
);
}
@freezed
class Order with _$Order {
const Order._();
const factory Order({
required String id,
required String orderNumber,
required String outletId,
required String userId,
required String tableNumber,
required String orderType,
required String status,
required int subtotal,
required int taxAmount,
required int discountAmount,
required int totalAmount,
required num totalCost,
required int remainingAmount,
required String paymentStatus,
required int refundAmount,
required String refundReason,
required bool isVoid,
required bool isRefund,
required String notes,
required Map<String, dynamic> metadata,
required DateTime createdAt,
required DateTime updatedAt,
required List<OrderItem> orderItems,
required List<Payment> payments,
required int totalPaid,
required int paymentCount,
required String splitType,
}) = _Order;
factory Order.empty() => Order(
id: '',
orderNumber: '',
outletId: '',
userId: '',
tableNumber: '',
orderType: '',
status: '',
subtotal: 0,
taxAmount: 0,
discountAmount: 0,
totalAmount: 0,
totalCost: 0,
remainingAmount: 0,
paymentStatus: '',
refundAmount: 0,
isVoid: false,
isRefund: false,
notes: '',
metadata: const {},
createdAt: DateTime(1970),
updatedAt: DateTime(1970),
orderItems: const [],
payments: const [],
totalPaid: 0,
paymentCount: 0,
splitType: '',
refundReason: '',
);
factory Order.mockOrder() => Order(
id: 'ORD-001',
orderNumber: 'A-1001',
outletId: 'OUTLET-001',
userId: 'USER-001',
tableNumber: 'T01',
orderType: 'Dine In',
status: 'Completed',
subtotal: 150000,
taxAmount: 15000,
discountAmount: 5000,
totalAmount: 160000,
totalCost: 100000,
remainingAmount: 0,
paymentStatus: 'Paid',
refundAmount: 0,
refundReason: '',
isVoid: false,
isRefund: false,
notes: 'Customer requested less sugar',
metadata: {'source': 'POS', 'device': 'iPad', 'customer_name': "John Doe"},
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
updatedAt: DateTime.now(),
orderItems: [
OrderItem(
id: 'ITEM-001',
orderId: 'ORD-001',
productId: 'PROD-001',
productName: 'Cappuccino',
productVariantId: 'VAR-001',
productVariantName: 'Hot',
quantity: 2,
unitPrice: 25000,
totalPrice: 50000,
modifiers: [
{'name': 'Extra Shot', 'price': 5000},
],
notes: 'No sugar',
status: 'Served',
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
updatedAt: DateTime.now(),
printerType: 'Barista',
printToChecker: false,
paidQuantity: 2,
categoryId: 'CAT-001',
categoryName: 'Minuman',
),
OrderItem(
id: 'ITEM-002',
orderId: 'ORD-001',
productId: 'PROD-002',
productName: 'Spaghetti Carbonara',
productVariantId: 'VAR-002',
productVariantName: '',
quantity: 1,
unitPrice: 55000,
totalPrice: 55000,
modifiers: [],
notes: '',
status: 'Served',
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
updatedAt: DateTime.now(),
printerType: 'Kitchen',
printToChecker: false,
paidQuantity: 1,
categoryId: 'CAT-002',
categoryName: 'Makanan',
),
OrderItem(
id: 'ITEM-003',
orderId: 'ORD-001',
productId: 'PROD-003',
productName: 'Pasta',
productVariantId: 'VAR-002',
productVariantName: '',
quantity: 1,
unitPrice: 50000,
totalPrice: 50000,
modifiers: [],
notes: '',
status: 'Served',
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
updatedAt: DateTime.now(),
printerType: 'Kitchen',
printToChecker: false,
paidQuantity: 1,
categoryId: 'CAT-002',
categoryName: 'Makanan',
),
],
payments: [
Payment(
id: 'PAY-001',
orderId: 'ORD-001',
paymentMethodId: 'PM-CASH',
paymentMethodName: 'Cash',
paymentMethodType: 'Cash',
amount: 160000,
status: 'Success',
splitNumber: 1,
splitTotal: 1,
splitDescription: 'Full payment by cash',
refundAmount: 0,
metadata: {'cashier': 'Efril', 'device': 'POS iPad'},
createdAt: DateTime.now().subtract(const Duration(minutes: 45)),
updatedAt: DateTime.now(),
),
],
totalPaid: 160000,
paymentCount: 1,
splitType: 'Single',
);
// For Add Item Order
factory Order.fromLastOrder(Order order, List<ProductQuantity> products) {
final subtotal = products.fold<int>(
0,
(sum, item) => sum + item.lineTotal.toInt(),
);
return order.copyWith(
orderItems: products.map(OrderItem.fromProductQuantity).toList(),
totalAmount: subtotal,
subtotal: subtotal,
metadata: order.metadata,
);
}
List<OrderItem> get orderItemNotCancelled =>
orderItems.where((item) => item.status != 'cancelled').toList();
}
@freezed
class OrderItem with _$OrderItem {
const OrderItem._();
const factory OrderItem({
required String id,
required String orderId,
required String productId,
required String productName,
required String productVariantId,
required String productVariantName,
required int quantity,
required int unitPrice,
required int totalPrice,
required List<dynamic> modifiers,
required String notes,
required String status,
required DateTime createdAt,
required DateTime updatedAt,
required String printerType,
required bool printToChecker,
required int paidQuantity,
required String categoryId,
required String categoryName,
/// Angka timbangan baris ini. `null` untuk produk cacah.
double? weight,
@Default('') String unitId,
@Default('') String unitName,
@Default('') String unitAbbreviation,
}) = _OrderItem;
factory OrderItem.empty() => OrderItem(
id: '',
orderId: '',
productId: '',
productName: '',
productVariantId: '',
productVariantName: '',
quantity: 0,
unitPrice: 0,
totalPrice: 0,
modifiers: const [],
notes: '',
status: '',
createdAt: DateTime(1970),
updatedAt: DateTime(1970),
printerType: '',
printToChecker: false,
paidQuantity: 0,
categoryId: '',
categoryName: '',
);
factory OrderItem.fromProductQuantity(ProductQuantity productQuantity) =>
OrderItem(
id: '',
orderId: '',
productId: productQuantity.product.id,
productName: productQuantity.product.name,
productVariantId: productQuantity.variant?.id ?? '',
productVariantName: productQuantity.variant?.name ?? '',
quantity: productQuantity.isWeighted ? 1 : productQuantity.quantity,
unitPrice: productQuantity.product.price.toInt(),
totalPrice: productQuantity.lineTotal.toInt(),
modifiers: [],
notes: productQuantity.notes,
status: 'pending',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
printerType: productQuantity.product.printerType,
printToChecker: productQuantity.product.printToChecker,
paidQuantity: 0,
categoryId: '',
categoryName: '',
weight: productQuantity.weight,
unitId: productQuantity.product.unitId,
unitName: productQuantity.product.unitName,
unitAbbreviation: productQuantity.product.unitAbbreviation,
);
/// Baris hasil penimbangan — utuh, tidak bisa dipecah sebagian.
bool get isWeighted => weight != null && weight! > 0;
/// Singkatan satuan untuk ditampilkan; jatuh ke nama panjang bila kosong.
String get unitLabel =>
unitAbbreviation.isNotEmpty ? unitAbbreviation : unitName;
/// `4,2 ons` untuk baris berbobot, `2` untuk baris biasa.
///
/// Jangan menampilkan [quantity] mentah pada baris berbobot — nilainya selalu
/// 1 dan terbaca seperti "satu ons".
String get quantityLabel {
if (!isWeighted) return quantity.toString();
final value = weight!.weightFormat;
return unitLabel.isEmpty ? value : '$value $unitLabel';
}
/// `4,2 ons x Rp 4.500` atau `2 x Rp 25.000`.
String get quantityPriceLabel =>
'$quantityLabel x ${unitPrice.currencyFormatRpV2}';
}