feat(shopinbit): add Isar fields for car research retry
What changed, and why it matters
This commit adds three new data fields to the app's local database to support a 'car research retry' feature for ShopInBit tickets. It also makes the app clear the ticket table if the database schema no longer matches, and makes ticket listing silently return an empty list on errors. There is no obvious security bug, but the broad error catching could hide unrelated database problems from users.
Treat as a routine feature commit. Review whether catch-all exception handlers should log or surface errors rather than silently discarding them, and confirm the schema-migration clear path cannot be triggered by an attacker through malformed local data.
Security signals we found
Broad catch-all exception handling around database queries and schema migration
Silent data loss on schema mismatch: ticket table is cleared without user confirmation
Silent empty result on query failure could mask corruption or attack-induced database errors
No input validation, authentication, or cryptography changes visible in the diff
Evidence from the diff
The patch extends the Isar collection schema for ShopInBitTicket with carResearchInvoiceId, feeTicketNumber, and needsCreateRequest, regenerates the generated Isar query code, and mirrors the fields in ShopInBitOrderModel. To handle the schema change on existing installs, MainDB.open() now attempts a dummy query and, on any exception, clears the shopInBitTickets collection. getShopInBitTickets() also now catches all exceptions and returns []. The change is defensive and recoverable because the comment states tickets are recoverable from the API.
Changed components
lib/db/isar/main_db.dartlib/models/isar/models/shopinbit_ticket.dartlib/models/isar/models/shopinbit_ticket.g.dartlib/models/shopinbit/shopinbit_order_model.dartInspect captured patch +243 / −1
diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart
index f3589d3..6c6a3e8 100644
--- a/lib/db/isar/main_db.dart
+++ b/lib/db/isar/main_db.dart
@@ -81,6 +81,14 @@ class MainDB {
name: "wallet_data",
maxSizeMiB: Platform.isWindows ? 1024 : 512,
);
+
+ // Clear on schema mismatch; tickets are recoverable from the API.
+ try {
+ isar.shopInBitTickets.where().findAllSync();
+ } catch (_) {
+ await isar.writeTxn(() async => isar.shopInBitTickets.clear());
+ }
+
return true;
}
@@ -650,7 +658,11 @@ class MainDB {
// ========== ShopInBit tickets ===============================================
List<ShopInBitTicket> getShopInBitTickets() {
- return isar.shopInBitTickets.where().sortByCreatedAtDesc().findAllSync();
+ try {
+ return isar.shopInBitTickets.where().sortByCreatedAtDesc().findAllSync();
+ } catch (_) {
+ return [];
+ }
}
Future<int> putShopInBitTicket(ShopInBitTicket ticket) async {
diff --git a/lib/models/isar/models/shopinbit_ticket.dart b/lib/models/isar/models/shopinbit_ticket.dart
index f3ffab4..4b571ec 100644
--- a/lib/models/isar/models/shopinbit_ticket.dart
+++ b/lib/models/isar/models/shopinbit_ticket.dart
@@ -29,6 +29,11 @@ class ShopInBitTicket {
late List<ShopInBitTicketMessage> messages;
late DateTime createdAt;
late int apiTicketId;
+
+ // Car research retry support
+ String? carResearchInvoiceId;
+ String? feeTicketNumber;
+ late bool needsCreateRequest;
}
@embedded
diff --git a/lib/models/isar/models/shopinbit_ticket.g.dart b/lib/models/isar/models/shopinbit_ticket.g.dart
index 14afa3d..bfc8bc9 100644
--- a/lib/models/isar/models/shopinbit_ticket.g.dart
+++ b/lib/models/isar/models/shopinbit_ticket.g.dart
@@ -106,6 +106,21 @@ const ShopInBitTicketSchema = CollectionSchema(
name: r'ticketId',
type: IsarType.string,
),
+ r'carResearchInvoiceId': PropertySchema(
+ id: 17,
+ name: r'carResearchInvoiceId',
+ type: IsarType.string,
+ ),
+ r'feeTicketNumber': PropertySchema(
+ id: 18,
+ name: r'feeTicketNumber',
+ type: IsarType.string,
+ ),
+ r'needsCreateRequest': PropertySchema(
+ id: 19,
+ name: r'needsCreateRequest',
+ type: IsarType.bool,
+ ),
},
estimateSize: _shopInBitTicketEstimateSize,
@@ -182,6 +197,18 @@ int _shopInBitTicketEstimateSize(
bytesCount += 3 + object.shippingPostalCode.length * 3;
bytesCount += 3 + object.shippingStreet.length * 3;
bytesCount += 3 + object.ticketId.length * 3;
+ {
+ final value = object.carResearchInvoiceId;
+ if (value != null) {
+ bytesCount += 3 + value.length * 3;
+ }
+ }
+ {
+ final value = object.feeTicketNumber;
+ if (value != null) {
+ bytesCount += 3 + value.length * 3;
+ }
+ }
return bytesCount;
}
@@ -213,6 +240,9 @@ void _shopInBitTicketSerialize(
writer.writeString(offsets[14], object.shippingStreet);
writer.writeByte(offsets[15], object.status.index);
writer.writeString(offsets[16], object.ticketId);
+ writer.writeString(offsets[17], object.carResearchInvoiceId);
+ writer.writeString(offsets[18], object.feeTicketNumber);
+ writer.writeBool(offsets[19], object.needsCreateRequest);
}
ShopInBitTicket _shopInBitTicketDeserialize(
@@ -251,6 +281,9 @@ ShopInBitTicket _shopInBitTicketDeserialize(
_ShopInBitTicketstatusValueEnumMap[reader.readByteOrNull(offsets[15])] ??
ShopInBitOrderStatus.pending;
object.ticketId = reader.readString(offsets[16]);
+ object.carResearchInvoiceId = reader.readStringOrNull(offsets[17]);
+ object.feeTicketNumber = reader.readStringOrNull(offsets[18]);
+ object.needsCreateRequest = reader.readBool(offsets[19]);
return object;
}
@@ -310,6 +343,12 @@ P _shopInBitTicketDeserializeProp<P>(
as P;
case 16:
return (reader.readString(offset)) as P;
+ case 17:
+ return (reader.readStringOrNull(offset)) as P;
+ case 18:
+ return (reader.readStringOrNull(offset)) as P;
+ case 19:
+ return (reader.readBool(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
@@ -2633,6 +2672,83 @@ extension ShopInBitTicketQueryFilter
);
});
}
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ carResearchInvoiceIdIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const FilterCondition.isNull(property: r'carResearchInvoiceId'),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ carResearchInvoiceIdIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const FilterCondition.isNotNull(property: r'carResearchInvoiceId'),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ carResearchInvoiceIdEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ FilterCondition.equalTo(
+ property: r'carResearchInvoiceId',
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ feeTicketNumberIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const FilterCondition.isNull(property: r'feeTicketNumber'),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ feeTicketNumberIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const FilterCondition.isNotNull(property: r'feeTicketNumber'),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ feeTicketNumberEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ FilterCondition.equalTo(
+ property: r'feeTicketNumber',
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterFilterCondition>
+ needsCreateRequestEqualTo(bool value) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ FilterCondition.equalTo(property: r'needsCreateRequest', value: value),
+ );
+ });
+ }
}
extension ShopInBitTicketQueryObject
@@ -2872,6 +2988,20 @@ extension ShopInBitTicketQuerySortBy
return query.addSortBy(r'ticketId', Sort.desc);
});
}
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterSortBy>
+ sortByNeedsCreateRequest() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'needsCreateRequest', Sort.asc);
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterSortBy>
+ sortByNeedsCreateRequestDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'needsCreateRequest', Sort.desc);
+ });
+ }
}
extension ShopInBitTicketQuerySortThenBy
@@ -3110,6 +3240,20 @@ extension ShopInBitTicketQuerySortThenBy
return query.addSortBy(r'ticketId', Sort.desc);
});
}
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterSortBy>
+ thenByNeedsCreateRequest() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'needsCreateRequest', Sort.asc);
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QAfterSortBy>
+ thenByNeedsCreateRequestDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'needsCreateRequest', Sort.desc);
+ });
+ }
}
extension ShopInBitTicketQueryWhereDistinct
@@ -3246,6 +3390,33 @@ extension ShopInBitTicketQueryWhereDistinct
return query.addDistinctBy(r'ticketId', caseSensitive: caseSensitive);
});
}
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QDistinct>
+ distinctByCarResearchInvoiceId({bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(
+ r'carResearchInvoiceId',
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QDistinct>
+ distinctByFeeTicketNumber({bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(
+ r'feeTicketNumber',
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, ShopInBitTicket, QDistinct>
+ distinctByNeedsCreateRequest() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(r'needsCreateRequest');
+ });
+ }
}
extension ShopInBitTicketQueryProperty
@@ -3372,6 +3543,27 @@ extension ShopInBitTicketQueryProperty
return query.addPropertyName(r'ticketId');
});
}
+
+ QueryBuilder<ShopInBitTicket, String?, QQueryOperations>
+ carResearchInvoiceIdProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'carResearchInvoiceId');
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, String?, QQueryOperations>
+ feeTicketNumberProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'feeTicketNumber');
+ });
+ }
+
+ QueryBuilder<ShopInBitTicket, bool, QQueryOperations>
+ needsCreateRequestProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'needsCreateRequest');
+ });
+ }
}
// **************************************************************************
diff --git a/lib/models/shopinbit/shopinbit_order_model.dart b/lib/models/shopinbit/shopinbit_order_model.dart
index 89c1787..1fe8c41 100644
--- a/lib/models/shopinbit/shopinbit_order_model.dart
+++ b/lib/models/shopinbit/shopinbit_order_model.dart
@@ -165,6 +165,33 @@ class ShopInBitOrderModel extends ChangeNotifier {
}
}
+ String? _carResearchInvoiceId;
+ String? get carResearchInvoiceId => _carResearchInvoiceId;
+ set carResearchInvoiceId(String? value) {
+ if (_carResearchInvoiceId != value) {
+ _carResearchInvoiceId = value;
+ notifyListeners();
+ }
+ }
+
+ String? _feeTicketNumber;
+ String? get feeTicketNumber => _feeTicketNumber;
+ set feeTicketNumber(String? value) {
+ if (_feeTicketNumber != value) {
+ _feeTicketNumber = value;
+ notifyListeners();
+ }
+ }
+
+ bool _needsCreateRequest = false;
+ bool get needsCreateRequest => _needsCreateRequest;
+ set needsCreateRequest(bool value) {
+ if (_needsCreateRequest != value) {
+ _needsCreateRequest = value;
+ notifyListeners();
+ }
+ }
+
List<ShopInBitMessage> _messages = [];
List<ShopInBitMessage> get messages => List.unmodifiable(_messages);
void addMessage(ShopInBitMessage message) {
@@ -193,6 +220,9 @@ class ShopInBitOrderModel extends ChangeNotifier {
..shippingCountry = _shippingCountry
..paymentMethod = _paymentMethod
..apiTicketId = _apiTicketId
+ ..carResearchInvoiceId = _carResearchInvoiceId
+ ..feeTicketNumber = _feeTicketNumber
+ ..needsCreateRequest = _needsCreateRequest
..messages = _messages
.map(
(m) => ShopInBitTicketMessage()
@@ -221,6 +251,9 @@ class ShopInBitOrderModel extends ChangeNotifier {
.._shippingPostalCode = ticket.shippingPostalCode
.._shippingCountry = ticket.shippingCountry
.._paymentMethod = ticket.paymentMethod
+ .._carResearchInvoiceId = ticket.carResearchInvoiceId
+ .._feeTicketNumber = ticket.feeTicketNumber
+ .._needsCreateRequest = ticket.needsCreateRequest
.._messages = ticket.messages
.map(
(m) => ShopInBitMessage(
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.