Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
Cash Reconciliation
Rekonsiliasi kas dan laporan keuangan
๐ฏ Overview
Reconciliation adalah salah satu fitur utama dalam MStore Mobile yang menyediakan fungsionalitas untuk rekonsiliasi kas dan laporan keuangan.
๐ Features
- โ
Daily reconciliation
- โ
Cash counting
- โ
Discrepancy resolution
- โ
Financial reports
- โ
Audit trail
๐๏ธ Architecture
BLoC Implementation
BLoC: ReconciliationBloc
// Events
abstract class ReconciliationEvent extends Equatable {}
class LoadReconciliation extends ReconciliationEvent {}
class CreateReconciliation extends ReconciliationEvent {}
class UpdateReconciliation extends ReconciliationEvent {}
class DeleteReconciliation extends ReconciliationEvent {}
// States
abstract class ReconciliationState extends Equatable {}
class ReconciliationInitial extends ReconciliationState {}
class ReconciliationLoading extends ReconciliationState {}
class ReconciliationLoaded extends ReconciliationState {}
class ReconciliationError extends ReconciliationState {}
// BLoC
class ReconciliationBloc extends Bloc<ReconciliationEvent, ReconciliationState> {
final ReconciliationRepository _repository;
ReconciliationBloc({required ReconciliationRepository repository})
: _repository = repository,
super(ReconciliationInitial()) {
on<LoadReconciliation>(_onLoad);
on<CreateReconciliation>(_onCreate);
on<UpdateReconciliation>(_onUpdate);
on<DeleteReconciliation>(_onDelete);
}
Future<void> _onLoad(
LoadReconciliation event,
Emitter<ReconciliationState> emit,
) async {
emit(ReconciliationLoading());
final result = await _repository.getReconciliations();
result.fold(
(failure) => emit(ReconciliationError(failure.message)),
(data) => emit(ReconciliationLoaded(data)),
);
}
}
Repository Pattern
abstract class ReconciliationRepository {
Future<Either<Failure, List<Reconciliation>>> getReconciliations();
Future<Either<Failure, Reconciliation>> getReconciliationById(String id);
Future<Either<Failure, Reconciliation>> createReconciliation(Reconciliation data);
Future<Either<Failure, Reconciliation>> updateReconciliation(String id, Reconciliation data);
Future<Either<Failure, void>> deleteReconciliation(String id);
}
class ReconciliationRepositoryImpl implements ReconciliationRepository {
final ReconciliationApi _api;
final ReconciliationLocalRepository _localRepo;
@override
Future<Either<Failure, List<Reconciliation>>> getReconciliations() async {
try {
// Try local first (offline-first)
final local = await _localRepo.getReconciliations();
// Sync with API in background
final result = await _api.getReconciliations();
result.fold(
(failure) => null,
(data) => _localRepo.saveReconciliations(data),
);
return Right(local.isNotEmpty ? local : result.getOrElse(() => []));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
}
๐ก API Integration
Endpoints
/api/v1/reconciliations/*
Request/Response Examples
Get List
GET /api/v1/reconciliations/*
Authorization: Bearer {access_token}
Response:
{
"success": true,
"data": [
{
"id": "123",
"name": "Example",
"created_at": "2024-10-14T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 100
}
}
๐พ Local Database (Isar)
@collection
class ReconciliationLocal {
Id id = Isar.autoIncrement;
@Index()
String? reconciliationId;
String? name;
DateTime? createdAt;
DateTime? updatedAt;
DateTime? syncedAt;
bool? isSynced;
bool? isDeleted;
}
Queries
// Get all
final items = await isar.reconciliationLocals.where().findAll();
// Get by ID
final item = await isar.reconciliationLocals
.filter()
.reconciliationIdEqualTo(id)
.findFirst();
// Search
final results = await isar.reconciliationLocals
.filter()
.nameContains(query, caseSensitive: false)
.findAll();
// Get unsynced
final unsynced = await isar.reconciliationLocals
.filter()
.isSyncedEqualTo(false)
.findAll();
๐ Offline-First Strategy
Write Operations
- Save to local Isar immediately
- Show success to user
- Add to sync queue
- Background sync when online
- Update with server response
Read Operations
- Read from local Isar (fast)
- Show to user immediately
- Background fetch from API
- Update local cache if changed
- Notify UI if data updated
Conflict Resolution
- Strategy: Last-write-wins
- Timestamp: Server timestamp as source of truth
- Logging: All conflicts logged for audit
๐จ UI Components
Main Screen
class ReconciliationPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<ReconciliationBloc>()..add(LoadReconciliation()),
child: Scaffold(
appBar: AppBar(title: Text('Cash Reconciliation')),
body: BlocBuilder<ReconciliationBloc, ReconciliationState>(
builder: (context, state) {
if (state is ReconciliationLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is ReconciliationError) {
return ErrorWidget(message: state.message);
}
if (state is ReconciliationLoaded) {
return ReconciliationListView(items: state.items);
}
return SizedBox.shrink();
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _navigateToCreate(context),
child: Icon(Icons.add),
),
),
);
}
}
๐งช Testing
Unit Tests
void main() {
group('ReconciliationBloc', () {
late ReconciliationBloc bloc;
late MockReconciliationRepository mockRepository;
setUp(() {
mockRepository = MockReconciliationRepository();
bloc = ReconciliationBloc(repository: mockRepository);
});
tearDown(() {
bloc.close();
});
test('initial state is ReconciliationInitial', () {
expect(bloc.state, equals(ReconciliationInitial()));
});
blocTest<ReconciliationBloc, ReconciliationState>(
'emits [Loading, Loaded] when Load succeeds',
build: () {
when(() => mockRepository.getReconciliations()).thenAnswer(
(_) async => Right([Reconciliation(id: '1', name: 'Test')]),
);
return bloc;
},
act: (bloc) => bloc.add(LoadReconciliation()),
expect: () => [
ReconciliationLoading(),
isA<ReconciliationLoaded>(),
],
);
});
}
- Lazy Loading: Load data on demand
- Pagination: Implement pagination for large datasets
- Caching: Cache frequently accessed data
- Indexing: Use Isar indexes for fast queries
- Background Sync: Sync in background to avoid blocking UI
๐ Security
- Authorization: Check user permissions before operations
- Data Encryption: Sensitive data encrypted in Isar
- Input Validation: Validate all user inputs
- Audit Trail: Log all operations for audit
- Use Cupertino widgets
- Follow iOS HIG
- Handle safe area insets
Android
- Use Material widgets
- Follow Material Design
- Handle back button
Last Updated: October 14, 2024
Status: โ
Production Ready