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:
vjrj 2026-07-11 07:24:23 +02:00
parent 89addb1ed7
commit cfa1053842
25 changed files with 4690 additions and 31 deletions

View file

@ -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();