feat(offers): asking price on lots, published with sale offers (schema v11)
The data model always meant sale offers to carry an informational price (data-model §2.8, sharing-model §4.4); with offer state collapsed onto the Lot, the price lives there. Null amount on a sell lot = 'to be agreed' — the NIP-99 event simply omits the price tag. - schema v11: Lots.price_amount/price_currency, guarded migration, schema dump + generated schema_v11 for the round-trip test - ShareableLot/VarietyLot carry the price (sell-only in shareableLots); addLot/updateLot persist it and clear it when the lot leaves sale - OffersCubit.publishLots forwards it (outbox flush picks it up free) - lot sheet: price + currency inputs revealed only on 'For sale', with the €/Ğ1/hours quick-picks extracted to a shared CurrencyQuickPicks widget (reused by the sale sheet) - backup JSON round-trips the new columns - i18n share.price/priceHint in en/es/pt/ast
This commit is contained in:
parent
5ecdd1222a
commit
1103b6ce15
25 changed files with 4690 additions and 31 deletions
2171
apps/app_seeds/drift_schemas/drift_schema_v11.json
Normal file
2171
apps/app_seeds/drift_schemas/drift_schema_v11.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -79,6 +79,8 @@ class InventoryJsonCodec {
|
|||
'originPlace': l.originPlace,
|
||||
'abundance': l.abundance?.name,
|
||||
'preservationFormat': l.preservationFormat?.name,
|
||||
'priceAmount': l.priceAmount,
|
||||
'priceCurrency': l.priceCurrency,
|
||||
},
|
||||
],
|
||||
'vernacularNames': [
|
||||
|
|
@ -338,6 +340,8 @@ class InventoryJsonCodec {
|
|||
PreservationFormat.values,
|
||||
m['preservationFormat'],
|
||||
),
|
||||
priceAmount: (m['priceAmount'] as num?)?.toDouble(),
|
||||
priceCurrency: m['priceCurrency'] as String?,
|
||||
);
|
||||
}),
|
||||
vernacularNames: _rows(root, 'vernacularNames', (m) {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,8 @@ class ShareableLot extends Equatable {
|
|||
this.category,
|
||||
this.harvestYear,
|
||||
this.isOrganic = false,
|
||||
this.priceAmount,
|
||||
this.priceCurrency,
|
||||
});
|
||||
|
||||
final String lotId;
|
||||
|
|
@ -246,9 +248,23 @@ class ShareableLot extends Equatable {
|
|||
/// filter the market for it.
|
||||
final bool isOrganic;
|
||||
|
||||
/// Asking price, only populated for [OfferStatus.sell] lots. Null amount on
|
||||
/// a sale means "price to be agreed" — the offer publishes without a price.
|
||||
final double? priceAmount;
|
||||
final String? priceCurrency;
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[lotId, varietyId, summary, offerStatus, category, harvestYear, isOrganic];
|
||||
List<Object?> get props => [
|
||||
lotId,
|
||||
varietyId,
|
||||
summary,
|
||||
offerStatus,
|
||||
category,
|
||||
harvestYear,
|
||||
isOrganic,
|
||||
priceAmount,
|
||||
priceCurrency,
|
||||
];
|
||||
}
|
||||
|
||||
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
|
||||
|
|
@ -322,6 +338,8 @@ class VarietyLot extends Equatable {
|
|||
this.abundance,
|
||||
this.preservationFormat,
|
||||
this.offerStatus = OfferStatus.private,
|
||||
this.priceAmount,
|
||||
this.priceCurrency,
|
||||
this.germinationTests = const [],
|
||||
this.conditionChecks = const [],
|
||||
});
|
||||
|
|
@ -354,6 +372,11 @@ class VarietyLot extends Equatable {
|
|||
/// the network is Block 2.
|
||||
final OfferStatus offerStatus;
|
||||
|
||||
/// Asking price, meaningful when [offerStatus] is `sell`. Null amount means
|
||||
/// "price to be agreed".
|
||||
final double? priceAmount;
|
||||
final String? priceCurrency;
|
||||
|
||||
final List<GerminationEntry> germinationTests;
|
||||
|
||||
/// Storage-condition checks, most-recent first (`conditionChecks.first` = last).
|
||||
|
|
@ -377,6 +400,8 @@ class VarietyLot extends Equatable {
|
|||
abundance,
|
||||
preservationFormat,
|
||||
offerStatus,
|
||||
priceAmount,
|
||||
priceCurrency,
|
||||
germinationTests,
|
||||
conditionChecks,
|
||||
];
|
||||
|
|
@ -1281,9 +1306,12 @@ class VarietyRepository {
|
|||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
OfferStatus offerStatus = OfferStatus.private,
|
||||
double? priceAmount,
|
||||
String? priceCurrency,
|
||||
}) async {
|
||||
final (created, updated) = await _stamp();
|
||||
final id = idGen.newId();
|
||||
final forSale = offerStatus == OfferStatus.sell;
|
||||
await _db
|
||||
.into(_db.lots)
|
||||
.insert(
|
||||
|
|
@ -1306,6 +1334,8 @@ class VarietyRepository {
|
|||
abundance: Value(abundance),
|
||||
preservationFormat: Value(preservationFormat),
|
||||
offerStatus: Value(offerStatus),
|
||||
priceAmount: Value(forSale ? priceAmount : null),
|
||||
priceCurrency: Value(forSale ? priceCurrency : null),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
|
|
@ -1326,8 +1356,13 @@ class VarietyRepository {
|
|||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
OfferStatus offerStatus = OfferStatus.private,
|
||||
double? priceAmount,
|
||||
String? priceCurrency,
|
||||
}) async {
|
||||
final (_, updated) = await _stamp();
|
||||
// Price only makes sense on a lot offered for sale; clearing the sale
|
||||
// status clears the price with it.
|
||||
final forSale = offerStatus == OfferStatus.sell;
|
||||
await (_db.update(_db.lots)..where((l) => l.id.equals(lotId))).write(
|
||||
LotsCompanion(
|
||||
type: Value(type),
|
||||
|
|
@ -1343,6 +1378,8 @@ class VarietyRepository {
|
|||
abundance: Value(abundance),
|
||||
preservationFormat: Value(preservationFormat),
|
||||
offerStatus: Value(offerStatus),
|
||||
priceAmount: Value(forSale ? priceAmount : null),
|
||||
priceCurrency: Value(forSale ? priceCurrency : null),
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
|
|
@ -1580,6 +1617,12 @@ class VarietyRepository {
|
|||
category: v.category,
|
||||
harvestYear: l.harvestYear,
|
||||
isOrganic: v.isOrganic,
|
||||
priceAmount: l.offerStatus == OfferStatus.sell
|
||||
? l.priceAmount
|
||||
: null,
|
||||
priceCurrency: l.offerStatus == OfferStatus.sell
|
||||
? l.priceCurrency
|
||||
: null,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
@ -2363,6 +2406,8 @@ class VarietyRepository {
|
|||
abundance: l.abundance,
|
||||
preservationFormat: l.preservationFormat,
|
||||
offerStatus: l.offerStatus,
|
||||
priceAmount: l.priceAmount,
|
||||
priceCurrency: l.priceCurrency,
|
||||
germinationTests: germinationTests,
|
||||
conditionChecks: conditionChecks,
|
||||
quantity: hasQuantity
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class AppDatabase extends _$AppDatabase {
|
|||
|
||||
/// Current schema version; also stamped into interchange exports so an
|
||||
/// importer knows which app generation wrote the file (data-model §7).
|
||||
static const int currentSchemaVersion = 10;
|
||||
static const int currentSchemaVersion = 11;
|
||||
|
||||
@override
|
||||
int get schemaVersion => currentSchemaVersion;
|
||||
|
|
@ -153,6 +153,16 @@ class AppDatabase extends _$AppDatabase {
|
|||
await m.createTable(sales);
|
||||
}
|
||||
}
|
||||
// v11: asking price on Lots — published with "sell" market offers.
|
||||
// Guarded (see the v7 note above).
|
||||
if (from < 11) {
|
||||
if (!await _hasColumn('lots', 'price_amount')) {
|
||||
await m.addColumn(lots, lots.priceAmount);
|
||||
}
|
||||
if (!await _hasColumn('lots', 'price_currency')) {
|
||||
await m.addColumn(lots, lots.priceCurrency);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -3266,6 +3266,28 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
).withConverter<PreservationFormat?>(
|
||||
$LotsTable.$converterpreservationFormatn,
|
||||
);
|
||||
static const VerificationMeta _priceAmountMeta = const VerificationMeta(
|
||||
'priceAmount',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<double> priceAmount = GeneratedColumn<double>(
|
||||
'price_amount',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _priceCurrencyMeta = const VerificationMeta(
|
||||
'priceCurrency',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> priceCurrency = GeneratedColumn<String>(
|
||||
'price_currency',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
|
|
@ -3289,6 +3311,8 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
originPlace,
|
||||
abundance,
|
||||
preservationFormat,
|
||||
priceAmount,
|
||||
priceCurrency,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
|
|
@ -3429,6 +3453,24 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('price_amount')) {
|
||||
context.handle(
|
||||
_priceAmountMeta,
|
||||
priceAmount.isAcceptableOrUnknown(
|
||||
data['price_amount']!,
|
||||
_priceAmountMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('price_currency')) {
|
||||
context.handle(
|
||||
_priceCurrencyMeta,
|
||||
priceCurrency.isAcceptableOrUnknown(
|
||||
data['price_currency']!,
|
||||
_priceCurrencyMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -3532,6 +3574,14 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
data['${effectivePrefix}preservation_format'],
|
||||
),
|
||||
),
|
||||
priceAmount: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}price_amount'],
|
||||
),
|
||||
priceCurrency: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}price_currency'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3600,6 +3650,17 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
/// How the (seed) lot is physically conserved — see [PreservationFormat].
|
||||
/// Distinct from [storageLocation]. Optional.
|
||||
final PreservationFormat? preservationFormat;
|
||||
|
||||
/// Asking price when [offerStatus] is `sell` (data-model §2.8 puts price on
|
||||
/// the Offer; with offer state collapsed onto the Lot, it lives here and is
|
||||
/// published with the market offer). Informational only — money changes
|
||||
/// hands off-platform, never in-app. Null amount on a sell lot means
|
||||
/// "price to be agreed" (the offer publishes without a price).
|
||||
final double? priceAmount;
|
||||
|
||||
/// Free-text currency, like [Sales.currency]: "€", "Ğ1", a local/time
|
||||
/// currency. Never assumed (sharing-model §6).
|
||||
final String? priceCurrency;
|
||||
const Lot({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
|
|
@ -3622,6 +3683,8 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
this.originPlace,
|
||||
this.abundance,
|
||||
this.preservationFormat,
|
||||
this.priceAmount,
|
||||
this.priceCurrency,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
|
|
@ -3683,6 +3746,12 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
$LotsTable.$converterpreservationFormatn.toSql(preservationFormat),
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || priceAmount != null) {
|
||||
map['price_amount'] = Variable<double>(priceAmount);
|
||||
}
|
||||
if (!nullToAbsent || priceCurrency != null) {
|
||||
map['price_currency'] = Variable<String>(priceCurrency);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
@ -3733,6 +3802,12 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
preservationFormat: preservationFormat == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(preservationFormat),
|
||||
priceAmount: priceAmount == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(priceAmount),
|
||||
priceCurrency: priceCurrency == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(priceCurrency),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3773,6 +3848,8 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
preservationFormat: $LotsTable.$converterpreservationFormatn.fromJson(
|
||||
serializer.fromJson<String?>(json['preservationFormat']),
|
||||
),
|
||||
priceAmount: serializer.fromJson<double?>(json['priceAmount']),
|
||||
priceCurrency: serializer.fromJson<String?>(json['priceCurrency']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
|
@ -3808,6 +3885,8 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
'preservationFormat': serializer.toJson<String?>(
|
||||
$LotsTable.$converterpreservationFormatn.toJson(preservationFormat),
|
||||
),
|
||||
'priceAmount': serializer.toJson<double?>(priceAmount),
|
||||
'priceCurrency': serializer.toJson<String?>(priceCurrency),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3833,6 +3912,8 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
Value<String?> originPlace = const Value.absent(),
|
||||
Value<Abundance?> abundance = const Value.absent(),
|
||||
Value<PreservationFormat?> preservationFormat = const Value.absent(),
|
||||
Value<double?> priceAmount = const Value.absent(),
|
||||
Value<String?> priceCurrency = const Value.absent(),
|
||||
}) => Lot(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
|
|
@ -3863,6 +3944,10 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
preservationFormat: preservationFormat.present
|
||||
? preservationFormat.value
|
||||
: this.preservationFormat,
|
||||
priceAmount: priceAmount.present ? priceAmount.value : this.priceAmount,
|
||||
priceCurrency: priceCurrency.present
|
||||
? priceCurrency.value
|
||||
: this.priceCurrency,
|
||||
);
|
||||
Lot copyWithCompanion(LotsCompanion data) {
|
||||
return Lot(
|
||||
|
|
@ -3915,6 +4000,12 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
preservationFormat: data.preservationFormat.present
|
||||
? data.preservationFormat.value
|
||||
: this.preservationFormat,
|
||||
priceAmount: data.priceAmount.present
|
||||
? data.priceAmount.value
|
||||
: this.priceAmount,
|
||||
priceCurrency: data.priceCurrency.present
|
||||
? data.priceCurrency.value
|
||||
: this.priceCurrency,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3941,7 +4032,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
..write('originName: $originName, ')
|
||||
..write('originPlace: $originPlace, ')
|
||||
..write('abundance: $abundance, ')
|
||||
..write('preservationFormat: $preservationFormat')
|
||||
..write('preservationFormat: $preservationFormat, ')
|
||||
..write('priceAmount: $priceAmount, ')
|
||||
..write('priceCurrency: $priceCurrency')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
|
@ -3969,6 +4062,8 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
originPlace,
|
||||
abundance,
|
||||
preservationFormat,
|
||||
priceAmount,
|
||||
priceCurrency,
|
||||
]);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
|
|
@ -3994,7 +4089,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
other.originName == this.originName &&
|
||||
other.originPlace == this.originPlace &&
|
||||
other.abundance == this.abundance &&
|
||||
other.preservationFormat == this.preservationFormat);
|
||||
other.preservationFormat == this.preservationFormat &&
|
||||
other.priceAmount == this.priceAmount &&
|
||||
other.priceCurrency == this.priceCurrency);
|
||||
}
|
||||
|
||||
class LotsCompanion extends UpdateCompanion<Lot> {
|
||||
|
|
@ -4019,6 +4116,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
final Value<String?> originPlace;
|
||||
final Value<Abundance?> abundance;
|
||||
final Value<PreservationFormat?> preservationFormat;
|
||||
final Value<double?> priceAmount;
|
||||
final Value<String?> priceCurrency;
|
||||
final Value<int> rowid;
|
||||
const LotsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
|
|
@ -4042,6 +4141,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
this.originPlace = const Value.absent(),
|
||||
this.abundance = const Value.absent(),
|
||||
this.preservationFormat = const Value.absent(),
|
||||
this.priceAmount = const Value.absent(),
|
||||
this.priceCurrency = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
LotsCompanion.insert({
|
||||
|
|
@ -4066,6 +4167,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
this.originPlace = const Value.absent(),
|
||||
this.abundance = const Value.absent(),
|
||||
this.preservationFormat = const Value.absent(),
|
||||
this.priceAmount = const Value.absent(),
|
||||
this.priceCurrency = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
createdAt = Value(createdAt),
|
||||
|
|
@ -4094,6 +4197,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
Expression<String>? originPlace,
|
||||
Expression<String>? abundance,
|
||||
Expression<String>? preservationFormat,
|
||||
Expression<double>? priceAmount,
|
||||
Expression<String>? priceCurrency,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
|
|
@ -4118,6 +4223,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
if (originPlace != null) 'origin_place': originPlace,
|
||||
if (abundance != null) 'abundance': abundance,
|
||||
if (preservationFormat != null) 'preservation_format': preservationFormat,
|
||||
if (priceAmount != null) 'price_amount': priceAmount,
|
||||
if (priceCurrency != null) 'price_currency': priceCurrency,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
|
@ -4144,6 +4251,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
Value<String?>? originPlace,
|
||||
Value<Abundance?>? abundance,
|
||||
Value<PreservationFormat?>? preservationFormat,
|
||||
Value<double?>? priceAmount,
|
||||
Value<String?>? priceCurrency,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return LotsCompanion(
|
||||
|
|
@ -4168,6 +4277,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
originPlace: originPlace ?? this.originPlace,
|
||||
abundance: abundance ?? this.abundance,
|
||||
preservationFormat: preservationFormat ?? this.preservationFormat,
|
||||
priceAmount: priceAmount ?? this.priceAmount,
|
||||
priceCurrency: priceCurrency ?? this.priceCurrency,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
|
@ -4250,6 +4361,12 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
),
|
||||
);
|
||||
}
|
||||
if (priceAmount.present) {
|
||||
map['price_amount'] = Variable<double>(priceAmount.value);
|
||||
}
|
||||
if (priceCurrency.present) {
|
||||
map['price_currency'] = Variable<String>(priceCurrency.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
|
|
@ -4280,6 +4397,8 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
..write('originPlace: $originPlace, ')
|
||||
..write('abundance: $abundance, ')
|
||||
..write('preservationFormat: $preservationFormat, ')
|
||||
..write('priceAmount: $priceAmount, ')
|
||||
..write('priceCurrency: $priceCurrency, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
|
|
@ -11663,6 +11782,8 @@ typedef $$LotsTableCreateCompanionBuilder =
|
|||
Value<String?> originPlace,
|
||||
Value<Abundance?> abundance,
|
||||
Value<PreservationFormat?> preservationFormat,
|
||||
Value<double?> priceAmount,
|
||||
Value<String?> priceCurrency,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$LotsTableUpdateCompanionBuilder =
|
||||
|
|
@ -11688,6 +11809,8 @@ typedef $$LotsTableUpdateCompanionBuilder =
|
|||
Value<String?> originPlace,
|
||||
Value<Abundance?> abundance,
|
||||
Value<PreservationFormat?> preservationFormat,
|
||||
Value<double?> priceAmount,
|
||||
Value<String?> priceCurrency,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
|
|
@ -11812,6 +11935,16 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> {
|
|||
column: $table.preservationFormat,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<double> get priceAmount => $composableBuilder(
|
||||
column: $table.priceAmount,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get priceCurrency => $composableBuilder(
|
||||
column: $table.priceCurrency,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> {
|
||||
|
|
@ -11926,6 +12059,16 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> {
|
|||
column: $table.preservationFormat,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<double> get priceAmount => $composableBuilder(
|
||||
column: $table.priceAmount,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get priceCurrency => $composableBuilder(
|
||||
column: $table.priceCurrency,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$LotsTableAnnotationComposer
|
||||
|
|
@ -12030,6 +12173,16 @@ class $$LotsTableAnnotationComposer
|
|||
column: $table.preservationFormat,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<double> get priceAmount => $composableBuilder(
|
||||
column: $table.priceAmount,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get priceCurrency => $composableBuilder(
|
||||
column: $table.priceCurrency,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$LotsTableTableManager
|
||||
|
|
@ -12082,6 +12235,8 @@ class $$LotsTableTableManager
|
|||
Value<Abundance?> abundance = const Value.absent(),
|
||||
Value<PreservationFormat?> preservationFormat =
|
||||
const Value.absent(),
|
||||
Value<double?> priceAmount = const Value.absent(),
|
||||
Value<String?> priceCurrency = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => LotsCompanion(
|
||||
id: id,
|
||||
|
|
@ -12105,6 +12260,8 @@ class $$LotsTableTableManager
|
|||
originPlace: originPlace,
|
||||
abundance: abundance,
|
||||
preservationFormat: preservationFormat,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
|
|
@ -12131,6 +12288,8 @@ class $$LotsTableTableManager
|
|||
Value<Abundance?> abundance = const Value.absent(),
|
||||
Value<PreservationFormat?> preservationFormat =
|
||||
const Value.absent(),
|
||||
Value<double?> priceAmount = const Value.absent(),
|
||||
Value<String?> priceCurrency = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => LotsCompanion.insert(
|
||||
id: id,
|
||||
|
|
@ -12154,6 +12313,8 @@ class $$LotsTableTableManager
|
|||
originPlace: originPlace,
|
||||
abundance: abundance,
|
||||
preservationFormat: preservationFormat,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
|
|
|||
|
|
@ -109,6 +109,17 @@ class Lots extends Table with SyncColumns {
|
|||
/// Distinct from [storageLocation]. Optional.
|
||||
TextColumn get preservationFormat =>
|
||||
textEnum<PreservationFormat>().nullable()();
|
||||
|
||||
/// Asking price when [offerStatus] is `sell` (data-model §2.8 puts price on
|
||||
/// the Offer; with offer state collapsed onto the Lot, it lives here and is
|
||||
/// published with the market offer). Informational only — money changes
|
||||
/// hands off-platform, never in-app. Null amount on a sell lot means
|
||||
/// "price to be agreed" (the offer publishes without a price).
|
||||
RealColumn get priceAmount => real().nullable()();
|
||||
|
||||
/// Free-text currency, like [Sales.currency]: "€", "Ğ1", a local/time
|
||||
/// currency. Never assumed (sharing-model §6).
|
||||
TextColumn get priceCurrency => text().nullable()();
|
||||
}
|
||||
|
||||
/// Optional germination history for a Lot; percent is derived in code.
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@
|
|||
"add": "¿Compártesla?",
|
||||
"title": "¿Compártesla?",
|
||||
"nudge": "Tienes a esgaya: podríes compartir un poco.",
|
||||
"price": "Preciu",
|
||||
"priceHint": "Déxalu baleru p'alcordalu depués",
|
||||
"private": "Namás pa min",
|
||||
"gift": "Pa regalar",
|
||||
"exchange": "Pa trocar",
|
||||
|
|
|
|||
|
|
@ -304,6 +304,8 @@
|
|||
"add": "Do you share it?",
|
||||
"title": "Do you share it?",
|
||||
"nudge": "You have plenty — you could share some.",
|
||||
"price": "Price",
|
||||
"priceHint": "Leave it empty to agree it later",
|
||||
"private": "Just for me",
|
||||
"gift": "To give away",
|
||||
"exchange": "To swap",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@
|
|||
"add": "¿La compartes?",
|
||||
"title": "¿La compartes?",
|
||||
"nudge": "Tienes de sobra: podrías compartir un poco.",
|
||||
"price": "Precio",
|
||||
"priceHint": "Déjalo vacío para acordarlo luego",
|
||||
"private": "Solo para mí",
|
||||
"gift": "Para regalar",
|
||||
"exchange": "Para intercambiar",
|
||||
|
|
|
|||
|
|
@ -300,6 +300,8 @@
|
|||
"add": "Partilhas esta?",
|
||||
"title": "Partilhas esta?",
|
||||
"nudge": "Tens de sobra: podias partilhar um pouco.",
|
||||
"price": "Preço",
|
||||
"priceHint": "Deixa vazio para combinar depois",
|
||||
"private": "Só para mim",
|
||||
"gift": "Para dar",
|
||||
"exchange": "Para trocar",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 4
|
||||
/// Strings: 1968 (492 per locale)
|
||||
/// Strings: 1976 (494 per locale)
|
||||
///
|
||||
/// Built on 2026-07-11 at 05:50 UTC
|
||||
/// Built on 2026-07-11 at 06:00 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -547,6 +547,8 @@ class _Translations$share$ast extends Translations$share$en {
|
|||
@override String get add => '¿Compártesla?';
|
||||
@override String get title => '¿Compártesla?';
|
||||
@override String get nudge => 'Tienes a esgaya: podríes compartir un poco.';
|
||||
@override String get price => 'Preciu';
|
||||
@override String get priceHint => 'Déxalu baleru p\'alcordalu depués';
|
||||
@override String get private => 'Namás pa min';
|
||||
@override String get gift => 'Pa regalar';
|
||||
@override String get exchange => 'Pa trocar';
|
||||
|
|
@ -1501,6 +1503,8 @@ extension on TranslationsAst {
|
|||
'share.add' => '¿Compártesla?',
|
||||
'share.title' => '¿Compártesla?',
|
||||
'share.nudge' => 'Tienes a esgaya: podríes compartir un poco.',
|
||||
'share.price' => 'Preciu',
|
||||
'share.priceHint' => 'Déxalu baleru p\'alcordalu depués',
|
||||
'share.private' => 'Namás pa min',
|
||||
'share.gift' => 'Pa regalar',
|
||||
'share.exchange' => 'Pa trocar',
|
||||
|
|
|
|||
|
|
@ -988,6 +988,12 @@ class Translations$share$en {
|
|||
/// en: 'You have plenty — you could share some.'
|
||||
String get nudge => 'You have plenty — you could share some.';
|
||||
|
||||
/// en: 'Price'
|
||||
String get price => 'Price';
|
||||
|
||||
/// en: 'Leave it empty to agree it later'
|
||||
String get priceHint => 'Leave it empty to agree it later';
|
||||
|
||||
/// en: 'Just for me'
|
||||
String get private => 'Just for me';
|
||||
|
||||
|
|
@ -2473,6 +2479,8 @@ extension on Translations {
|
|||
'share.add' => 'Do you share it?',
|
||||
'share.title' => 'Do you share it?',
|
||||
'share.nudge' => 'You have plenty — you could share some.',
|
||||
'share.price' => 'Price',
|
||||
'share.priceHint' => 'Leave it empty to agree it later',
|
||||
'share.private' => 'Just for me',
|
||||
'share.gift' => 'To give away',
|
||||
'share.exchange' => 'To swap',
|
||||
|
|
|
|||
|
|
@ -547,6 +547,8 @@ class _Translations$share$es extends Translations$share$en {
|
|||
@override String get add => '¿La compartes?';
|
||||
@override String get title => '¿La compartes?';
|
||||
@override String get nudge => 'Tienes de sobra: podrías compartir un poco.';
|
||||
@override String get price => 'Precio';
|
||||
@override String get priceHint => 'Déjalo vacío para acordarlo luego';
|
||||
@override String get private => 'Solo para mí';
|
||||
@override String get gift => 'Para regalar';
|
||||
@override String get exchange => 'Para intercambiar';
|
||||
|
|
@ -1503,6 +1505,8 @@ extension on TranslationsEs {
|
|||
'share.add' => '¿La compartes?',
|
||||
'share.title' => '¿La compartes?',
|
||||
'share.nudge' => 'Tienes de sobra: podrías compartir un poco.',
|
||||
'share.price' => 'Precio',
|
||||
'share.priceHint' => 'Déjalo vacío para acordarlo luego',
|
||||
'share.private' => 'Solo para mí',
|
||||
'share.gift' => 'Para regalar',
|
||||
'share.exchange' => 'Para intercambiar',
|
||||
|
|
|
|||
|
|
@ -544,6 +544,8 @@ class _Translations$share$pt extends Translations$share$en {
|
|||
@override String get add => 'Partilhas esta?';
|
||||
@override String get title => 'Partilhas esta?';
|
||||
@override String get nudge => 'Tens de sobra: podias partilhar um pouco.';
|
||||
@override String get price => 'Preço';
|
||||
@override String get priceHint => 'Deixa vazio para combinar depois';
|
||||
@override String get private => 'Só para mim';
|
||||
@override String get gift => 'Para dar';
|
||||
@override String get exchange => 'Para trocar';
|
||||
|
|
@ -1497,6 +1499,8 @@ extension on TranslationsPt {
|
|||
'share.add' => 'Partilhas esta?',
|
||||
'share.title' => 'Partilhas esta?',
|
||||
'share.nudge' => 'Tens de sobra: podias partilhar um pouco.',
|
||||
'share.price' => 'Preço',
|
||||
'share.priceHint' => 'Deixa vazio para combinar depois',
|
||||
'share.private' => 'Só para mim',
|
||||
'share.gift' => 'Para dar',
|
||||
'share.exchange' => 'Para trocar',
|
||||
|
|
|
|||
|
|
@ -284,6 +284,8 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
areaGeohash: areaGeohash,
|
||||
category: lot.category,
|
||||
isOrganic: lot.isOrganic,
|
||||
priceAmount: lot.priceAmount,
|
||||
priceCurrency: lot.priceCurrency,
|
||||
imageUrl: await _coverThumbnail(lot.varietyId),
|
||||
);
|
||||
final result = await transport.publish(offer);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
OfferStatus offerStatus = OfferStatus.private,
|
||||
double? priceAmount,
|
||||
String? priceCurrency,
|
||||
}) => _repo.addLot(
|
||||
varietyId: varietyId,
|
||||
type: type,
|
||||
|
|
@ -93,6 +95,8 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
abundance: abundance,
|
||||
preservationFormat: preservationFormat,
|
||||
offerStatus: offerStatus,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
|
||||
Future<void> updateLot({
|
||||
|
|
@ -107,6 +111,8 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
Abundance? abundance,
|
||||
PreservationFormat? preservationFormat,
|
||||
OfferStatus offerStatus = OfferStatus.private,
|
||||
double? priceAmount,
|
||||
String? priceCurrency,
|
||||
}) => _repo.updateLot(
|
||||
lotId: lotId,
|
||||
type: type,
|
||||
|
|
@ -119,6 +125,8 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
abundance: abundance,
|
||||
preservationFormat: preservationFormat,
|
||||
offerStatus: offerStatus,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
|
||||
Future<void> deleteLot(String lotId) => _repo.softDeleteLot(lotId);
|
||||
|
|
|
|||
44
apps/app_seeds/lib/ui/currency_quick_picks.dart
Normal file
44
apps/app_seeds/lib/ui/currency_quick_picks.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// One-tap currency chips bound to a free-text [controller]. Ğ1 sits quietly
|
||||
/// among the familiar ones — offered, never imposed — and free text still
|
||||
/// takes anything else (sharing-model §6). Shared by the sale sheet, the lot
|
||||
/// price field and the hand-over sheet so every money-ish input feels the same.
|
||||
class CurrencyQuickPicks extends StatelessWidget {
|
||||
const CurrencyQuickPicks({
|
||||
required this.controller,
|
||||
required this.keyPrefix,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
/// Namespaces the chip [Key]s (e.g. `sale` → `sale.currencyChip.€`).
|
||||
final String keyPrefix;
|
||||
|
||||
/// Fires after a chip toggles the controller text (for `setState`).
|
||||
final VoidCallback onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final c in ['€', 'Ğ1', t.sale.hours])
|
||||
ChoiceChip(
|
||||
key: Key('$keyPrefix.currencyChip.$c'),
|
||||
label: Text(c),
|
||||
selected: controller.text.trim() == c,
|
||||
onSelected: (sel) {
|
||||
controller.text = sel ? c : '';
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Opens the "record a sale" sheet. Optionally pre-attached to [varietyId]
|
||||
|
|
@ -140,22 +141,10 @@ class _SaleSheetState extends State<_SaleSheet> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Quick picks so a currency is one tap. Ğ1 sits quietly among the
|
||||
// familiar ones — offered, never imposed — and free text still takes
|
||||
// anything else.
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final c in ['€', 'Ğ1', t.sale.hours])
|
||||
ChoiceChip(
|
||||
key: Key('sale.currencyChip.$c'),
|
||||
label: Text(c),
|
||||
selected: _currency.text.trim() == c,
|
||||
onSelected: (sel) => setState(() {
|
||||
_currency.text = sel ? c : '';
|
||||
}),
|
||||
),
|
||||
],
|
||||
CurrencyQuickPicks(
|
||||
controller: _currency,
|
||||
keyPrefix: 'sale',
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import '../domain/seed_viability.dart';
|
|||
import '../domain/species_reference_links.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/variety_detail_cubit.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'harvest_date_picker.dart';
|
||||
import 'photo_pick.dart';
|
||||
import 'plantare_sheet.dart';
|
||||
|
|
@ -1464,6 +1465,18 @@ Future<void> _showLotSheet(
|
|||
existing?.originName != null || existing?.originPlace != null;
|
||||
var showAbundance = existing?.abundance != null;
|
||||
var showShare = selectedShare != OfferStatus.private;
|
||||
final existingPrice = existing?.priceAmount;
|
||||
final priceAmountCtrl = TextEditingController(
|
||||
// Drop a trailing .0 so the field shows "5", not "5.0".
|
||||
text: existingPrice == null
|
||||
? ''
|
||||
: (existingPrice == existingPrice.roundToDouble()
|
||||
? existingPrice.toInt().toString()
|
||||
: existingPrice.toString()),
|
||||
);
|
||||
final priceCurrencyCtrl = TextEditingController(
|
||||
text: existing?.priceCurrency ?? '',
|
||||
);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -1660,6 +1673,49 @@ Future<void> _showLotSheet(
|
|||
style: Theme.of(sheetContext).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// Price only when selling — informational, agreed and
|
||||
// paid off-app. Empty amount = "to be agreed".
|
||||
if (selectedShare == OfferStatus.sell) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('lot.priceAmount'),
|
||||
controller: priceAmountCtrl,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: t.share.price,
|
||||
helperText: t.share.priceHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('lot.priceCurrency'),
|
||||
controller: priceCurrencyCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.sale.currency,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CurrencyQuickPicks(
|
||||
controller: priceCurrencyCtrl,
|
||||
keyPrefix: 'lot',
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
],
|
||||
],
|
||||
// Layer 2: advanced seed-bank details, collapsed by default.
|
||||
// Only for seed lots, and (condition checks) an existing lot.
|
||||
|
|
@ -1710,6 +1766,10 @@ Future<void> _showLotSheet(
|
|||
onPressed: () {
|
||||
final originName = _nullIfBlank(originNameCtrl.text);
|
||||
final originPlace = _nullIfBlank(originPlaceCtrl.text);
|
||||
final priceAmount = double.tryParse(
|
||||
priceAmountCtrl.text.replaceAll(',', '.').trim(),
|
||||
);
|
||||
final priceCurrency = _nullIfBlank(priceCurrencyCtrl.text);
|
||||
if (editing) {
|
||||
cubit.updateLot(
|
||||
lotId: existing.id,
|
||||
|
|
@ -1723,6 +1783,8 @@ Future<void> _showLotSheet(
|
|||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
} else {
|
||||
cubit.addLot(
|
||||
|
|
@ -1736,6 +1798,8 @@ Future<void> _showLotSheet(
|
|||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
}
|
||||
Navigator.of(sheetContext).pop();
|
||||
|
|
|
|||
|
|
@ -40,4 +40,71 @@ void main() {
|
|||
final lots = await repo.shareableLots();
|
||||
expect(lots.where((l) => l.lotId == privateId), isEmpty);
|
||||
});
|
||||
|
||||
test('sell lots expose their asking price; other statuses never do',
|
||||
() async {
|
||||
final repo = newTestRepository(db);
|
||||
final varietyId = await repo.addQuickVariety(label: 'Calabaza');
|
||||
final sellId = await repo.addLot(
|
||||
varietyId: varietyId,
|
||||
offerStatus: OfferStatus.sell,
|
||||
priceAmount: 3.5,
|
||||
priceCurrency: 'Ğ1',
|
||||
);
|
||||
// Price passed on a gift lot is dropped, not stored.
|
||||
final giftId = await repo.addLot(
|
||||
varietyId: varietyId,
|
||||
offerStatus: OfferStatus.shared,
|
||||
priceAmount: 99,
|
||||
priceCurrency: '€',
|
||||
);
|
||||
|
||||
final lots = await repo.shareableLots();
|
||||
final sell = lots.singleWhere((l) => l.lotId == sellId);
|
||||
expect(sell.priceAmount, 3.5);
|
||||
expect(sell.priceCurrency, 'Ğ1');
|
||||
final gift = lots.singleWhere((l) => l.lotId == giftId);
|
||||
expect(gift.priceAmount, isNull);
|
||||
expect(gift.priceCurrency, isNull);
|
||||
});
|
||||
|
||||
test('a sell lot without amount is "to be agreed" — shareable, no price',
|
||||
() async {
|
||||
final repo = newTestRepository(db);
|
||||
final varietyId = await repo.addQuickVariety(label: 'Lechuga');
|
||||
final sellId = await repo.addLot(
|
||||
varietyId: varietyId,
|
||||
offerStatus: OfferStatus.sell,
|
||||
);
|
||||
|
||||
final lots = await repo.shareableLots();
|
||||
final sell = lots.singleWhere((l) => l.lotId == sellId);
|
||||
expect(sell.priceAmount, isNull);
|
||||
});
|
||||
|
||||
test('moving a lot out of sale clears its stored price', () async {
|
||||
final repo = newTestRepository(db);
|
||||
final varietyId = await repo.addQuickVariety(label: 'Haba');
|
||||
final lotId = await repo.addLot(
|
||||
varietyId: varietyId,
|
||||
offerStatus: OfferStatus.sell,
|
||||
priceAmount: 2,
|
||||
priceCurrency: '€',
|
||||
);
|
||||
|
||||
await repo.updateLot(
|
||||
lotId: lotId,
|
||||
type: LotType.seed,
|
||||
offerStatus: OfferStatus.shared,
|
||||
// Price args still sent by the form; must be ignored off-sale.
|
||||
priceAmount: 2,
|
||||
priceCurrency: '€',
|
||||
);
|
||||
|
||||
final detail = await repo.watchVariety(varietyId).first;
|
||||
final lot = detail!.lots.singleWhere((l) => l.id == lotId);
|
||||
expect(lot.priceAmount, isNull);
|
||||
expect(lot.priceCurrency, isNull);
|
||||
expect(lot.offerStatus, OfferStatus.shared);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ void main() {
|
|||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('freshly created database matches the exported schema v10', () async {
|
||||
final schema = await verifier.schemaAt(10);
|
||||
test('freshly created database matches the exported schema v11', () async {
|
||||
final schema = await verifier.schemaAt(11);
|
||||
final db = AppDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 10);
|
||||
await verifier.migrateAndValidate(db, 11);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
// Every historical version upgrades cleanly to the current schema (v10).
|
||||
for (var from = 1; from <= 9; from++) {
|
||||
test('upgrades v$from → v10 and matches the fresh schema', () async {
|
||||
// Every historical version upgrades cleanly to the current schema (v11).
|
||||
for (var from = 1; from <= 10; from++) {
|
||||
test('upgrades v$from → v11 and matches the fresh schema', () async {
|
||||
final connection = await verifier.startAt(from);
|
||||
final db = AppDatabase(connection);
|
||||
await verifier.migrateAndValidate(db, 10);
|
||||
await verifier.migrateAndValidate(db, 11);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'schema_v7.dart' as v7;
|
|||
import 'schema_v8.dart' as v8;
|
||||
import 'schema_v9.dart' as v9;
|
||||
import 'schema_v10.dart' as v10;
|
||||
import 'schema_v11.dart' as v11;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -39,10 +40,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v9.DatabaseAtV9(db);
|
||||
case 10:
|
||||
return v10.DatabaseAtV10(db);
|
||||
case 11:
|
||||
return v11.DatabaseAtV11(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
}
|
||||
|
|
|
|||
2015
apps/app_seeds/test/db/schema/schema_v11.dart
Normal file
2015
apps/app_seeds/test/db/schema/schema_v11.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -294,6 +294,43 @@ void main() {
|
|||
await cubit.close();
|
||||
});
|
||||
|
||||
test('publishLots carries the lot asking price on sale offers', () async {
|
||||
final transport = FakeOfferTransport();
|
||||
final cubit = OffersCubit(transport);
|
||||
await cubit.publishLots(
|
||||
const [
|
||||
ShareableLot(
|
||||
lotId: 'lot-sale',
|
||||
varietyId: 'var-1',
|
||||
summary: 'Tomate',
|
||||
offerStatus: OfferStatus.sell,
|
||||
priceAmount: 3.5,
|
||||
priceCurrency: 'Ğ1',
|
||||
),
|
||||
// "To be agreed": sale without a figure publishes without a price.
|
||||
ShareableLot(
|
||||
lotId: 'lot-negotiable',
|
||||
varietyId: 'var-2',
|
||||
summary: 'Judía',
|
||||
offerStatus: OfferStatus.sell,
|
||||
),
|
||||
],
|
||||
authorPubkeyHex: 'ab' * 32,
|
||||
areaGeohash: 'sp3e9',
|
||||
);
|
||||
|
||||
await cubit.discover('sp3');
|
||||
await pumpEventQueue();
|
||||
final sale = cubit.state.offers.singleWhere((o) => o.id == 'lot-sale');
|
||||
expect(sale.type, OfferType.sale);
|
||||
expect(sale.priceAmount, 3.5);
|
||||
expect(sale.priceCurrency, 'Ğ1');
|
||||
final negotiable =
|
||||
cubit.state.offers.singleWhere((o) => o.id == 'lot-negotiable');
|
||||
expect(negotiable.priceAmount, isNull);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('publishLots embeds an inline thumbnail data-URI on the offer',
|
||||
() async {
|
||||
final transport = FakeOfferTransport();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue