feat: make server owner of tabs

This commit is contained in:
2026-10-20 16:15:14 +02:00
parent 5754ae742d
commit 93acfe15a9
26 changed files with 3654 additions and 389 deletions
+38 -1
View File
@@ -162,6 +162,9 @@ class RemoteSyncState extends Table {
TextColumn get serverCursor => text().nullable()();
/// Where the feed of settlements made anywhere in the venue left off.
TextColumn get settlementsCursor => text().nullable()();
DateTimeColumn get lastSuccessfulSyncAt => dateTime().nullable()();
DateTimeColumn get lastAttemptedSyncAt => dateTime().nullable()();
@@ -204,6 +207,26 @@ class TabEvents extends Table {
Set<Column> get primaryKey => {id};
}
/// Outcomes of the stock adjustments sent by the dashboard, waiting to be
/// reported. A row also records that its adjustment was handled, so it is
/// kept for a while after the server acknowledges it: the same adjustment
/// can still arrive in a response computed before the server saw the outcome.
@DataClassName('StockAdjustmentResultRow')
class StockAdjustmentResults extends Table {
/// The adjustment's id from the server.
TextColumn get id => text()();
/// The outcome as JSON, exactly as it is sent.
TextColumn get payload => text()();
BoolColumn get acknowledged => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
@DriftDatabase(
tables: [
Products,
@@ -216,13 +239,14 @@ class TabEvents extends Table {
RemoteSyncState,
ProcessedRemoteCommands,
TabEvents,
StockAdjustmentResults,
],
)
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 15;
int get schemaVersion => 17;
@override
MigrationStrategy get migration {
@@ -325,6 +349,19 @@ class AppDatabase extends _$AppDatabase {
if (from < 15) {
await migrator.createTable(tabEvents);
}
if (from < 16) {
await migrator.createTable(stockAdjustmentResults);
}
if (from < 17 &&
await hasTableColumn('remote_sync_state', 'device_id') &&
!await hasTableColumn('remote_sync_state', 'settlements_cursor')) {
await migrator.addColumn(
remoteSyncState,
remoteSyncState.settlementsCursor,
);
}
},
);
}
+618
View File
@@ -3060,6 +3060,18 @@ class $RemoteSyncStateTable extends RemoteSyncState
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _settlementsCursorMeta = const VerificationMeta(
'settlementsCursor',
);
@override
late final GeneratedColumn<String> settlementsCursor =
GeneratedColumn<String>(
'settlements_cursor',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _lastSuccessfulSyncAtMeta =
const VerificationMeta('lastSuccessfulSyncAt');
@override
@@ -3098,6 +3110,7 @@ class $RemoteSyncStateTable extends RemoteSyncState
List<GeneratedColumn> get $columns => [
deviceId,
serverCursor,
settlementsCursor,
lastSuccessfulSyncAt,
lastAttemptedSyncAt,
consecutiveFailureCount,
@@ -3131,6 +3144,15 @@ class $RemoteSyncStateTable extends RemoteSyncState
),
);
}
if (data.containsKey('settlements_cursor')) {
context.handle(
_settlementsCursorMeta,
settlementsCursor.isAcceptableOrUnknown(
data['settlements_cursor']!,
_settlementsCursorMeta,
),
);
}
if (data.containsKey('last_successful_sync_at')) {
context.handle(
_lastSuccessfulSyncAtMeta,
@@ -3175,6 +3197,10 @@ class $RemoteSyncStateTable extends RemoteSyncState
DriftSqlType.string,
data['${effectivePrefix}server_cursor'],
),
settlementsCursor: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}settlements_cursor'],
),
lastSuccessfulSyncAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}last_successful_sync_at'],
@@ -3200,12 +3226,16 @@ class RemoteSyncStateRow extends DataClass
implements Insertable<RemoteSyncStateRow> {
final String deviceId;
final String? serverCursor;
/// Where the feed of settlements made anywhere in the venue left off.
final String? settlementsCursor;
final DateTime? lastSuccessfulSyncAt;
final DateTime? lastAttemptedSyncAt;
final int consecutiveFailureCount;
const RemoteSyncStateRow({
required this.deviceId,
this.serverCursor,
this.settlementsCursor,
this.lastSuccessfulSyncAt,
this.lastAttemptedSyncAt,
required this.consecutiveFailureCount,
@@ -3217,6 +3247,9 @@ class RemoteSyncStateRow extends DataClass
if (!nullToAbsent || serverCursor != null) {
map['server_cursor'] = Variable<String>(serverCursor);
}
if (!nullToAbsent || settlementsCursor != null) {
map['settlements_cursor'] = Variable<String>(settlementsCursor);
}
if (!nullToAbsent || lastSuccessfulSyncAt != null) {
map['last_successful_sync_at'] = Variable<DateTime>(lastSuccessfulSyncAt);
}
@@ -3233,6 +3266,9 @@ class RemoteSyncStateRow extends DataClass
serverCursor: serverCursor == null && nullToAbsent
? const Value.absent()
: Value(serverCursor),
settlementsCursor: settlementsCursor == null && nullToAbsent
? const Value.absent()
: Value(settlementsCursor),
lastSuccessfulSyncAt: lastSuccessfulSyncAt == null && nullToAbsent
? const Value.absent()
: Value(lastSuccessfulSyncAt),
@@ -3251,6 +3287,9 @@ class RemoteSyncStateRow extends DataClass
return RemoteSyncStateRow(
deviceId: serializer.fromJson<String>(json['deviceId']),
serverCursor: serializer.fromJson<String?>(json['serverCursor']),
settlementsCursor: serializer.fromJson<String?>(
json['settlementsCursor'],
),
lastSuccessfulSyncAt: serializer.fromJson<DateTime?>(
json['lastSuccessfulSyncAt'],
),
@@ -3268,6 +3307,7 @@ class RemoteSyncStateRow extends DataClass
return <String, dynamic>{
'deviceId': serializer.toJson<String>(deviceId),
'serverCursor': serializer.toJson<String?>(serverCursor),
'settlementsCursor': serializer.toJson<String?>(settlementsCursor),
'lastSuccessfulSyncAt': serializer.toJson<DateTime?>(
lastSuccessfulSyncAt,
),
@@ -3281,12 +3321,16 @@ class RemoteSyncStateRow extends DataClass
RemoteSyncStateRow copyWith({
String? deviceId,
Value<String?> serverCursor = const Value.absent(),
Value<String?> settlementsCursor = const Value.absent(),
Value<DateTime?> lastSuccessfulSyncAt = const Value.absent(),
Value<DateTime?> lastAttemptedSyncAt = const Value.absent(),
int? consecutiveFailureCount,
}) => RemoteSyncStateRow(
deviceId: deviceId ?? this.deviceId,
serverCursor: serverCursor.present ? serverCursor.value : this.serverCursor,
settlementsCursor: settlementsCursor.present
? settlementsCursor.value
: this.settlementsCursor,
lastSuccessfulSyncAt: lastSuccessfulSyncAt.present
? lastSuccessfulSyncAt.value
: this.lastSuccessfulSyncAt,
@@ -3302,6 +3346,9 @@ class RemoteSyncStateRow extends DataClass
serverCursor: data.serverCursor.present
? data.serverCursor.value
: this.serverCursor,
settlementsCursor: data.settlementsCursor.present
? data.settlementsCursor.value
: this.settlementsCursor,
lastSuccessfulSyncAt: data.lastSuccessfulSyncAt.present
? data.lastSuccessfulSyncAt.value
: this.lastSuccessfulSyncAt,
@@ -3319,6 +3366,7 @@ class RemoteSyncStateRow extends DataClass
return (StringBuffer('RemoteSyncStateRow(')
..write('deviceId: $deviceId, ')
..write('serverCursor: $serverCursor, ')
..write('settlementsCursor: $settlementsCursor, ')
..write('lastSuccessfulSyncAt: $lastSuccessfulSyncAt, ')
..write('lastAttemptedSyncAt: $lastAttemptedSyncAt, ')
..write('consecutiveFailureCount: $consecutiveFailureCount')
@@ -3330,6 +3378,7 @@ class RemoteSyncStateRow extends DataClass
int get hashCode => Object.hash(
deviceId,
serverCursor,
settlementsCursor,
lastSuccessfulSyncAt,
lastAttemptedSyncAt,
consecutiveFailureCount,
@@ -3340,6 +3389,7 @@ class RemoteSyncStateRow extends DataClass
(other is RemoteSyncStateRow &&
other.deviceId == this.deviceId &&
other.serverCursor == this.serverCursor &&
other.settlementsCursor == this.settlementsCursor &&
other.lastSuccessfulSyncAt == this.lastSuccessfulSyncAt &&
other.lastAttemptedSyncAt == this.lastAttemptedSyncAt &&
other.consecutiveFailureCount == this.consecutiveFailureCount);
@@ -3348,6 +3398,7 @@ class RemoteSyncStateRow extends DataClass
class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
final Value<String> deviceId;
final Value<String?> serverCursor;
final Value<String?> settlementsCursor;
final Value<DateTime?> lastSuccessfulSyncAt;
final Value<DateTime?> lastAttemptedSyncAt;
final Value<int> consecutiveFailureCount;
@@ -3355,6 +3406,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
const RemoteSyncStateCompanion({
this.deviceId = const Value.absent(),
this.serverCursor = const Value.absent(),
this.settlementsCursor = const Value.absent(),
this.lastSuccessfulSyncAt = const Value.absent(),
this.lastAttemptedSyncAt = const Value.absent(),
this.consecutiveFailureCount = const Value.absent(),
@@ -3363,6 +3415,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
RemoteSyncStateCompanion.insert({
required String deviceId,
this.serverCursor = const Value.absent(),
this.settlementsCursor = const Value.absent(),
this.lastSuccessfulSyncAt = const Value.absent(),
this.lastAttemptedSyncAt = const Value.absent(),
this.consecutiveFailureCount = const Value.absent(),
@@ -3371,6 +3424,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
static Insertable<RemoteSyncStateRow> custom({
Expression<String>? deviceId,
Expression<String>? serverCursor,
Expression<String>? settlementsCursor,
Expression<DateTime>? lastSuccessfulSyncAt,
Expression<DateTime>? lastAttemptedSyncAt,
Expression<int>? consecutiveFailureCount,
@@ -3379,6 +3433,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
return RawValuesInsertable({
if (deviceId != null) 'device_id': deviceId,
if (serverCursor != null) 'server_cursor': serverCursor,
if (settlementsCursor != null) 'settlements_cursor': settlementsCursor,
if (lastSuccessfulSyncAt != null)
'last_successful_sync_at': lastSuccessfulSyncAt,
if (lastAttemptedSyncAt != null)
@@ -3392,6 +3447,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
RemoteSyncStateCompanion copyWith({
Value<String>? deviceId,
Value<String?>? serverCursor,
Value<String?>? settlementsCursor,
Value<DateTime?>? lastSuccessfulSyncAt,
Value<DateTime?>? lastAttemptedSyncAt,
Value<int>? consecutiveFailureCount,
@@ -3400,6 +3456,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
return RemoteSyncStateCompanion(
deviceId: deviceId ?? this.deviceId,
serverCursor: serverCursor ?? this.serverCursor,
settlementsCursor: settlementsCursor ?? this.settlementsCursor,
lastSuccessfulSyncAt: lastSuccessfulSyncAt ?? this.lastSuccessfulSyncAt,
lastAttemptedSyncAt: lastAttemptedSyncAt ?? this.lastAttemptedSyncAt,
consecutiveFailureCount:
@@ -3417,6 +3474,9 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
if (serverCursor.present) {
map['server_cursor'] = Variable<String>(serverCursor.value);
}
if (settlementsCursor.present) {
map['settlements_cursor'] = Variable<String>(settlementsCursor.value);
}
if (lastSuccessfulSyncAt.present) {
map['last_successful_sync_at'] = Variable<DateTime>(
lastSuccessfulSyncAt.value,
@@ -3443,6 +3503,7 @@ class RemoteSyncStateCompanion extends UpdateCompanion<RemoteSyncStateRow> {
return (StringBuffer('RemoteSyncStateCompanion(')
..write('deviceId: $deviceId, ')
..write('serverCursor: $serverCursor, ')
..write('settlementsCursor: $settlementsCursor, ')
..write('lastSuccessfulSyncAt: $lastSuccessfulSyncAt, ')
..write('lastAttemptedSyncAt: $lastAttemptedSyncAt, ')
..write('consecutiveFailureCount: $consecutiveFailureCount, ')
@@ -4060,6 +4121,332 @@ class TabEventsCompanion extends UpdateCompanion<TabEventRow> {
}
}
class $StockAdjustmentResultsTable extends StockAdjustmentResults
with TableInfo<$StockAdjustmentResultsTable, StockAdjustmentResultRow> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$StockAdjustmentResultsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _payloadMeta = const VerificationMeta(
'payload',
);
@override
late final GeneratedColumn<String> payload = GeneratedColumn<String>(
'payload',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _acknowledgedMeta = const VerificationMeta(
'acknowledged',
);
@override
late final GeneratedColumn<bool> acknowledged = GeneratedColumn<bool>(
'acknowledged',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledged" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt',
);
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
);
@override
List<GeneratedColumn> get $columns => [id, payload, acknowledged, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'stock_adjustment_results';
@override
VerificationContext validateIntegrity(
Insertable<StockAdjustmentResultRow> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('payload')) {
context.handle(
_payloadMeta,
payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta),
);
} else if (isInserting) {
context.missing(_payloadMeta);
}
if (data.containsKey('acknowledged')) {
context.handle(
_acknowledgedMeta,
acknowledged.isAcceptableOrUnknown(
data['acknowledged']!,
_acknowledgedMeta,
),
);
}
if (data.containsKey('created_at')) {
context.handle(
_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
);
} else if (isInserting) {
context.missing(_createdAtMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
StockAdjustmentResultRow map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return StockAdjustmentResultRow(
id: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
payload: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}payload'],
)!,
acknowledged: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}acknowledged'],
)!,
createdAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
);
}
@override
$StockAdjustmentResultsTable createAlias(String alias) {
return $StockAdjustmentResultsTable(attachedDatabase, alias);
}
}
class StockAdjustmentResultRow extends DataClass
implements Insertable<StockAdjustmentResultRow> {
/// The adjustment's id from the server.
final String id;
/// The outcome as JSON, exactly as it is sent.
final String payload;
final bool acknowledged;
final DateTime createdAt;
const StockAdjustmentResultRow({
required this.id,
required this.payload,
required this.acknowledged,
required this.createdAt,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['payload'] = Variable<String>(payload);
map['acknowledged'] = Variable<bool>(acknowledged);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
StockAdjustmentResultsCompanion toCompanion(bool nullToAbsent) {
return StockAdjustmentResultsCompanion(
id: Value(id),
payload: Value(payload),
acknowledged: Value(acknowledged),
createdAt: Value(createdAt),
);
}
factory StockAdjustmentResultRow.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return StockAdjustmentResultRow(
id: serializer.fromJson<String>(json['id']),
payload: serializer.fromJson<String>(json['payload']),
acknowledged: serializer.fromJson<bool>(json['acknowledged']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'payload': serializer.toJson<String>(payload),
'acknowledged': serializer.toJson<bool>(acknowledged),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
StockAdjustmentResultRow copyWith({
String? id,
String? payload,
bool? acknowledged,
DateTime? createdAt,
}) => StockAdjustmentResultRow(
id: id ?? this.id,
payload: payload ?? this.payload,
acknowledged: acknowledged ?? this.acknowledged,
createdAt: createdAt ?? this.createdAt,
);
StockAdjustmentResultRow copyWithCompanion(
StockAdjustmentResultsCompanion data,
) {
return StockAdjustmentResultRow(
id: data.id.present ? data.id.value : this.id,
payload: data.payload.present ? data.payload.value : this.payload,
acknowledged: data.acknowledged.present
? data.acknowledged.value
: this.acknowledged,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('StockAdjustmentResultRow(')
..write('id: $id, ')
..write('payload: $payload, ')
..write('acknowledged: $acknowledged, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, payload, acknowledged, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is StockAdjustmentResultRow &&
other.id == this.id &&
other.payload == this.payload &&
other.acknowledged == this.acknowledged &&
other.createdAt == this.createdAt);
}
class StockAdjustmentResultsCompanion
extends UpdateCompanion<StockAdjustmentResultRow> {
final Value<String> id;
final Value<String> payload;
final Value<bool> acknowledged;
final Value<DateTime> createdAt;
final Value<int> rowid;
const StockAdjustmentResultsCompanion({
this.id = const Value.absent(),
this.payload = const Value.absent(),
this.acknowledged = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
});
StockAdjustmentResultsCompanion.insert({
required String id,
required String payload,
this.acknowledged = const Value.absent(),
required DateTime createdAt,
this.rowid = const Value.absent(),
}) : id = Value(id),
payload = Value(payload),
createdAt = Value(createdAt);
static Insertable<StockAdjustmentResultRow> custom({
Expression<String>? id,
Expression<String>? payload,
Expression<bool>? acknowledged,
Expression<DateTime>? createdAt,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (payload != null) 'payload': payload,
if (acknowledged != null) 'acknowledged': acknowledged,
if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid,
});
}
StockAdjustmentResultsCompanion copyWith({
Value<String>? id,
Value<String>? payload,
Value<bool>? acknowledged,
Value<DateTime>? createdAt,
Value<int>? rowid,
}) {
return StockAdjustmentResultsCompanion(
id: id ?? this.id,
payload: payload ?? this.payload,
acknowledged: acknowledged ?? this.acknowledged,
createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (payload.present) {
map['payload'] = Variable<String>(payload.value);
}
if (acknowledged.present) {
map['acknowledged'] = Variable<bool>(acknowledged.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('StockAdjustmentResultsCompanion(')
..write('id: $id, ')
..write('payload: $payload, ')
..write('acknowledged: $acknowledged, ')
..write('createdAt: $createdAt, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
@@ -4080,6 +4467,8 @@ abstract class _$AppDatabase extends GeneratedDatabase {
late final $ProcessedRemoteCommandsTable processedRemoteCommands =
$ProcessedRemoteCommandsTable(this);
late final $TabEventsTable tabEvents = $TabEventsTable(this);
late final $StockAdjustmentResultsTable stockAdjustmentResults =
$StockAdjustmentResultsTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@@ -4095,6 +4484,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
remoteSyncState,
processedRemoteCommands,
tabEvents,
stockAdjustmentResults,
];
@override
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
@@ -6355,6 +6745,7 @@ typedef $$RemoteSyncStateTableCreateCompanionBuilder =
RemoteSyncStateCompanion Function({
required String deviceId,
Value<String?> serverCursor,
Value<String?> settlementsCursor,
Value<DateTime?> lastSuccessfulSyncAt,
Value<DateTime?> lastAttemptedSyncAt,
Value<int> consecutiveFailureCount,
@@ -6364,6 +6755,7 @@ typedef $$RemoteSyncStateTableUpdateCompanionBuilder =
RemoteSyncStateCompanion Function({
Value<String> deviceId,
Value<String?> serverCursor,
Value<String?> settlementsCursor,
Value<DateTime?> lastSuccessfulSyncAt,
Value<DateTime?> lastAttemptedSyncAt,
Value<int> consecutiveFailureCount,
@@ -6389,6 +6781,11 @@ class $$RemoteSyncStateTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get settlementsCursor => $composableBuilder(
column: $table.settlementsCursor,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get lastSuccessfulSyncAt => $composableBuilder(
column: $table.lastSuccessfulSyncAt,
builder: (column) => ColumnFilters(column),
@@ -6424,6 +6821,11 @@ class $$RemoteSyncStateTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get settlementsCursor => $composableBuilder(
column: $table.settlementsCursor,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get lastSuccessfulSyncAt => $composableBuilder(
column: $table.lastSuccessfulSyncAt,
builder: (column) => ColumnOrderings(column),
@@ -6457,6 +6859,11 @@ class $$RemoteSyncStateTableAnnotationComposer
builder: (column) => column,
);
GeneratedColumn<String> get settlementsCursor => $composableBuilder(
column: $table.settlementsCursor,
builder: (column) => column,
);
GeneratedColumn<DateTime> get lastSuccessfulSyncAt => $composableBuilder(
column: $table.lastSuccessfulSyncAt,
builder: (column) => column,
@@ -6512,6 +6919,7 @@ class $$RemoteSyncStateTableTableManager
({
Value<String> deviceId = const Value.absent(),
Value<String?> serverCursor = const Value.absent(),
Value<String?> settlementsCursor = const Value.absent(),
Value<DateTime?> lastSuccessfulSyncAt = const Value.absent(),
Value<DateTime?> lastAttemptedSyncAt = const Value.absent(),
Value<int> consecutiveFailureCount = const Value.absent(),
@@ -6519,6 +6927,7 @@ class $$RemoteSyncStateTableTableManager
}) => RemoteSyncStateCompanion(
deviceId: deviceId,
serverCursor: serverCursor,
settlementsCursor: settlementsCursor,
lastSuccessfulSyncAt: lastSuccessfulSyncAt,
lastAttemptedSyncAt: lastAttemptedSyncAt,
consecutiveFailureCount: consecutiveFailureCount,
@@ -6528,6 +6937,7 @@ class $$RemoteSyncStateTableTableManager
({
required String deviceId,
Value<String?> serverCursor = const Value.absent(),
Value<String?> settlementsCursor = const Value.absent(),
Value<DateTime?> lastSuccessfulSyncAt = const Value.absent(),
Value<DateTime?> lastAttemptedSyncAt = const Value.absent(),
Value<int> consecutiveFailureCount = const Value.absent(),
@@ -6535,6 +6945,7 @@ class $$RemoteSyncStateTableTableManager
}) => RemoteSyncStateCompanion.insert(
deviceId: deviceId,
serverCursor: serverCursor,
settlementsCursor: settlementsCursor,
lastSuccessfulSyncAt: lastSuccessfulSyncAt,
lastAttemptedSyncAt: lastAttemptedSyncAt,
consecutiveFailureCount: consecutiveFailureCount,
@@ -6936,6 +7347,208 @@ typedef $$TabEventsTableProcessedTableManager =
TabEventRow,
PrefetchHooks Function()
>;
typedef $$StockAdjustmentResultsTableCreateCompanionBuilder =
StockAdjustmentResultsCompanion Function({
required String id,
required String payload,
Value<bool> acknowledged,
required DateTime createdAt,
Value<int> rowid,
});
typedef $$StockAdjustmentResultsTableUpdateCompanionBuilder =
StockAdjustmentResultsCompanion Function({
Value<String> id,
Value<String> payload,
Value<bool> acknowledged,
Value<DateTime> createdAt,
Value<int> rowid,
});
class $$StockAdjustmentResultsTableFilterComposer
extends Composer<_$AppDatabase, $StockAdjustmentResultsTable> {
$$StockAdjustmentResultsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get payload => $composableBuilder(
column: $table.payload,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get acknowledged => $composableBuilder(
column: $table.acknowledged,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnFilters(column),
);
}
class $$StockAdjustmentResultsTableOrderingComposer
extends Composer<_$AppDatabase, $StockAdjustmentResultsTable> {
$$StockAdjustmentResultsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get payload => $composableBuilder(
column: $table.payload,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get acknowledged => $composableBuilder(
column: $table.acknowledged,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnOrderings(column),
);
}
class $$StockAdjustmentResultsTableAnnotationComposer
extends Composer<_$AppDatabase, $StockAdjustmentResultsTable> {
$$StockAdjustmentResultsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get payload =>
$composableBuilder(column: $table.payload, builder: (column) => column);
GeneratedColumn<bool> get acknowledged => $composableBuilder(
column: $table.acknowledged,
builder: (column) => column,
);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
}
class $$StockAdjustmentResultsTableTableManager
extends
RootTableManager<
_$AppDatabase,
$StockAdjustmentResultsTable,
StockAdjustmentResultRow,
$$StockAdjustmentResultsTableFilterComposer,
$$StockAdjustmentResultsTableOrderingComposer,
$$StockAdjustmentResultsTableAnnotationComposer,
$$StockAdjustmentResultsTableCreateCompanionBuilder,
$$StockAdjustmentResultsTableUpdateCompanionBuilder,
(
StockAdjustmentResultRow,
BaseReferences<
_$AppDatabase,
$StockAdjustmentResultsTable,
StockAdjustmentResultRow
>,
),
StockAdjustmentResultRow,
PrefetchHooks Function()
> {
$$StockAdjustmentResultsTableTableManager(
_$AppDatabase db,
$StockAdjustmentResultsTable table,
) : super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$StockAdjustmentResultsTableFilterComposer(
$db: db,
$table: table,
),
createOrderingComposer: () =>
$$StockAdjustmentResultsTableOrderingComposer(
$db: db,
$table: table,
),
createComputedFieldComposer: () =>
$$StockAdjustmentResultsTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({
Value<String> id = const Value.absent(),
Value<String> payload = const Value.absent(),
Value<bool> acknowledged = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => StockAdjustmentResultsCompanion(
id: id,
payload: payload,
acknowledged: acknowledged,
createdAt: createdAt,
rowid: rowid,
),
createCompanionCallback:
({
required String id,
required String payload,
Value<bool> acknowledged = const Value.absent(),
required DateTime createdAt,
Value<int> rowid = const Value.absent(),
}) => StockAdjustmentResultsCompanion.insert(
id: id,
payload: payload,
acknowledged: acknowledged,
createdAt: createdAt,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$StockAdjustmentResultsTableProcessedTableManager =
ProcessedTableManager<
_$AppDatabase,
$StockAdjustmentResultsTable,
StockAdjustmentResultRow,
$$StockAdjustmentResultsTableFilterComposer,
$$StockAdjustmentResultsTableOrderingComposer,
$$StockAdjustmentResultsTableAnnotationComposer,
$$StockAdjustmentResultsTableCreateCompanionBuilder,
$$StockAdjustmentResultsTableUpdateCompanionBuilder,
(
StockAdjustmentResultRow,
BaseReferences<
_$AppDatabase,
$StockAdjustmentResultsTable,
StockAdjustmentResultRow
>,
),
StockAdjustmentResultRow,
PrefetchHooks Function()
>;
class $AppDatabaseManager {
final _$AppDatabase _db;
@@ -6963,4 +7576,9 @@ class $AppDatabaseManager {
);
$$TabEventsTableTableManager get tabEvents =>
$$TabEventsTableTableManager(_db, _db.tabEvents);
$$StockAdjustmentResultsTableTableManager get stockAdjustmentResults =>
$$StockAdjustmentResultsTableTableManager(
_db,
_db.stockAdjustmentResults,
);
}
+116 -34
View File
@@ -1,14 +1,11 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import '../models/remote_command.dart';
import '../models/tab_event.dart';
import 'app_database.dart';
class InsufficientStockException implements Exception {
const InsufficientStockException();
@override
String toString() => 'Not enough stock';
}
class ProductNotFoundException implements Exception {
final String productId;
@@ -20,44 +17,129 @@ class ProductNotFoundException implements Exception {
/// Atomic stock updates shared by the product and tab services, so they can
/// run inside either one's transaction.
///
/// Stock may go below zero: the bar keeps serving when the count was off.
extension StockQueries on AppDatabase {
/// Products with infinite stock are only checked for existence.
Future<void> decreaseStock(String productId, int amount) async {
_validateStockAmount(amount);
final updatedRows =
await (update(products)..where(
(row) =>
row.id.equals(productId) &
row.infiniteStock.not() &
row.stockQuantity.isBiggerOrEqualValue(amount),
))
.write(
ProductsCompanion.custom(
stockQuantity: products.stockQuantity - Variable(amount),
),
);
if (updatedRows == 1) return;
final product = await _findProduct(productId);
if (!product.infiniteStock) throw const InsufficientStockException();
if (await changeTrackedStock(productId, -amount) == 0) {
await _findProduct(productId);
}
}
Future<void> increaseStock(String productId, int amount) async {
_validateStockAmount(amount);
final updatedRows =
await (update(products)..where(
(row) => row.id.equals(productId) & row.infiniteStock.not(),
))
.write(
ProductsCompanion.custom(
stockQuantity: products.stockQuantity + Variable(amount),
),
);
if (await changeTrackedStock(productId, amount) == 0) {
await _findProduct(productId);
}
}
if (updatedRows != 1) await _findProduct(productId);
/// Adds [delta] to the stock of [productId] when its stock is tracked.
/// Returns the number of products changed: 0 when the product has infinite
/// stock or no longer exists, which is not an error.
Future<int> changeTrackedStock(String productId, int delta) {
return (update(products)
..where((row) => row.id.equals(productId) & row.infiniteStock.not()))
.write(
ProductsCompanion.custom(
stockQuantity: products.stockQuantity + Variable(delta),
),
);
}
/// Queues a `stock_set` event, so a stock count or tracking change made on
/// this tablet reaches the server. Call it inside the transaction that
/// writes the stock.
Future<void> queueStockSet(TabEvent event) async {
await into(tabEvents).insert(
TabEventsCompanion.insert(
id: event.id,
payload: jsonEncode(event.payload),
createdAt: DateTime.now(),
),
);
}
/// Replaces the local stock of the products in [serverStock] with the
/// server's, then replays the outbox events the server hasn't applied yet
/// on top, the way the server will. The events the server acknowledged
/// must have left the outbox first: they are in [serverStock] already.
///
/// Products the server doesn't know keep their local stock.
Future<void> rebaseStock(List<ServerStock> serverStock) {
return transaction(() async {
final stock = {
for (final entry in serverStock)
entry.productId: (stock: entry.stock, countedAt: entry.countedAt),
};
// Read here, so changes made while the sync was in flight are included
final outbox =
await (select(tabEvents)..orderBy([
(row) => OrderingTerm.asc(row.createdAt),
(row) => OrderingTerm.asc(const CustomExpression<int>('rowid')),
]))
.get();
for (final row in outbox) {
final event = jsonDecode(row.payload) as Map<String, dynamic>;
final productId = event['productId'] as String?;
final current = stock[productId];
if (productId == null || current == null) continue;
final occurredAt = DateTime.tryParse(
event['occurredAt'] as String? ?? '',
);
if (occurredAt == null) continue;
final countedAt = current.countedAt;
switch (TabEventType.fromValue(event['type'] as String? ?? '')) {
// An older count than the server's is outdated
case TabEventType.stockSet:
if (countedAt != null && occurredAt.isBefore(countedAt)) continue;
stock[productId] = (
stock: event['quantity'] as int?,
countedAt: occurredAt,
);
// A sale before the count is already in the count
case TabEventType.lineAdded || TabEventType.lineRemoved
when current.stock != null &&
(countedAt == null || occurredAt.isAfter(countedAt)):
final quantity = event['quantity'] as int? ?? 0;
final sign = event['type'] == TabEventType.lineAdded.value ? -1 : 1;
stock[productId] = (
stock: current.stock! + sign * quantity,
countedAt: countedAt,
);
default:
continue;
}
}
for (final product in await select(products).get()) {
final rebased = stock[product.id];
if (rebased == null) continue;
final infinite = rebased.stock == null;
// An untracked product keeps its last count for when tracking resumes
final quantity = rebased.stock ?? product.stockQuantity;
if (product.infiniteStock == infinite &&
product.stockQuantity == quantity) {
continue;
}
await (update(
products,
)..where((row) => row.id.equals(product.id))).write(
ProductsCompanion(
stockQuantity: Value(quantity),
infiniteStock: Value(infinite),
),
);
}
});
}
Future<ProductRow> _findProduct(String productId) async {