Skip to main content

📋 Overview

Dokumentasi ini menjelaskan implementasi lengkap fitur offline-first untuk transaksi POS di MStore Mobile menggunakan Isar database dan batch sync ke backend.

🎯 Fitur Utama

Offline Transaction

Simpan transaksi lokal saat offline menggunakan Isar

Auto Batch Sync

Sinkronisasi otomatis saat koneksi kembali online

Conflict Detection

Deteksi duplikasi menggunakan offline_reference

Sync Status Tracking

Tracking status: pending, syncing, synced, failed

🏗️ Arsitektur

┌─────────────────────────────────────┐
│         Flutter App                 │
│  ┌───────────────────────────────┐  │
│  │   TransactionLocalService     │  │
│  │   (Create Offline)            │  │
│  └───────────┬───────────────────┘  │
│              │                       │
│  ┌───────────▼───────────────────┐  │
│  │   Isar DB (Local Storage)     │  │
│  │   TransactionLocalEntity      │  │
│  └───────────┬───────────────────┘  │
│              │                       │
│  ┌───────────▼───────────────────┐  │
│  │   BatchSyncService            │  │
│  │   (Auto Sync)                 │  │
│  └───────────┬───────────────────┘  │
└──────────────┼───────────────────────┘

               │ HTTP POST /batch-sync

┌──────────────▼───────────────────────┐
│         Backend API                  │
│  ┌───────────────────────────────┐  │
│  │   Check Duplicate             │  │
│  │   (offline_reference)         │  │
│  └───────────┬───────────────────┘  │
│              │                       │
│  ┌───────────▼───────────────────┐  │
│  │   Save to MySQL               │  │
│  └───────────────────────────────┘  │
└──────────────────────────────────────┘

📦 Komponen Utama

1. TransactionLocalEntity

Entity Isar untuk menyimpan transaksi offline:
@collection
@Name("transaction_local")
class TransactionLocalEntity {
  Id id = Isar.autoIncrement;
  
  @Index(unique: true)
  late String offlineId;          // UUID v4
  
  @Index(unique: true)
  late String offlineReference;   // DEVICE_ID-timestamp
  
  late String deviceId;
  late String branchCode;
  late int idUserApps;
  late double grandTotal;
  late String status;
  
  @Enumerated(EnumType.name)
  late SyncStatus syncStatus;     // pending, syncing, synced, failed
  
  int? serverId;                  // ID dari server setelah sync
  String? transactionCode;        // Kode transaksi dari server
  late DateTime createdAtDevice;
  late String itemsJson;          // Items dalam format JSON
}

enum SyncStatus {
  pending,
  syncing,
  synced,
  failed,
}
File: lib/database/isar/entities/transaction_local_entity.dart

2. TransactionLocalService

Service untuk CRUD transaksi offline:
@LazySingleton()
class TransactionLocalService {
  final IsarDb _isarDb;
  
  // Create offline transaction
  Future<TransactionLocalEntity> createTransaction({
    required String deviceId,
    required String branchCode,
    required double grandTotal,
    required String itemsJson,
    // ... other params
  }) async {
    final offlineId = Uuid().v4();
    final offlineReference = '$deviceId-${DateTime.now().millisecondsSinceEpoch}';
    
    final transaction = TransactionLocalEntity()
      ..offlineId = offlineId
      ..offlineReference = offlineReference
      ..syncStatus = SyncStatus.pending
      // ... set other fields
    
    await _isarDb.isar.writeTxn(() async {
      await _isarDb.isar.transactionLocalEntitys.put(transaction);
    });
    
    return transaction;
  }
  
  // Get pending transactions
  Future<List<TransactionLocalEntity>> getPendingTransactions() async {
    return await _isarDb.isar.transactionLocalEntitys
        .filter()
        .syncStatusEqualTo(SyncStatus.pending)
        .findAll();
  }
  
