Files
sendico/frontend/pshared/lib/provider/payment/payments.dart

314 lines
7.8 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/service.dart';
import 'package:pshared/utils/exception.dart';
class PaymentsProvider with ChangeNotifier {
static const Duration _pendingRefreshInterval = Duration(seconds: 10);
OrganizationsProvider? _organizations;
String? _loadedOrganizationRef;
Resource<List<Payment>> _resource = Resource(data: []);
bool _isLoaded = false;
bool _isLoadingMore = false;
String? _nextCursor;
int? _limit;
String? _quotationRef;
DateTime? _createdFrom;
DateTime? _createdTo;
List<String>? _states;
int _opSeq = 0;
Timer? _pendingRefreshTimer;
bool _isPendingRefreshInFlight = false;
Resource<List<Payment>> get resource => _resource;
List<Payment> get payments => _resource.data ?? [];
bool get isLoading => _resource.isLoading;
Exception? get error => _resource.error;
bool get isReady =>
_isLoaded && !_resource.isLoading && _resource.error == null;
bool get isLoadingMore => _isLoadingMore;
String? get nextCursor => _nextCursor;
bool get canLoadMore => _nextCursor != null && _nextCursor!.isNotEmpty;
void update(OrganizationsProvider organizations) {
_organizations = organizations;
if (!organizations.isOrganizationSet) {
reset();
return;
}
final orgRef = organizations.current.id;
if (_loadedOrganizationRef != orgRef) {
_loadedOrganizationRef = orgRef;
unawaited(refresh());
}
}
Future<void> refresh({
int? limit,
String? quotationRef,
DateTime? createdFrom,
DateTime? createdTo,
List<String>? states,
}) async {
await _refresh(
limit: limit,
quotationRef: quotationRef,
createdFrom: createdFrom,
createdTo: createdTo,
states: states,
showLoading: true,
updateError: true,
);
}
Future<void> refreshSilently({
int? limit,
String? quotationRef,
DateTime? createdFrom,
DateTime? createdTo,
List<String>? states,
}) async {
await _refresh(
limit: limit,
quotationRef: quotationRef,
createdFrom: createdFrom,
createdTo: createdTo,
states: states,
showLoading: false,
updateError: false,
);
}
void mergePayments(List<Payment> incoming) {
if (incoming.isEmpty) return;
final existing = List<Payment>.from(_resource.data ?? const []);
final combined = <Payment>[...incoming, ...existing];
final seen = <String>{};
final merged = <Payment>[];
for (final payment in combined) {
final key = _paymentKey(payment);
if (key == null || key.isEmpty) {
merged.add(payment);
continue;
}
if (seen.contains(key)) continue;
seen.add(key);
merged.add(payment);
}
_applyResource(_resource.copyWith(data: merged), notify: true);
}
Future<void> _refresh({
int? limit,
String? quotationRef,
DateTime? createdFrom,
DateTime? createdTo,
List<String>? states,
required bool showLoading,
required bool updateError,
}) async {
final org = _organizations;
if (org == null || !org.isOrganizationSet) return;
_limit = limit;
_quotationRef = _normalize(quotationRef);
_createdFrom = createdFrom?.toUtc();
_createdTo = createdTo?.toUtc();
_states = _normalizeStates(states);
_nextCursor = null;
_isLoadingMore = false;
final seq = ++_opSeq;
if (showLoading) {
_applyResource(
_resource.copyWith(isLoading: true, error: null),
notify: true,
);
}
try {
final page = await PaymentService.listPage(
org.current.id,
limit: _limit,
cursor: null,
quotationRef: _quotationRef,
createdFrom: _createdFrom,
createdTo: _createdTo,
states: _states,
);
if (seq != _opSeq) return;
_isLoaded = true;
_nextCursor = _normalize(page.nextCursor);
_applyResource(
Resource(data: page.items, isLoading: false, error: null),
notify: true,
);
} catch (e) {
if (seq != _opSeq) return;
if (updateError) {
_applyResource(
_resource.copyWith(isLoading: false, error: toException(e)),
notify: true,
);
} else if (showLoading) {
_applyResource(_resource.copyWith(isLoading: false), notify: true);
}
}
}
Future<void> loadMore() async {
final org = _organizations;
if (org == null || !org.isOrganizationSet) return;
if (_isLoadingMore || _resource.isLoading) return;
final cursor = _normalize(_nextCursor);
if (cursor == null) return;
final seq = _opSeq;
_isLoadingMore = true;
_applyResource(_resource.copyWith(error: null), notify: false);
notifyListeners();
try {
final page = await PaymentService.listPage(
org.current.id,
limit: _limit,
cursor: cursor,
quotationRef: _quotationRef,
createdFrom: _createdFrom,
createdTo: _createdTo,
states: _states,
);
if (seq != _opSeq) return;
final combined = List<Payment>.from(payments)..addAll(page.items);
_nextCursor = _normalize(page.nextCursor);
_applyResource(
_resource.copyWith(data: combined, error: null),
notify: false,
);
} catch (e) {
if (seq != _opSeq) return;
_applyResource(_resource.copyWith(error: toException(e)), notify: false);
} finally {
if (seq == _opSeq) {
_isLoadingMore = false;
notifyListeners();
}
}
}
void reset() {
_opSeq++;
_isLoaded = false;
_isLoadingMore = false;
_nextCursor = null;
_limit = null;
_quotationRef = null;
_createdFrom = null;
_createdTo = null;
_states = null;
_resource = Resource(data: []);
_stopPendingRefreshTimer();
notifyListeners();
}
void _applyResource(
Resource<List<Payment>> newResource, {
required bool notify,
}) {
_resource = newResource;
_syncPendingRefresh();
if (notify) notifyListeners();
}
String? _normalize(String? value) {
final trimmed = value?.trim();
if (trimmed == null || trimmed.isEmpty) return null;
return trimmed;
}
String? _paymentKey(Payment payment) {
final ref = _normalize(payment.paymentRef);
if (ref != null) return ref;
return _normalize(payment.idempotencyKey);
}
List<String>? _normalizeStates(List<String>? states) {
if (states == null || states.isEmpty) return null;
final normalized = states
.map((state) => state.trim())
.where((state) => state.isNotEmpty)
.toList();
if (normalized.isEmpty) return null;
return normalized;
}
void _syncPendingRefresh() {
final hasPending = payments.any((payment) => payment.isPending);
if (!hasPending) {
_stopPendingRefreshTimer();
return;
}
_ensurePendingRefreshTimer();
}
void _ensurePendingRefreshTimer() {
if (_pendingRefreshTimer != null) return;
_pendingRefreshTimer = Timer.periodic(
_pendingRefreshInterval,
(_) => _refreshPending(),
);
_refreshPending();
}
Future<void> _refreshPending() async {
if (_isPendingRefreshInFlight) return;
if (isLoading || isLoadingMore) return;
_isPendingRefreshInFlight = true;
try {
await refreshSilently(
limit: _limit,
quotationRef: _quotationRef,
createdFrom: _createdFrom,
createdTo: _createdTo,
states: _states,
);
} finally {
_isPendingRefreshInFlight = false;
}
}
void _stopPendingRefreshTimer() {
_pendingRefreshTimer?.cancel();
_pendingRefreshTimer = null;
_isPendingRefreshInFlight = false;
}
@override
void dispose() {
_stopPendingRefreshTimer();
super.dispose();
}
}