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>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/service/unit_registry.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
import '../datasources/local_data_provider.dart';
|
||||
import '../datasources/remote_data_provider.dart';
|
||||
@@ -12,10 +13,38 @@ import '../product_dtos.dart';
|
||||
class ProductRepository implements IProductRepository {
|
||||
final ProductRemoteDataProvider _remoteDataProvider;
|
||||
final ProductLocalDataProvider _localDataProvider;
|
||||
final UnitRegistry _unitRegistry;
|
||||
|
||||
final _logName = 'ProductRepository';
|
||||
|
||||
ProductRepository(this._remoteDataProvider, this._localDataProvider);
|
||||
ProductRepository(
|
||||
this._remoteDataProvider,
|
||||
this._localDataProvider,
|
||||
this._unitRegistry,
|
||||
);
|
||||
|
||||
/// Response produk hanya dijamin membawa `unit_id`. Lengkapi nama dan
|
||||
/// singkatannya dari master satuan supaya keranjang bisa menulis "4,2 ons".
|
||||
Product _withUnit(Product product) {
|
||||
if (!product.isSoldByWeight || product.unitId.isEmpty) return product;
|
||||
if (product.unitAbbreviation.isNotEmpty && product.unitName.isNotEmpty) {
|
||||
return product;
|
||||
}
|
||||
|
||||
final unit = _unitRegistry.byId(product.unitId);
|
||||
if (unit == null) return product;
|
||||
|
||||
return product.copyWith(
|
||||
unitName: product.unitName.isNotEmpty ? product.unitName : unit.name,
|
||||
unitAbbreviation: product.unitAbbreviation.isNotEmpty
|
||||
? product.unitAbbreviation
|
||||
: unit.abbreviation,
|
||||
);
|
||||
}
|
||||
|
||||
List<Product> _withUnits(List<Product> products) =>
|
||||
products.map(_withUnit).toList();
|
||||
|
||||
|
||||
@override
|
||||
Future<Either<ProductFailure, Product>> getProductById(String id) async {
|
||||
@@ -33,7 +62,7 @@ class ProductRepository implements IProductRepository {
|
||||
);
|
||||
}
|
||||
|
||||
final productDomain = product.data!.toDomain();
|
||||
final productDomain = _withUnit(product.data!.toDomain());
|
||||
|
||||
log('✅ Product loaded: ${productDomain.name}', name: _logName);
|
||||
return Right(productDomain);
|
||||
@@ -85,7 +114,9 @@ class ProductRepository implements IProductRepository {
|
||||
|
||||
// 🧱 Bangun entity domain ListProduct
|
||||
final result = ListProduct(
|
||||
products: cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
products: _withUnits(
|
||||
cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
),
|
||||
totalCount: totalCount,
|
||||
page: page,
|
||||
limit: limit,
|
||||
@@ -132,7 +163,9 @@ class ProductRepository implements IProductRepository {
|
||||
return left(cachedProducts.error!);
|
||||
}
|
||||
|
||||
final products = cachedProducts.data!.map((p) => p.toDomain()).toList();
|
||||
final products = _withUnits(
|
||||
cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
);
|
||||
|
||||
final totalCount = await _localDataProvider.getTotalCount(
|
||||
categoryId: categoryId,
|
||||
@@ -180,7 +213,9 @@ class ProductRepository implements IProductRepository {
|
||||
}
|
||||
|
||||
// ✅ Mapping ke domain entity (kalau hasilnya masih berupa DTO)
|
||||
final products = results.data!.map((p) => p.toDomain()).toList();
|
||||
final products = _withUnits(
|
||||
results.data!.map((p) => p.toDomain()).toList(),
|
||||
);
|
||||
|
||||
log(
|
||||
'✅ Local search completed: ${products.length} results',
|
||||
@@ -369,7 +404,7 @@ class ProductRepository implements IProductRepository {
|
||||
|
||||
final products = result.data!.toDomain();
|
||||
|
||||
return right(products);
|
||||
return right(products.copyWith(products: _withUnits(products.products)));
|
||||
} catch (e, s) {
|
||||
log('getProducts', name: _logName, error: e, stackTrace: s);
|
||||
return left(const ProductFailure.unexpectedError());
|
||||
|
||||
Reference in New Issue
Block a user