  // Update sync status
  Future<void> updateSyncStatus({
    required String offlineId,
    required SyncStatus status,
    int? serverId,
    String? transactionCode,
  }) async {
    // ... implementation
  }
}
File: lib/core/transaction/transaction_local_service.dart

3. BatchSyncService

Service untuk batch sync ke backend:
@LazySingleton()
class BatchSyncService {
  final TransactionApi _transactionApi;
  final TransactionLocalService _transactionLocalService;
  
  Future<int> syncPendingTransactions({
    required String deviceId,
  }) async {
    // 1. Get pending transactions
    final pending = await _transactionLocalService.getPendingTransactions();
    
    // 2. Update status to syncing
    for (final tx in pending) {
      await _transactionLocalService.updateSyncStatus(
        offlineId: tx.offlineId,
        status: SyncStatus.syncing,
      );
    }
    
    // 3. Prepare payload
    final payload = {
      'transactions': pending.map((tx) => {
        'offline_id': tx.offlineId,
        'offline_reference': tx.offlineReference,
        'device_id': tx.deviceId,
        'branch_code': tx.branchCode,
        'grand_total': tx.grandTotal,
        'items': jsonDecode(tx.itemsJson),
        // ... other fields
      }).toList(),
      'device_id': deviceId,
      'synced_at': DateTime.now().toIso8601String(),
    };
    
    // 4. Send to backend
    final response = await _transactionApi.batchSyncOfflineTransactions(payload);
    
    // 5. Update status based on results
    final results = response.data['data']['results'] as List;
    await _transactionLocalService.batchUpdateSyncStatus(results);
    
    return results.where((r) => r['success'] == true).length;
  }
}
File: lib/core/sync/batch_sync_service.dart

4. OfflineSyncService Integration

Integrasi auto-sync saat online:
class OfflineSyncService with WidgetsBindingObserver {
  final BatchSyncService _batchSyncService = getIt<BatchSyncService>();
  
  void initialize({
    Duration? transactionSyncTTL,
  }) {
    this.transactionSyncTTL = transactionSyncTTL ?? const Duration(minutes: 2);
    
    // Listen connectivity changes
    Connectivity().onConnectivityChanged.listen((results) async {
      final isOnline = results.any((r) => r != ConnectivityResult.none);
      if (isOnline) {
        await _triggerTransactionSyncIfStale(ttl: this.transactionSyncTTL);
      }
    });
  }
  
  Future<void> _triggerTransactionSyncIfStale({required Duration ttl}) async {
    // Check if stale based on TTL
    final deviceId = await DeviceInfoService().getOrCreateDeviceId();
    final syncedCount = await _batchSyncService.syncPendingTransactions(
      deviceId: deviceId,
    );
    
    AppLog.i('sync', 'Transactions synced', meta: {'count': syncedCount});
  }
}
File: lib/core/sync/offline_sync_service.dart

5. TransactionApi Endpoint

Endpoint untuk batch sync:
@RestApi()
abstract class TransactionApi {
  @POST('/api/v1/pos/offline/batch-sync')
  Future<HttpResponse<dynamic>> batchSyncOfflineTransactions(
    @Body() Map<String, dynamic> body
  );
}
File: lib/core/transaction/transaction_api.dart

🔧 Dependency Injection

Registrasi di NetworkModule:
@module
abstract class NetworkModule {
  @LazySingleton()
  TransactionLocalService transactionLocalService(IsarDb isarDb) {
    return TransactionLocalService(isarDb);
  }
  
  @LazySingleton()
  BatchSyncService batchSyncService(
    TransactionApi transactionApi,
    TransactionLocalService transactionLocalService,
  ) {
    return BatchSyncService(transactionApi, transactionLocalService);
  }
}

📝 Cara Penggunaan

1. Create Offline Transaction

final transactionLocalService = getIt<TransactionLocalService>();

