Files
nearle_pos/lib/domain/entities/product.dart
2026-07-31 17:06:52 +05:30

142 lines
3.6 KiB
Dart

import 'package:equatable/equatable.dart';
import '../../core/constants/app_constants.dart';
/// Merchandise categories shown as filter chips on the POS dashboard.
enum ProductCategory {
dairy('Dairy', '🥛'),
grocery('Grocery', '🛒'),
fruits('Fruits', '🍎'),
vegetables('Vegetables', '🥦'),
beverages('Beverages', '🥤'),
snacks('Snacks', '🍪'),
personalCare('Personal Care', '🧴'),
household('Household', '🏠');
const ProductCategory(this.label, this.emoji);
final String label;
final String emoji;
}
/// Unit of measure — drives whether fractional quantities are permitted.
enum UnitOfMeasure {
piece('pc'),
kilogram('kg'),
gram('g'),
litre('L'),
millilitre('ml'),
pack('pack');
const UnitOfMeasure(this.symbol);
final String symbol;
bool get allowsFractional =>
this == UnitOfMeasure.kilogram || this == UnitOfMeasure.litre;
}
/// A sellable item in the catalogue.
class Product extends Equatable {
const Product({
required this.id,
required this.name,
required this.barcode,
required this.sku,
required this.category,
required this.price,
required this.stock,
this.mrp,
this.emoji = '📦',
this.imageUrl,
this.unit = UnitOfMeasure.piece,
this.gstRate = AppConstants.defaultGstRate,
this.hsnCode,
this.brand,
this.isActive = true,
});
final String id;
final String name;
final String barcode;
final String sku;
final ProductCategory category;
/// Selling price per [unit], inclusive of GST (Indian retail convention).
final double price;
/// Printed maximum retail price, used to display savings.
final double? mrp;
final double stock;
final String emoji;
final String? imageUrl;
final UnitOfMeasure unit;
final double gstRate;
/// HSN/SAC code. Printed on the tax invoice — legally required on a GST
/// invoice above the turnover threshold. Blank when the catalogue omits it.
final String? hsnCode;
final String? brand;
final bool isActive;
bool get isOutOfStock => stock <= 0;
bool get isLowStock =>
stock > 0 && stock <= AppConstants.lowStockThreshold;
bool get hasDiscount => mrp != null && mrp! > price;
double get savings => hasDiscount ? (mrp! - price) : 0;
double get discountPercent =>
hasDiscount ? ((mrp! - price) / mrp!) * 100 : 0;
/// Price stripped of embedded GST — the taxable value.
double get netPrice => price / (1 + gstRate);
/// GST rupees embedded inside [price].
double get taxPerUnit => price - netPrice;
/// Fuzzy match used by the search bar across name, barcode, SKU and brand.
bool matches(String query) {
final q = query.trim().toLowerCase();
if (q.isEmpty) return true;
return name.toLowerCase().contains(q) ||
barcode.toLowerCase().contains(q) ||
sku.toLowerCase().contains(q) ||
(brand?.toLowerCase().contains(q) ?? false) ||
category.label.toLowerCase().contains(q);
}
Product copyWith({
String? name,
double? price,
double? mrp,
double? stock,
ProductCategory? category,
bool? isActive,
}) {
return Product(
id: id,
name: name ?? this.name,
barcode: barcode,
sku: sku,
category: category ?? this.category,
price: price ?? this.price,
mrp: mrp ?? this.mrp,
stock: stock ?? this.stock,
emoji: emoji,
imageUrl: imageUrl,
unit: unit,
gstRate: gstRate,
hsnCode: hsnCode,
brand: brand,
isActive: isActive ?? this.isActive,
);
}
@override
List<Object?> get props => [id, name, barcode, sku, price, stock, isActive];
}