Files
apskel-pos-flutter-v2/lib/infrastructure/product/dtos/product_dto.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

266 lines
8.8 KiB
Dart

part of '../product_dtos.dart';
@freezed
class ListProductDto with _$ListProductDto {
const ListProductDto._();
const factory ListProductDto({
@JsonKey(name: "products") required List<ProductDto> products,
@JsonKey(name: "total_count") required int totalCount,
@JsonKey(name: "page") required int page,
@JsonKey(name: "limit") required int limit,
@JsonKey(name: "total_pages") required int totalPages,
}) = _ListProductDto;
factory ListProductDto.fromJson(Map<String, dynamic> json) =>
_$ListProductDtoFromJson(json);
ListProduct toDomain() => ListProduct(
products: products.map((dto) => dto.toDomain()).toList(),
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalPages,
);
}
/// Satuan yang menempel pada produk.
///
/// Backend hanya dijamin mengirim `unit_id`; sebagian response ikut menyertakan
/// objek `unit` lengkap. Kalau ada, dipakai langsung supaya POS tidak perlu
/// menunggu master satuan termuat.
@freezed
class ProductUnitDto with _$ProductUnitDto {
const ProductUnitDto._();
const factory ProductUnitDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "name") String? name,
@JsonKey(name: "abbreviation") String? abbreviation,
}) = _ProductUnitDto;
factory ProductUnitDto.fromJson(Map<String, dynamic> json) =>
_$ProductUnitDtoFromJson(json);
}
@freezed
class ProductDto with _$ProductDto {
const ProductDto._();
const factory ProductDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "organization_id") String? organizationId,
@JsonKey(name: "category_id") String? categoryId,
@JsonKey(name: "sku") String? sku,
@JsonKey(name: "name") String? name,
@JsonKey(name: "description") String? description,
@JsonKey(name: "price") double? price,
@JsonKey(name: "cost") double? cost,
@JsonKey(name: "business_type") String? businessType,
@JsonKey(name: "image_url") String? imageUrl,
@JsonKey(name: "printer_type") String? printerType,
@JsonKey(name: "print_to_checker") bool? printToChecker,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "is_active") bool? isActive,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "variants") List<ProductVariantDto>? variants,
@JsonKey(name: "sell_by") String? sellBy,
@JsonKey(name: "unit_id") String? unitId,
@JsonKey(name: "unit_name") String? unitName,
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
@JsonKey(name: "unit") ProductUnitDto? unit,
}) = _ProductDto;
factory ProductDto.fromJson(Map<String, dynamic> json) =>
_$ProductDtoFromJson(json);
String get _resolvedUnitId =>
(unitId?.isNotEmpty == true ? unitId : unit?.id) ?? '';
String get _resolvedUnitName =>
(unitName?.isNotEmpty == true ? unitName : unit?.name) ?? '';
String get _resolvedUnitAbbreviation =>
(unitAbbreviation?.isNotEmpty == true
? unitAbbreviation
: unit?.abbreviation) ??
'';
/// Mapping ke domain
Product toDomain() => Product(
id: id ?? '',
organizationId: organizationId ?? '',
categoryId: categoryId ?? '',
sku: sku ?? '',
name: name ?? '',
description: description ?? '',
price: price ?? 0.0,
cost: cost ?? 0.0,
businessType: businessType ?? '',
imageUrl: imageUrl ?? '',
printerType: printerType ?? '',
printToChecker: printToChecker ?? false,
metadata: metadata ?? {},
isActive: isActive ?? false,
createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '',
variants: variants?.map((v) => v.toDomain()).toList() ?? [],
sellBy: SellBy.normalize(sellBy),
unitId: _resolvedUnitId,
unitName: _resolvedUnitName,
unitAbbreviation: _resolvedUnitAbbreviation,
);
Map<String, dynamic> toMap() => {
'id': id,
'organization_id': organizationId,
'category_id': categoryId,
'sku': sku,
'name': name,
'description': description,
'price': price,
'cost': cost,
'business_type': businessType,
'image_url': imageUrl,
'printer_type': printerType,
'print_to_checker': printToChecker == true ? 1 : 0,
'metadata': metadata != null ? jsonEncode(metadata) : null,
'is_active': isActive == true ? 1 : 0,
'created_at': createdAt,
'updated_at': updatedAt,
'sell_by': SellBy.normalize(sellBy),
'unit_id': _resolvedUnitId,
'unit_name': _resolvedUnitName,
'unit_abbreviation': _resolvedUnitAbbreviation,
};
factory ProductDto.fromMap(
Map<String, dynamic> map,
List<ProductVariantDto> variants,
) => ProductDto(
id: map['id'] as String?,
organizationId: map['organization_id'] as String?,
categoryId: map['category_id'] as String?,
sku: map['sku'] as String?,
name: map['name'] as String?,
description: map['description'] as String?,
price: map['price'] != null ? (map['price'] as num).toDouble() : null,
cost: map['cost'] != null ? (map['cost'] as num).toDouble() : null,
businessType: map['business_type'] as String?,
imageUrl: map['image_url'] as String?,
printerType: map['printer_type'] as String?,
printToChecker: map['print_to_checker'] != null
? (map['print_to_checker'] as int) == 1
: null,
metadata: map['metadata'] != null
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
: null,
isActive: map['is_active'] != null ? (map['is_active'] as int) == 1 : null,
createdAt: map['created_at'] as String?,
updatedAt: map['updated_at'] as String?,
variants: variants,
sellBy: map['sell_by'] as String?,
unitId: map['unit_id'] as String?,
unitName: map['unit_name'] as String?,
unitAbbreviation: map['unit_abbreviation'] as String?,
);
factory ProductDto.fromDomain(Product product) => ProductDto(
id: product.id,
organizationId: product.organizationId,
categoryId: product.categoryId,
sku: product.sku,
name: product.name,
description: product.description,
price: product.price,
cost: product.cost,
businessType: product.businessType,
imageUrl: product.imageUrl,
printerType: product.printerType,
printToChecker: product.printToChecker,
metadata: product.metadata,
isActive: product.isActive,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
variants: product.variants
.map((v) => ProductVariantDto.fromDomain(v))
.toList(),
sellBy: product.sellBy,
unitId: product.unitId,
unitName: product.unitName,
unitAbbreviation: product.unitAbbreviation,
);
}
@freezed
class ProductVariantDto with _$ProductVariantDto {
const ProductVariantDto._();
const factory ProductVariantDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "product_id") String? productId,
@JsonKey(name: "name") String? name,
@JsonKey(name: "price_modifier") double? priceModifier,
@JsonKey(name: "cost") double? cost,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
}) = _ProductVariantDto;
factory ProductVariantDto.fromJson(Map<String, dynamic> json) =>
_$ProductVariantDtoFromJson(json);
/// Mapping ke domain
ProductVariant toDomain() => ProductVariant(
id: id ?? '',
productId: productId ?? '',
name: name ?? '',
priceModifier: priceModifier ?? 0.0,
cost: cost ?? 0.0,
metadata: metadata ?? {},
createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '',
);
Map<String, dynamic> toMap() => {
'id': id,
'product_id': productId,
'name': name,
'price_modifier': priceModifier,
'cost': cost,
'metadata': metadata != null ? jsonEncode(metadata) : null,
'created_at': createdAt,
'updated_at': updatedAt,
};
factory ProductVariantDto.fromMap(Map<String, dynamic> map) =>
ProductVariantDto(
id: map['id'] as String?,
productId: map['product_id'] as String?,
name: map['name'] as String?,
priceModifier: map['price_modifier'] != null
? (map['price_modifier'] as num).toDouble()
: null,
cost: map['cost'] != null ? (map['cost'] as num).toDouble() : null,
metadata: map['metadata'] != null
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
: null,
createdAt: map['created_at'] as String?,
updatedAt: map['updated_at'] as String?,
);
factory ProductVariantDto.fromDomain(ProductVariant variant) =>
ProductVariantDto(
id: variant.id,
productId: variant.productId,
name: variant.name,
priceModifier: variant.priceModifier,
cost: variant.cost,
metadata: variant.metadata,
createdAt: variant.createdAt,
updatedAt: variant.updatedAt,
);
}