final transaction = await transactionLocalService.createTransaction(
  deviceId: 'DEVICE001',
  branchCode: 'BRN-MST-OSK00001',
  idUserApps: 1,
  grandTotal: 33000.0,
  subtotal: 33000.0,
  discountTotal: 0.0,
  taxTotal: 0.0,
  status: 'paid',
  paymentStatus: 'paid',
  paymentMethod: 'CASH',
  itemsJson: jsonEncode([
    {
      'product_id': 1,
      'product_name': 'Kopi',
      'quantity': 2,
      'price': 15000,
      'subtotal': 30000,
    }
  ]),
);

print('Created offline: ${transaction.offlineReference}');

2. Manual Sync

final batchSyncService = getIt<BatchSyncService>();
final deviceId = await DeviceInfoService().getOrCreateDeviceId();

final syncedCount = await batchSyncService.syncPendingTransactions(
  deviceId: deviceId,
);

print('Synced $syncedCount transactions');

3. Check Sync Statistics

final stats = await transactionLocalService.getStatistics();

print('Pending: ${stats['pending']}');
print('Synced: ${stats['synced']}');
print('Failed: ${stats['failed']}');

4. Retry Failed Transactions

await transactionLocalService.retryFailedTransactions();
await batchSyncService.syncPendingTransactions(deviceId: deviceId);

🚀 Setup & Installation

1. Update IsarDb Schema

Tambahkan TransactionLocalEntity ke schema Isar:
// lib/database/isar/isar_db.dart
final db = await Isar.open([
  BranchEntitySchema,
  ProductEntitySchema,
  TransactionLocalEntitySchema,  // ← Add this
  // ... other schemas
]);

2. Run Build Runner

Generate file .g.dart:
cd /Users/faisalaffan/Documents/01_FAISAL/01_HUSTLE/19_MUSHOLA_STORE/mstore_mobile
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs

3. Initialize OfflineSyncService

Di main.dart:
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize DI
  await configureDependencies();
  
  // Initialize Isar
  await getIt<IsarDb>().open();
  
  // Initialize OfflineSyncService
  getIt<OfflineSyncService>().initialize(
    transactionSyncTTL: const Duration(minutes: 2),
  );
  
  runApp(MyApp());
}

🔍 Monitoring & Debugging

AppLog Integration

Semua operasi offline-first sudah terintegrasi dengan AppLog:
AppLog.i('transaction_local', 'Transaction created offline', meta: {
  'offlineId': offlineId,
  'grandTotal': grandTotal,
});

AppLog.i('batch_sync', 'Batch sync completed', meta: {
  'total': results.length,
  'success': successCount,
  'failed': failedCount,
});

Isar Inspector

Gunakan Isar Inspector untuk debug database lokal:
flutter pub run isar_inspector

⚠️ Best Practices

Penting: Selalu gunakan offline_reference yang unik untuk mencegah duplikasi transaksi saat sync.
Jalankan cleanup transaksi yang sudah sync secara berkala untuk menghemat storage:
await batchSyncService.cleanupOldSyncedTransactions(daysOld: 30);
TTL default untuk transaction sync adalah 2 menit. Sesuaikan berdasarkan kebutuhan bisnis.

🐛 Troubleshooting

Transaksi Tidak Tersinkronisasi

  1. Periksa koneksi internet
  2. Cek status sync: await transactionLocalService.getStatistics()
  3. Lihat log error di AppLog
  4. Retry manual: await transactionLocalService.retryFailedTransactions()

Duplikasi Transaksi

Backend sudah menangani duplikasi menggunakan offline_reference. Jika tetap terjadi duplikasi:
  1. Pastikan offline_reference unik (format: DEVICE_ID-timestamp)
  2. Periksa index unique di database backend
  3. Cek response dari batch-sync endpoint

Build Runner Error

# Clean dan rebuild
flutter clean
flutter pub get
flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputs

📚 Referensi

🎯 Next Steps