second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import 'package:uuid/uuid.dart';
import '../../core/utils/extensions.dart';
import '../../domain/entities/customer.dart';
import '../../domain/repositories/customer_repository.dart';
import '../datasources/local_store.dart';
class CustomerRepositoryImpl implements CustomerRepository {
CustomerRepositoryImpl(this._store);
final LocalStore _store;
static const _uuid = Uuid();
String _digits(String v) => v.replaceAll(RegExp(r'\D'), '');
@override
Future<Customer?> findByMobile(String mobile) async {
final needle = _digits(mobile);
return _store.customers
.firstWhereOrNull((c) => _digits(c.mobile) == needle);
}
@override
Future<Customer?> findById(String id) async => _store.customerById(id);
@override
Future<Customer> create(Customer customer) async {
final existing = await findByMobile(customer.mobile);
if (existing != null) {
throw StateError('A customer with this mobile number already exists.');
}
final created = Customer(
id: _uuid.v4(),
name: customer.name.trim(),
mobile: _digits(customer.mobile),
email: customer.email?.trim().isEmpty ?? true
? null
: customer.email!.trim(),
gender: customer.gender,
dateOfBirth: customer.dateOfBirth,
loyaltyPoints: 0,
lifetimeSpend: 0,
visitCount: 0,
createdAt: DateTime.now(),
);
_store.putCustomer(created);
return created;
}
@override
Future<Customer> update(Customer customer) async {
_store.putCustomer(customer);
return customer;
}
@override
Future<Customer> recordSale({
required String customerId,
required double amount,
required int pointsEarned,
required int pointsRedeemed,
}) async {
final current = _store.customerById(customerId);
if (current == null) {
throw StateError('Customer $customerId not found.');
}
final updated = current.copyWith(
loyaltyPoints:
(current.loyaltyPoints - pointsRedeemed + pointsEarned)
.clamp(0, 1 << 31),
lifetimeSpend: (current.lifetimeSpend + amount).asMoney,
visitCount: current.visitCount + 1,
lastVisitAt: DateTime.now(),
);
_store.putCustomer(updated);
return updated;
}
@override
Future<List<Customer>> search(String query) async {
final q = query.trim().toLowerCase();
if (q.isEmpty) return recent();
return _store.customers
.where((c) =>
c.name.toLowerCase().contains(q) || _digits(c.mobile).contains(q))
.toList();
}
@override
Future<List<Customer>> recent({int limit = 20}) async {
final list = _store.customers.toList()
..sort((a, b) => (b.lastVisitAt ?? DateTime(2000))
.compareTo(a.lastVisitAt ?? DateTime(2000)));
return list.take(limit).toList();
}
}

View File

@@ -0,0 +1,68 @@
import '../../core/utils/extensions.dart';
import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../datasources/local_store.dart';
class ProductRepositoryImpl implements ProductRepository {
ProductRepositoryImpl(this._store);
final LocalStore _store;
@override
Future<List<Product>> getAll() async =>
_store.products.where((p) => p.isActive).toList();
@override
Future<List<Product>> getByCategory(ProductCategory category) async =>
_store.products
.where((p) => p.isActive && p.category == category)
.toList();
@override
Future<Product?> findByBarcode(String barcode) async {
final needle = barcode.trim();
return _store.products.firstWhereOrNull(
(p) => p.barcode == needle && p.isActive,
);
}
@override
Future<Product?> findById(String id) async => _store.productById(id);
@override
Future<List<Product>> search(String query) async {
final q = query.trim();
if (q.isEmpty) return getAll();
final results = _store.products.where((p) => p.isActive && p.matches(q));
// Rank exact barcode and SKU hits above fuzzy name matches so the top
// result is the one the cashier almost certainly meant.
final ranked = results.toList()
..sort((a, b) => _score(b, q).compareTo(_score(a, q)));
return ranked;
}
int _score(Product p, String q) {
final lq = q.toLowerCase();
if (p.barcode == q) return 100;
if (p.sku.toLowerCase() == lq) return 90;
if (p.name.toLowerCase().startsWith(lq)) return 70;
if (p.name.toLowerCase().contains(lq)) return 50;
if (p.brand?.toLowerCase().contains(lq) ?? false) return 30;
return 10;
}
@override
Future<void> decrementStock(Map<String, double> quantities) async {
quantities.forEach((id, qty) {
final p = _store.productById(id);
if (p == null) return;
final next = (p.stock - qty).clamp(0, double.infinity).toDouble();
_store.putProduct(p.copyWith(stock: next));
});
}
@override
Future<void> upsert(Product product) async => _store.putProduct(product);
}

View File

