Pull the catalogue from a real endpoint, with delta sync
Replaces the last simulation on the inbound side. RemoteCatalogueSource
returned SeedData after a fake progress bar; there was no wire format, no
endpoint, and no way to receive an update short of reinstalling.
Wire format (data/remote/catalogue_wire.dart)
- Tolerant where it should be: a catalogue of 4,000 products must not fail to
import over one absent emoji, so optional fields take defaults and an
unrecognised category files under Grocery — the item still scans, prices and
bills.
- Strict where it matters: no id, name, barcode or price and the import fails.
A silently dropped product is a shelf item that scans to nothing, discovered
with a queue waiting.
- GST accepts 18 or 0.18 and reads both the same. Back offices disagree about
which they mean, and getting it wrong silently changes the tax on every line.
HTTP source with paging and deltas
- GET {base}/catalogue?since={revision}&page={n}. Paged because a supermarket
catalogue is tens of thousands of rows: one response times out on a shop's
line and stalls the UI decoding it. Capped at 200 pages so a bad deployment
cannot become an infinite request loop against a shop's connection.
- `since` carries the revision already held, so a normal morning fetches a
handful of price changes rather than the whole book. A server that cannot do
deltas ignores it and answers is_delta:false — the terminal reads the flag
rather than assuming, so both work.
- A bad credential is non-retryable and says so, leaving the working catalogue
in place so billing continues.
Applying deltas without losing local state
- A full snapshot withdraws what it omits; a delta must not. Read as a
snapshot, the first morning price change would empty the shelf.
- Retired products are marked inactive, not deleted — order lines already
recorded point at them, and a hard delete would orphan a bill's history.
- Locally registered shoppers survive a pull, as before.
- The unsynced-stock replay is now scoped to the products the pull actually
overwrote. It exists because a server count predates local sales; running it
over a delta that never carried that product would subtract those units a
second time and quietly empty a shelf that is full. Both halves of that rule
are tested.
MQTT stays the nudge, not the transport: a catalogue push on
pos/{store}/catalogue makes every terminal pull immediately, but the rows come
over HTTP, because a broker is the wrong shape for tens of thousands of them.
Tests: 210 -> 234. docs/sync-contract.md now covers both directions, including
a field-by-field table of what happens when something is missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
533
test/unit/catalogue_sync_test.dart
Normal file
533
test/unit/catalogue_sync_test.dart
Normal file
@@ -0,0 +1,533 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nearle_pos/core/config/sync_config.dart';
|
||||
import 'package:nearle_pos/data/datasources/local_store.dart';
|
||||
import 'package:nearle_pos/data/datasources/seed_data.dart';
|
||||
import 'package:nearle_pos/data/local/app_database.dart';
|
||||
import 'package:nearle_pos/data/remote/catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/remote/catalogue_wire.dart';
|
||||
import 'package:nearle_pos/data/remote/http_catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/repositories/product_repository_impl.dart';
|
||||
import 'package:nearle_pos/data/repositories/customer_repository_impl.dart';
|
||||
import 'package:nearle_pos/data/repositories/transaction_repository_impl.dart';
|
||||
import 'package:nearle_pos/domain/entities/cart.dart';
|
||||
import 'package:nearle_pos/domain/entities/customer.dart';
|
||||
import 'package:nearle_pos/domain/entities/product.dart';
|
||||
import 'package:nearle_pos/domain/entities/transaction.dart';
|
||||
import 'package:nearle_pos/domain/usecases/checkout_sale.dart';
|
||||
|
||||
const _config = SyncConfig(
|
||||
transport: TransportKind.http,
|
||||
httpBaseUrl: 'https://api.example.com',
|
||||
apiKey: 'k',
|
||||
storeId: 'store-01',
|
||||
terminalId: 'T4A9',
|
||||
);
|
||||
|
||||
Map<String, Object?> _product(String id, {double price = 50}) => {
|
||||
'id': id,
|
||||
'name': 'Product $id',
|
||||
'barcode': 'bc-$id',
|
||||
'sku': 'sku-$id',
|
||||
'category': 'grocery',
|
||||
'price': price,
|
||||
'stock': 20,
|
||||
'gst_rate': 0.05,
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('wire format', () {
|
||||
test('a full record round-trips', () {
|
||||
final product = CatalogueWire.productFromJson({
|
||||
..._product('p1'),
|
||||
'mrp': 60.0,
|
||||
'emoji': '🍞',
|
||||
'unit': 'kilogram',
|
||||
'hsn_code': '1905',
|
||||
'brand': 'Nearle',
|
||||
'is_active': false,
|
||||
});
|
||||
|
||||
expect(product.id, 'p1');
|
||||
expect(product.mrp, 60);
|
||||
expect(product.unit, UnitOfMeasure.kilogram);
|
||||
expect(product.hsnCode, '1905');
|
||||
expect(product.isActive, isFalse);
|
||||
|
||||
final json = CatalogueWire.productToJson(product);
|
||||
expect(CatalogueWire.productFromJson(json), product);
|
||||
});
|
||||
|
||||
test('missing optional fields take sensible defaults', () {
|
||||
// A catalogue of 4,000 products must not fail to import over one absent
|
||||
// emoji.
|
||||
final product = CatalogueWire.productFromJson({
|
||||
'id': 'p1',
|
||||
'name': 'Bread',
|
||||
'barcode': '890',
|
||||
'price': 40.0,
|
||||
});
|
||||
|
||||
expect(product.sku, 'p1', reason: 'falls back to the id');
|
||||
expect(product.emoji, '📦');
|
||||
expect(product.unit, UnitOfMeasure.piece);
|
||||
expect(product.isActive, isTrue,
|
||||
reason: 'an omitted flag is not a withdrawn catalogue',);
|
||||
});
|
||||
|
||||
test('a record with no price or barcode is refused, not dropped', () {
|
||||
// A silently dropped product is a shelf item that scans to nothing,
|
||||
// discovered with a queue waiting.
|
||||
expect(
|
||||
() => CatalogueWire.productFromJson({'id': 'p1', 'name': 'X'}),
|
||||
throwsA(isA<CatalogueFormatException>()),
|
||||
);
|
||||
expect(
|
||||
() => CatalogueWire.productFromJson({
|
||||
'id': 'p1',
|
||||
'name': 'X',
|
||||
'barcode': '890',
|
||||
}),
|
||||
throwsA(isA<CatalogueFormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('a GST rate is read the same whether sent as 18 or 0.18', () {
|
||||
// Back offices disagree about which they mean, and getting it wrong
|
||||
// silently changes the tax on every line.
|
||||
double rate(Object? raw) => CatalogueWire.productFromJson({
|
||||
..._product('p1'),
|
||||
'gst_rate': raw,
|
||||
}).gstRate;
|
||||
|
||||
expect(rate(18), 0.18);
|
||||
expect(rate(0.18), 0.18);
|
||||
expect(rate(5), 0.05);
|
||||
expect(rate(0), 0);
|
||||
});
|
||||
|
||||
test('an unknown category files under Grocery rather than failing', () {
|
||||
// The item still scans, still prices, still bills. Refusing the whole
|
||||
// import over a display detail would be far worse.
|
||||
final product = CatalogueWire.productFromJson({
|
||||
..._product('p1'),
|
||||
'category': 'frozen-desserts',
|
||||
});
|
||||
|
||||
expect(product.category, ProductCategory.grocery);
|
||||
});
|
||||
|
||||
test('a category is matched by name or by label', () {
|
||||
ProductCategory of(String raw) => CatalogueWire.productFromJson({
|
||||
..._product('p1'),
|
||||
'category': raw,
|
||||
}).category;
|
||||
|
||||
expect(of('personalCare'), ProductCategory.personalCare);
|
||||
expect(of('Personal Care'), ProductCategory.personalCare);
|
||||
expect(of('BEVERAGES'), ProductCategory.beverages);
|
||||
});
|
||||
|
||||
test('customers carry their loyalty balance across', () {
|
||||
final customer = CatalogueWire.customerFromJson({
|
||||
'id': 'c1',
|
||||
'name': 'Meena',
|
||||
'mobile': '9840000001',
|
||||
'loyalty_points': 420,
|
||||
'lifetime_spend': 61000.0,
|
||||
'last_visit_at': '2026-07-30T10:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(customer.loyaltyPoints, 420);
|
||||
expect(customer.tier, MembershipTier.gold);
|
||||
expect(customer.lastVisitAt, isNotNull);
|
||||
});
|
||||
|
||||
test('a date is accepted as ISO text or epoch milliseconds', () {
|
||||
DateTime? born(Object? raw) => CatalogueWire.customerFromJson({
|
||||
'id': 'c1',
|
||||
'name': 'M',
|
||||
'mobile': '98',
|
||||
'date_of_birth': raw,
|
||||
}).dateOfBirth;
|
||||
|
||||
expect(born('1990-05-02'), DateTime(1990, 5, 2));
|
||||
expect(born(641606400000), isNotNull);
|
||||
expect(born('not a date'), isNull);
|
||||
expect(born(null), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('http source', () {
|
||||
HttpCatalogueSource sourceReturning(
|
||||
List<Map<String, Object?>> pages, {
|
||||
List<Uri>? record,
|
||||
}) {
|
||||
var index = 0;
|
||||
return HttpCatalogueSource(
|
||||
config: _config,
|
||||
client: _FakeClient((request) {
|
||||
record?.add(request.url);
|
||||
final body = pages[index < pages.length ? index : pages.length - 1];
|
||||
index++;
|
||||
return http.Response(jsonEncode(body), 200);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test('a single page is read straight through', () async {
|
||||
final source = sourceReturning([
|
||||
{
|
||||
'revision': 'rev-1',
|
||||
'is_delta': false,
|
||||
'has_more': false,
|
||||
'products': [_product('p1'), _product('p2')],
|
||||
'customers': [
|
||||
{'id': 'c1', 'name': 'Meena', 'mobile': '9840000001'},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
final snapshot = await source.fetch();
|
||||
|
||||
expect(snapshot.products, hasLength(2));
|
||||
expect(snapshot.customers, hasLength(1));
|
||||
expect(snapshot.revision, 'rev-1');
|
||||
expect(snapshot.isDelta, isFalse);
|
||||
});
|
||||
|
||||
test('every page is followed until has_more stops', () async {
|
||||
// A supermarket catalogue is tens of thousands of rows; a single response
|
||||
// times out on a shop's line.
|
||||
final source = sourceReturning([
|
||||
{
|
||||
'revision': 'rev-2',
|
||||
'has_more': true,
|
||||
'products': [_product('p1')],
|
||||
},
|
||||
{
|
||||
'revision': 'rev-2',
|
||||
'has_more': true,
|
||||
'products': [_product('p2')],
|
||||
},
|
||||
{
|
||||
'revision': 'rev-2',
|
||||
'has_more': false,
|
||||
'products': [_product('p3')],
|
||||
},
|
||||
]);
|
||||
|
||||
final snapshot = await source.fetch();
|
||||
|
||||
expect(snapshot.products.map((p) => p.id), ['p1', 'p2', 'p3']);
|
||||
});
|
||||
|
||||
test('the revision already held is sent as `since`', () async {
|
||||
final urls = <Uri>[];
|
||||
final source = sourceReturning(
|
||||
[
|
||||
{'revision': 'rev-9', 'is_delta': true, 'has_more': false},
|
||||
],
|
||||
record: urls,
|
||||
);
|
||||
|
||||
await source.fetch(since: 'rev-8');
|
||||
|
||||
expect(urls.single.queryParameters['since'], 'rev-8');
|
||||
expect(urls.single.queryParameters['terminal_id'], 'T4A9');
|
||||
});
|
||||
|
||||
test('a delta is flagged as one, and carries its withdrawals', () async {
|
||||
final source = sourceReturning([
|
||||
{
|
||||
'revision': 'rev-9',
|
||||
'is_delta': true,
|
||||
'has_more': false,
|
||||
'products': [_product('p1', price: 55)],
|
||||
'retired_product_ids': ['p7', 'p8'],
|
||||
},
|
||||
]);
|
||||
|
||||
final snapshot = await source.fetch(since: 'rev-8');
|
||||
|
||||
expect(snapshot.isDelta, isTrue);
|
||||
expect(snapshot.retiredProductIds, ['p7', 'p8']);
|
||||
expect(snapshot.changeCount, 3);
|
||||
});
|
||||
|
||||
test('an empty delta means already up to date', () async {
|
||||
final source = sourceReturning([
|
||||
{'revision': 'rev-8', 'is_delta': true, 'has_more': false},
|
||||
]);
|
||||
|
||||
final snapshot = await source.fetch(since: 'rev-8');
|
||||
expect(snapshot.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('a bad credential is not retryable', () async {
|
||||
final source = HttpCatalogueSource(
|
||||
config: _config,
|
||||
client: _FakeClient((_) => http.Response('nope', 401)),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
source.fetch(),
|
||||
throwsA(isA<CatalogueSyncException>()
|
||||
.having((e) => e.retryable, 'retryable', isFalse),),
|
||||
);
|
||||
});
|
||||
|
||||
test('a server error is retryable', () async {
|
||||
final source = HttpCatalogueSource(
|
||||
config: _config,
|
||||
client: _FakeClient((_) => http.Response('boom', 503)),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
source.fetch(),
|
||||
throwsA(isA<CatalogueSyncException>()
|
||||
.having((e) => e.retryable, 'retryable', isTrue),),
|
||||
);
|
||||
});
|
||||
|
||||
test('an unconfigured endpoint fails fast', () async {
|
||||
final source = HttpCatalogueSource(
|
||||
config: const SyncConfig(transport: TransportKind.http),
|
||||
client: _FakeClient((_) => http.Response('{}', 200)),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
source.fetch(),
|
||||
throwsA(isA<CatalogueSyncException>()
|
||||
.having((e) => e.retryable, 'retryable', isFalse),),
|
||||
);
|
||||
});
|
||||
|
||||
test('a server that never stops paging is cut off', () async {
|
||||
// A bad deployment must not become an infinite request loop against a
|
||||
// shop's connection.
|
||||
var calls = 0;
|
||||
final source = HttpCatalogueSource(
|
||||
config: _config,
|
||||
client: _FakeClient((_) {
|
||||
calls++;
|
||||
return http.Response(
|
||||
jsonEncode({'revision': 'r', 'has_more': true, 'products': []}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expectLater(source.fetch(), throwsA(isA<CatalogueSyncException>()));
|
||||
expect(calls, lessThanOrEqualTo(HttpCatalogueSource.maxPages + 1));
|
||||
});
|
||||
|
||||
test('one malformed product fails the import rather than vanishing',
|
||||
() async {
|
||||
final source = sourceReturning([
|
||||
{
|
||||
'revision': 'rev-1',
|
||||
'has_more': false,
|
||||
'products': [
|
||||
_product('p1'),
|
||||
{'id': 'p2', 'name': 'No barcode'},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await expectLater(
|
||||
source.fetch(),
|
||||
throwsA(isA<CatalogueFormatException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('applying to the terminal', () {
|
||||
late LocalStore store;
|
||||
|
||||
setUpAll(() {
|
||||
LocalStore.registerSeed(
|
||||
products: SeedData.products,
|
||||
customers: SeedData.customers,
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
store = LocalStore.instance;
|
||||
await store.reset(withCatalogue: true);
|
||||
});
|
||||
|
||||
test('a delta leaves everything it does not mention alone', () async {
|
||||
// Applied as a full snapshot, the first morning price change would empty
|
||||
// the shelf.
|
||||
final before = store.products.length;
|
||||
final milk = store.products.firstWhere((p) => p.barcode == '8901234500011');
|
||||
|
||||
await store.importCatalogue(
|
||||
products: [milk.copyWith(price: 71)],
|
||||
customers: const [],
|
||||
revision: 'rev-2',
|
||||
at: DateTime.now(),
|
||||
isDelta: true,
|
||||
);
|
||||
|
||||
expect(store.products, hasLength(before));
|
||||
expect(store.productById(milk.id)!.price, 71);
|
||||
});
|
||||
|
||||
test('a full snapshot withdraws what it omits', () async {
|
||||
final milk = store.products.firstWhere((p) => p.barcode == '8901234500011');
|
||||
|
||||
await store.importCatalogue(
|
||||
products: [milk],
|
||||
customers: const [],
|
||||
revision: 'rev-3',
|
||||
at: DateTime.now(),
|
||||
);
|
||||
|
||||
expect(store.products, hasLength(1));
|
||||
});
|
||||
|
||||
test('a retired product is withdrawn, not deleted', () async {
|
||||
// An order line already recorded points at it; a hard delete would orphan
|
||||
// a bill's history.
|
||||
final milk = store.products.firstWhere((p) => p.barcode == '8901234500011');
|
||||
|
||||
await store.importCatalogue(
|
||||
products: const [],
|
||||
customers: const [],
|
||||
revision: 'rev-4',
|
||||
at: DateTime.now(),
|
||||
isDelta: true,
|
||||
retiredProductIds: [milk.id],
|
||||
);
|
||||
|
||||
expect(store.productByBarcode('8901234500011'), isNull,
|
||||
reason: 'a withdrawn product must not scan',);
|
||||
|
||||
// The cache holds only what is sellable, so the surviving row has to be
|
||||
// checked on disk — which is the point: an order line already recorded
|
||||
// still resolves to a real product.
|
||||
final rows = await AppDatabase.instance.db.query(
|
||||
'products',
|
||||
where: 'id = ?',
|
||||
whereArgs: [milk.id],
|
||||
);
|
||||
expect(rows, hasLength(1));
|
||||
expect(rows.single['is_active'], 0);
|
||||
});
|
||||
|
||||
test('a locally added shopper survives a pull', () async {
|
||||
final customers = CustomerRepositoryImpl(store);
|
||||
final walkIn = await customers.create(const Customer(
|
||||
id: 'ignored',
|
||||
name: 'Local Only',
|
||||
mobile: '9000000077',
|
||||
),);
|
||||
|
||||
await store.importCatalogue(
|
||||
products: const [],
|
||||
customers: const [],
|
||||
revision: 'rev-5',
|
||||
at: DateTime.now(),
|
||||
isDelta: true,
|
||||
);
|
||||
|
||||
expect(await customers.findById(walkIn.id), isNotNull);
|
||||
});
|
||||
|
||||
test('a delta does not subtract already-sold stock a second time',
|
||||
() async {
|
||||
// The replay exists because a *full* pull overwrites stock with a server
|
||||
// figure that predates local sales. Running it over a delta that never
|
||||
// carried that product would quietly empty a shelf that is full.
|
||||
final products = ProductRepositoryImpl(store);
|
||||
final checkout = CheckoutSale(
|
||||
productRepository: products,
|
||||
customerRepository: CustomerRepositoryImpl(store),
|
||||
transactionRepository: TransactionRepositoryImpl(store),
|
||||
);
|
||||
|
||||
final milk = (await products.findByBarcode('8901234500011'))!;
|
||||
final opening = milk.stock;
|
||||
|
||||
await checkout(
|
||||
cart: Cart(lines: [CartLine(product: milk, quantity: 4)]),
|
||||
payments: const [
|
||||
PaymentSplit(method: PaymentMethod.cash, amount: 248, tendered: 250),
|
||||
],
|
||||
cashierName: 'Divya',
|
||||
terminalId: 'T0TEST',
|
||||
);
|
||||
|
||||
expect((await products.findById(milk.id))!.stock, opening - 4);
|
||||
|
||||
// A delta about a *different* product must not touch the milk count.
|
||||
final other = store.products.firstWhere((p) => p.id != milk.id);
|
||||
await store.importCatalogue(
|
||||
products: [other.copyWith(price: other.price + 1)],
|
||||
customers: const [],
|
||||
revision: 'rev-6',
|
||||
at: DateTime.now(),
|
||||
isDelta: true,
|
||||
);
|
||||
|
||||
expect((await products.findById(milk.id))!.stock, opening - 4,
|
||||
reason: 'the sold units must not be subtracted again',);
|
||||
});
|
||||
|
||||
test('a delta carrying the sold product replays its committed stock',
|
||||
() async {
|
||||
// The other half of the same rule: when the server *does* send a fresh
|
||||
// count for a product, that figure predates the local sale and the units
|
||||
// would otherwise reappear on the shelf.
|
||||
final products = ProductRepositoryImpl(store);
|
||||
final checkout = CheckoutSale(
|
||||
productRepository: products,
|
||||
customerRepository: CustomerRepositoryImpl(store),
|
||||
transactionRepository: TransactionRepositoryImpl(store),
|
||||
);
|
||||
|
||||
final milk = (await products.findByBarcode('8901234500011'))!;
|
||||
final opening = milk.stock;
|
||||
|
||||
await checkout(
|
||||
cart: Cart(lines: [CartLine(product: milk, quantity: 4)]),
|
||||
payments: const [
|
||||
PaymentSplit(method: PaymentMethod.cash, amount: 248, tendered: 250),
|
||||
],
|
||||
cashierName: 'Divya',
|
||||
terminalId: 'T0TEST',
|
||||
);
|
||||
|
||||
await store.importCatalogue(
|
||||
products: [milk],
|
||||
customers: const [],
|
||||
revision: 'rev-7',
|
||||
at: DateTime.now(),
|
||||
isDelta: true,
|
||||
);
|
||||
|
||||
expect((await products.findById(milk.id))!.stock, opening - 4,
|
||||
reason: 'the server count is stale by exactly the unsynced sales',);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeClient extends http.BaseClient {
|
||||
_FakeClient(this.respond);
|
||||
|
||||
final http.Response Function(http.BaseRequest) respond;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
final response = respond(request);
|
||||
return http.StreamedResponse(
|
||||
Stream.value(utf8.encode(response.body)),
|
||||
response.statusCode,
|
||||
request: request,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nearle_pos/data/datasources/local_store.dart';
|
||||
import 'package:nearle_pos/data/datasources/remote_catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/remote/simulated_catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/datasources/seed_data.dart';
|
||||
import 'package:nearle_pos/data/remote/simulated_order_transport.dart';
|
||||
import 'package:nearle_pos/data/repositories/customer_repository_impl.dart';
|
||||
@@ -276,7 +276,7 @@ void main() {
|
||||
test('the archived day totals match what was charged', () async {
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
SimulatedOrderTransport(isOffline: () => false),
|
||||
);
|
||||
|
||||
@@ -321,7 +321,7 @@ void main() {
|
||||
() async {
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
SimulatedOrderTransport(isOffline: () => false),
|
||||
);
|
||||
|
||||
@@ -351,7 +351,7 @@ void main() {
|
||||
test('a failed import is recorded with its error', () async {
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => true),
|
||||
SimulatedCatalogueSource(isOffline: () => true),
|
||||
SimulatedOrderTransport(isOffline: () => true),
|
||||
);
|
||||
|
||||
@@ -380,7 +380,7 @@ void main() {
|
||||
test('a cashier settles their own till, not the terminal', () async {
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
SimulatedOrderTransport(isOffline: () => false),
|
||||
);
|
||||
|
||||
@@ -406,7 +406,7 @@ void main() {
|
||||
test('scoping holds after the bills are uploaded and deleted', () async {
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
SimulatedOrderTransport(isOffline: () => false),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nearle_pos/data/datasources/local_store.dart';
|
||||
import 'package:nearle_pos/data/datasources/remote_catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/remote/simulated_catalogue_source.dart';
|
||||
import 'package:nearle_pos/data/datasources/seed_data.dart';
|
||||
import 'package:nearle_pos/data/local/app_database.dart';
|
||||
import 'package:nearle_pos/data/local/order_dao.dart';
|
||||
@@ -84,7 +84,7 @@ void main() {
|
||||
|
||||
SyncRepositoryImpl syncWith(OrderTransport transport) => SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
transport,
|
||||
);
|
||||
|
||||
@@ -342,7 +342,7 @@ void main() {
|
||||
|
||||
final sync = SyncRepositoryImpl(
|
||||
store,
|
||||
RemoteCatalogueSource(isOffline: () => false),
|
||||
SimulatedCatalogueSource(isOffline: () => false),
|
||||
transport,
|
||||
batchSize: 2,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user