feat(shopinbit): add ShopInBit API data models
What changed, and why it matters
This commit only adds new Dart data-model classes for a third-party shopping service called ShopInBit. It defines how the app represents things like addresses, tickets, payments, vouchers, and API errors. There is no executable network, authentication, or business logic here, and nothing in the commit changes existing behavior or fixes a known problem. It is a routine feature addition with no direct security relevance.
No security action required. Treat as normal feature code review; any future commits that wire these models into actual API calls, token handling, or webhook processing should be reviewed for TLS validation, authentication, input validation, and secret management.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff introduces 12 new files under lib/services/shopinbit/src/ containing pure data classes, enums, exception types, and a response wrapper. The code performs JSON deserialization with casts and default values, but there is no request construction, token storage, signature verification, webhook validation, or integration with wallet secrets. The endpoint URL is hard-coded as a constant only. No security bug or vulnerability is present in the added code itself.
Changed components
lib/services/shopinbit/src/api_exception.dartlib/services/shopinbit/src/api_response.dartlib/services/shopinbit/src/endpoints.dartlib/services/shopinbit/src/models/address.dartlib/services/shopinbit/src/models/auth_token.dartlib/services/shopinbit/src/models/car_research.dartlib/services/shopinbit/src/models/message.dartlib/services/shopinbit/src/models/models.dartlib/services/shopinbit/src/models/payment.dartlib/services/shopinbit/src/models/ticket.dartlib/services/shopinbit/src/models/voucher.dartlib/services/shopinbit/src/models/webhook_event.dartInspect captured patch +444 / −0
diff --git a/lib/services/shopinbit/src/api_exception.dart b/lib/services/shopinbit/src/api_exception.dart
new file mode 100644
index 0000000..6e35192
--- /dev/null
+++ b/lib/services/shopinbit/src/api_exception.dart
@@ -0,0 +1,24 @@
+class ApiException implements Exception {
+ final String message;
+ final int? statusCode;
+ final String? responseBody;
+
+ ApiException(this.message, {this.statusCode, this.responseBody});
+
+ factory ApiException.fromResponse(int statusCode, String body) {
+ return ApiException(
+ 'HTTP $statusCode',
+ statusCode: statusCode,
+ responseBody: body,
+ );
+ }
+
+ factory ApiException.network(Object error) {
+ return ApiException('Network error: $error');
+ }
+
+ @override
+ String toString() =>
+ 'ApiException: $message'
+ '${statusCode != null ? ' (status: $statusCode)' : ''}';
+}
diff --git a/lib/services/shopinbit/src/api_response.dart b/lib/services/shopinbit/src/api_response.dart
new file mode 100644
index 0000000..a1e9135
--- /dev/null
+++ b/lib/services/shopinbit/src/api_response.dart
@@ -0,0 +1,18 @@
+import 'api_exception.dart';
+
+class ApiResponse<T> {
+ final T? value;
+ final ApiException? exception;
+
+ ApiResponse({this.value, this.exception});
+
+ bool get hasError => exception != null;
+
+ T get valueOrThrow {
+ if (exception != null) throw exception!;
+ return value as T;
+ }
+
+ @override
+ String toString() => '{error: $exception, value: $value}';
+}
diff --git a/lib/services/shopinbit/src/endpoints.dart b/lib/services/shopinbit/src/endpoints.dart
new file mode 100644
index 0000000..00a18f6
--- /dev/null
+++ b/lib/services/shopinbit/src/endpoints.dart
@@ -0,0 +1,3 @@
+class Endpoints {
+ static const production = 'https://api.shopinbit.com';
+}
diff --git a/lib/services/shopinbit/src/models/address.dart b/lib/services/shopinbit/src/models/address.dart
new file mode 100644
index 0000000..5371a37
--- /dev/null
+++ b/lib/services/shopinbit/src/models/address.dart
@@ -0,0 +1,49 @@
+class Address {
+ final String? company;
+ final String? vat;
+ final String firstName;
+ final String lastName;
+ final String street;
+ final String zip;
+ final String city;
+ final String country;
+ final String? state;
+
+ Address({
+ this.company,
+ this.vat,
+ required this.firstName,
+ required this.lastName,
+ required this.street,
+ required this.zip,
+ required this.city,
+ required this.country,
+ this.state,
+ });
+
+ Map<String, dynamic> toJson() => {
+ 'company': company,
+ 'vat': vat,
+ 'firstName': firstName,
+ 'lastName': lastName,
+ 'street': street,
+ 'zip': zip,
+ 'city': city,
+ 'country': country,
+ 'state': state,
+ };
+
+ factory Address.fromJson(Map<String, dynamic> json) {
+ return Address(
+ company: json['company'] as String?,
+ vat: json['vat'] as String?,
+ firstName: json['firstName'] as String,
+ lastName: json['lastName'] as String,
+ street: json['street'] as String,
+ zip: json['zip'] as String,
+ city: json['city'] as String,
+ country: json['country'] as String,
+ state: json['state'] as String?,
+ );
+ }
+}
diff --git a/lib/services/shopinbit/src/models/auth_token.dart b/lib/services/shopinbit/src/models/auth_token.dart
new file mode 100644
index 0000000..af7816a
--- /dev/null
+++ b/lib/services/shopinbit/src/models/auth_token.dart
@@ -0,0 +1,25 @@
+class AuthToken {
+ final String accessToken;
+ final String tokenType;
+ final DateTime expiresAt;
+
+ AuthToken({
+ required this.accessToken,
+ required this.tokenType,
+ required this.expiresAt,
+ });
+
+ factory AuthToken.fromJson(Map<String, dynamic> json) {
+ return AuthToken(
+ accessToken: json['access_token'] as String,
+ tokenType: json['token_type'] as String,
+ // Tokens valid for 10 minutes per API docs.
+ expiresAt: DateTime.now().add(const Duration(minutes: 10)),
+ );
+ }
+
+ bool get isExpired => DateTime.now().isAfter(expiresAt);
+
+ bool get expiresSoon =>
+ DateTime.now().isAfter(expiresAt.subtract(const Duration(minutes: 1)));
+}
diff --git a/lib/services/shopinbit/src/models/car_research.dart b/lib/services/shopinbit/src/models/car_research.dart
new file mode 100644
index 0000000..ea1eceb
--- /dev/null
+++ b/lib/services/shopinbit/src/models/car_research.dart
@@ -0,0 +1,43 @@
+class CarResearchInvoice {
+ final String btcpayInvoice;
+ final DateTime expiresAt;
+ final Map<String, String> paymentLinks;
+
+ CarResearchInvoice({
+ required this.btcpayInvoice,
+ required this.expiresAt,
+ required this.paymentLinks,
+ });
+
+ factory CarResearchInvoice.fromJson(Map<String, dynamic> json) {
+ final linksRaw = json['payment_links'] as Map<String, dynamic>? ?? {};
+ return CarResearchInvoice(
+ btcpayInvoice: json['btcpay_invoice'] as String,
+ expiresAt: DateTime.parse(json['expires_at'] as String),
+ paymentLinks: linksRaw.map((k, v) => MapEntry(k, v as String)),
+ );
+ }
+}
+
+class CarResearchPaymentResult {
+ final String status;
+ final int ticketId;
+ final String ticketNumber;
+ final String externalCustomerKey;
+
+ CarResearchPaymentResult({
+ required this.status,
+ required this.ticketId,
+ required this.ticketNumber,
+ required this.externalCustomerKey,
+ });
+
+ factory CarResearchPaymentResult.fromJson(Map<String, dynamic> json) {
+ return CarResearchPaymentResult(
+ status: json['status'] as String,
+ ticketId: json['ticket_id'] as int,
+ ticketNumber: json['ticket_number'] as String,
+ externalCustomerKey: json['external_customer_key'] as String,
+ );
+ }
+}
diff --git a/lib/services/shopinbit/src/models/message.dart b/lib/services/shopinbit/src/models/message.dart
new file mode 100644
index 0000000..2048b72
--- /dev/null
+++ b/lib/services/shopinbit/src/models/message.dart
@@ -0,0 +1,19 @@
+class TicketMessage {
+ final DateTime timestamp;
+ final bool fromAgent;
+ final String content;
+
+ TicketMessage({
+ required this.timestamp,
+ required this.fromAgent,
+ required this.content,
+ });
+
+ factory TicketMessage.fromJson(Map<String, dynamic> json) {
+ return TicketMessage(
+ timestamp: DateTime.parse(json['timestamp'] as String),
+ fromAgent: json['from_agent'] as bool,
+ content: json['content'] as String,
+ );
+ }
+}
diff --git a/lib/services/shopinbit/src/models/models.dart b/lib/services/shopinbit/src/models/models.dart
new file mode 100644
index 0000000..7d4208c
--- /dev/null
+++ b/lib/services/shopinbit/src/models/models.dart
@@ -0,0 +1,8 @@
+export 'auth_token.dart';
+export 'ticket.dart';
+export 'message.dart';
+export 'address.dart';
+export 'payment.dart';
+export 'car_research.dart';
+export 'voucher.dart';
+export 'webhook_event.dart';
diff --git a/lib/services/shopinbit/src/models/payment.dart b/lib/services/shopinbit/src/models/payment.dart
new file mode 100644
index 0000000..bd0938d
--- /dev/null
+++ b/lib/services/shopinbit/src/models/payment.dart
@@ -0,0 +1,44 @@
+class PaymentInfo {
+ final String status;
+ final String customerPrice;
+ final String partnerPrice;
+ final int vatRate;
+ final String currency;
+ final DateTime? rateLockedUntil;
+ final Map<String, String> paymentLinks;
+ final String? due;
+
+ PaymentInfo({
+ required this.status,
+ required this.customerPrice,
+ required this.partnerPrice,
+ required this.vatRate,
+ required this.currency,
+ this.rateLockedUntil,
+ required this.paymentLinks,
+ this.due,
+ });
+
+ factory PaymentInfo.fromJson(Map<String, dynamic> json) {
+ final linksRaw = json['payment_links'] as Map<String, dynamic>? ?? {};
+ return PaymentInfo(
+ status: json['status'] as String,
+ customerPrice: (json['customer_price'] ?? '') as String,
+ partnerPrice: (json['partner_price'] ?? '') as String,
+ vatRate: _toInt(json['vat_rate']),
+ currency: (json['currency'] ?? 'EUR') as String,
+ rateLockedUntil: json['rate_locked_until'] != null
+ ? DateTime.parse(json['rate_locked_until'] as String)
+ : null,
+ paymentLinks: linksRaw.map((k, v) => MapEntry(k, v as String)),
+ due: json['due'] as String?,
+ );
+ }
+}
+
+int _toInt(dynamic v) {
+ if (v is int) return v;
+ if (v is String) return int.parse(v);
+ if (v is double) return v.toInt();
+ return 0;
+}
diff --git a/lib/services/shopinbit/src/models/ticket.dart b/lib/services/shopinbit/src/models/ticket.dart
new file mode 100644
index 0000000..eec6dd3
--- /dev/null
+++ b/lib/services/shopinbit/src/models/ticket.dart
@@ -0,0 +1,112 @@
+enum TicketState {
+ newTicket('NEW'),
+ checking('CHECKING'),
+ inProgress('IN PROGRESS'),
+ offerAvailable('OFFER AVAILABLE'),
+ clearing('CLEARING'),
+ shipped('SHIPPED'),
+ refunded('REFUNDED'),
+ fulfilled('FULFILLED'),
+ pendingClose('PENDING CLOSE'),
+ replyNeeded('REPLY NEEDED'),
+ closed('CLOSED'),
+ closedCancelled('CLOSED/CANCELLED'),
+ merged('MERGED');
+
+ final String value;
+ const TicketState(this.value);
+
+ static TicketState fromString(String s) {
+ return TicketState.values.firstWhere(
+ (e) => e.value == s,
+ orElse: () => TicketState.newTicket,
+ );
+ }
+}
+
+class TicketRef {
+ final int id;
+ final String number;
+
+ TicketRef({required this.id, required this.number});
+
+ factory TicketRef.fromJson(Map<String, dynamic> json) {
+ return TicketRef(id: _toInt(json['id']), number: json['number'].toString());
+ }
+}
+
+class TicketStatus {
+ final int ticketId;
+ final TicketState state;
+ final DateTime updatedAt;
+ final DateTime? lastAgentMessageAt;
+ final String? paymentInvoiceStatus;
+ final String? trackingLink;
+
+ TicketStatus({
+ required this.ticketId,
+ required this.state,
+ required this.updatedAt,
+ this.lastAgentMessageAt,
+ this.paymentInvoiceStatus,
+ this.trackingLink,
+ });
+
+ factory TicketStatus.fromJson(Map<String, dynamic> json) {
+ return TicketStatus(
+ ticketId: _toInt(json['ticket_id']),
+ state: TicketState.fromString(json['state'] as String),
+ updatedAt: DateTime.parse(json['updated_at'] as String),
+ lastAgentMessageAt: json['last_agent_message_at'] != null
+ ? DateTime.parse(json['last_agent_message_at'] as String)
+ : null,
+ paymentInvoiceStatus: json['payment_invoice_status'] as String?,
+ trackingLink: json['tracking_link'] as String?,
+ );
+ }
+}
+
+class TicketFull {
+ final int id;
+ final String number;
+ final String productName;
+ final String customerPrice;
+ final String partnerPrice;
+ final String partnerCommission;
+ final String netPurchasePrice;
+ final String netShippingCosts;
+ final int vatRate;
+
+ TicketFull({
+ required this.id,
+ required this.number,
+ required this.productName,
+ required this.customerPrice,
+ required this.partnerPrice,
+ required this.partnerCommission,
+ required this.netPurchasePrice,
+ required this.netShippingCosts,
+ required this.vatRate,
+ });
+
+ factory TicketFull.fromJson(Map<String, dynamic> json) {
+ return TicketFull(
+ id: _toInt(json['id']),
+ number: json['number'].toString(),
+ productName: (json['product_name'] ?? '').toString(),
+ customerPrice: (json['customer_price'] ?? '').toString(),
+ partnerPrice: (json['partner_price'] ?? '').toString(),
+ partnerCommission: (json['partner_commission'] ?? '').toString(),
+ netPurchasePrice: (json['net_purchase_price'] ?? '').toString(),
+ netShippingCosts: (json['net_shipping_costs'] ?? '').toString(),
+ vatRate: _toInt(json['vat_rate']),
+ );
+ }
+}
+
+int _toInt(dynamic v) {
+ if (v is int) return v;
+ if (v is String) return int.parse(v);
+ if (v is double) return v.toInt();
+ return 0;
+}
diff --git a/lib/services/shopinbit/src/models/voucher.dart b/lib/services/shopinbit/src/models/voucher.dart
new file mode 100644
index 0000000..97d048a
--- /dev/null
+++ b/lib/services/shopinbit/src/models/voucher.dart
@@ -0,0 +1,71 @@
+class VoucherInfo {
+ final bool valid;
+ final String? voucherCode;
+ final double? discountAmount;
+ final String? voucherType;
+ final int? priorityLevel;
+ final int? usageCount;
+ final int? maxUsage;
+ final bool? isUnlimited;
+ final int? remainingUses;
+ final String? validFrom;
+ final String? validUntil;
+ final String? error;
+
+ VoucherInfo({
+ required this.valid,
+ this.voucherCode,
+ this.discountAmount,
+ this.voucherType,
+ this.priorityLevel,
+ this.usageCount,
+ this.maxUsage,
+ this.isUnlimited,
+ this.remainingUses,
+ this.validFrom,
+ this.validUntil,
+ this.error,
+ });
+
+ factory VoucherInfo.fromJson(Map<String, dynamic> json) {
+ return VoucherInfo(
+ valid: json['valid'] as bool? ?? false,
+ voucherCode: json['voucher_code'] as String?,
+ discountAmount: (json['discount_amount'] as num?)?.toDouble(),
+ voucherType: json['voucher_type'] as String?,
+ priorityLevel: json['priority_level'] as int?,
+ usageCount: json['usage_count'] as int?,
+ maxUsage: json['max_usage'] as int?,
+ isUnlimited: json['is_unlimited'] as bool?,
+ remainingUses: json['remaining_uses'] as int?,
+ validFrom: json['valid_from'] as String?,
+ validUntil: json['valid_until'] as String?,
+ error: json['error'] as String?,
+ );
+ }
+}
+
+class VipRedemptionResult {
+ final int ticketId;
+ final String ticketNumber;
+ final String externalCustomerKey;
+ final String voucherCode;
+
+ VipRedemptionResult({
+ required this.ticketId,
+ required this.ticketNumber,
+ required this.externalCustomerKey,
+ required this.voucherCode,
+ });
+
+ factory VipRedemptionResult.fromJson(Map<String, dynamic> json) {
+ return VipRedemptionResult(
+ ticketId: json['ticket_id'] is int
+ ? json['ticket_id'] as int
+ : int.parse(json['ticket_id'].toString()),
+ ticketNumber: json['ticket_number'] as String,
+ externalCustomerKey: json['external_customer_key'] as String,
+ voucherCode: json['voucher_code'] as String,
+ );
+ }
+}
diff --git a/lib/services/shopinbit/src/models/webhook_event.dart b/lib/services/shopinbit/src/models/webhook_event.dart
new file mode 100644
index 0000000..7bf4169
--- /dev/null
+++ b/lib/services/shopinbit/src/models/webhook_event.dart
@@ -0,0 +1,28 @@
+enum WebhookEventType {
+ ticketStateChanged('ticket.state_changed'),
+ ticketMessageCreated('ticket.message_created');
+
+ final String value;
+ const WebhookEventType(this.value);
+
+ static WebhookEventType fromString(String s) {
+ return WebhookEventType.values.firstWhere(
+ (e) => e.value == s,
+ orElse: () => WebhookEventType.ticketStateChanged,
+ );
+ }
+}
+
+class WebhookEvent {
+ final WebhookEventType eventType;
+ final Map<String, dynamic> data;
+
+ WebhookEvent({required this.eventType, required this.data});
+
+ factory WebhookEvent.fromJson(Map<String, dynamic> json) {
+ return WebhookEvent(
+ eventType: WebhookEventType.fromString(json['event_type'] as String),
+ data: json['data'] as Map<String, dynamic>,
+ );
+ }
+}
Why this scored 15/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.