@@ -0,0 +1,159 @@
import 'package:uuid/uuid.dart';
import '../../core/utils/formatters.dart';
import '../../domain/entities/shift_report.dart';
import '../../domain/entities/sync_event.dart';
import '../../domain/repositories/sync_repository.dart';
import '../datasources/local_store.dart';
import '../datasources/remote_catalogue_source.dart';
class SyncRepositoryImpl implements SyncRepository {
SyncRepositoryImpl(this._store, this._catalogue, this._reports);
final LocalStore _store;
final RemoteCatalogueSource _catalogue;
final RemoteReportSink _reports;
static const _uuid = Uuid();
@override
bool get hasCatalogue => _store.hasCatalogue;
@override
DateTime? get lastImportAt => _store.lastImportAt;
@override
String? get catalogueRevision => _store.catalogueRevision;
@override
List<SyncEvent> get events => _store.events;
@override
bool get hasUnsyncedEvents => _store.hasUnsyncedEvents;
@override
Future<SyncEvent> importCatalogue({
void Function(double progress, String stage)? onProgress,
}) async {
final event = SyncEvent(
id: _uuid.v4(),
type: SyncEventType.catalogueImport,
status: SyncStatus.syncing,
createdAt: DateTime.now(),
summary: 'Catalogue import started',
);
_store.addEvent(event);
try {
final snapshot = await _catalogue.fetch(onProgress: onProgress);
_store.importCatalogue(
products: snapshot.products,
customers: snapshot.customers,
revision: snapshot.revision,
at: snapshot.fetchedAt,
);
final done = event.copyWith(
status: SyncStatus.synced,
syncedAt: DateTime.now(),
attempts: 1,
);
final settled = SyncEvent(
id: done.id,
type: done.type,
status: done.status,
createdAt: done.createdAt,
summary: '${snapshot.products.length} products, '
'${snapshot.customers.length} customers · ${snapshot.revision}',
payload: {
'products': snapshot.products.length,
'customers': snapshot.customers.length,
'revision': snapshot.revision,
},
syncedAt: done.syncedAt,
attempts: 1,
);
_store.updateEvent(settled);
return settled;
} catch (e) {
final failed = event.copyWith(
status: SyncStatus.failed,
error: e.toString(),
attempts: 1,
);
_store.updateEvent(failed);
return failed;
}
}
@override
ShiftReport buildShiftReport({
required DateTime businessDate,
required String terminalId,
required String cashierName,
}) {
return ShiftReport.fromTransactions(
transactions: _store.transactions,
businessDate: businessDate,
terminalId: terminalId,
cashierName: cashierName,
);
}
@override
Future<SyncEvent> pushShiftReport(ShiftReport report) async {
// Queued first, so the data is durable before the network is touched.
final queued = SyncEvent(
id: _uuid.v4(),
type: SyncEventType.shiftReport,
status: SyncStatus.pending,
createdAt: DateTime.now(),
summary: '${report.billCount} bills · '
'${Formatters.money(report.grossSales)} · '
'${Formatters.date(report.businessDate)}',
payload: report.toPayload(),
);
_store.addEvent(queued);
return _attempt(queued);
}
@override
Future<SyncEvent> retry(String eventId) async {
final matches = _store.events.where((e) => e.id == eventId).toList();
if (matches.isEmpty) {
throw StateError('No queued event with id $eventId');
}
final event = matches.first;
if (event.type == SyncEventType.catalogueImport) {
return importCatalogue();
}
return _attempt(event);
}
/// Sends one event, leaving it queued if the push fails.
Future<SyncEvent> _attempt(SyncEvent event) async {
_store.updateEvent(event.copyWith(status: SyncStatus.syncing));
try {
await _reports.push(event.payload);
final done = event.copyWith(
status: SyncStatus.synced,
syncedAt: DateTime.now(),
attempts: event.attempts + 1,
clearError: true,
);
_store.updateEvent(done);
return done.copyWith();
} catch (e) {
final failed = event.copyWith(
status: SyncStatus.failed,
error: e.toString(),
attempts: event.attempts + 1,
);
_store.updateEvent(failed);
return failed;
}
}
}

View File

@@ -0,0 +1,49 @@
import '../../core/utils/extensions.dart';
import '../../domain/entities/transaction.dart';
import '../../domain/repositories/transaction_repository.dart';
import '../datasources/local_store.dart';
class TransactionRepositoryImpl implements TransactionRepository {
TransactionRepositoryImpl(this._store);
final LocalStore _store;
@override
Future<SaleTransaction> save(SaleTransaction transaction) async {
_store.addTransaction(transaction);
return transaction;
}
@override
Future<List<SaleTransaction>> history({int limit = 50}) async =>
_store.transactions.take(limit).toList();
@override
Future<SaleTransaction?> findByInvoice(String invoiceNumber) async =>
_store.transactions
.firstWhereOrNull((t) => t.invoiceNumber == invoiceNumber);
@override
Future<int> nextInvoiceSequence() async => _store.nextInvoiceSequence();
@override
Future<void> park(ParkedBill bill) async => _store.addParked(bill);
@override
Future<List<ParkedBill>> parkedBills() async => _store.parked;
@override
Future<void> removeParked(String id) async => _store.removeParked(id);
@override
Future<double> salesTotalForDay(DateTime day) async {
return _store.transactions
.where((t) =>
t.status == TransactionStatus.completed &&
t.createdAt.year == day.year &&
t.createdAt.month == day.month &&
t.createdAt.day == day.day)
.fold(0.0, (sum, t) => sum + t.total)
.asMoney;
}
}