enum DestinationType { crypto, card, russianBank } enum EligibilityState { eligible, temporarilyUnavailable, requiresAction, unsupported, } enum DestinationIssue { invalid, unavailable, uncertain, conflict, removed, expired, bankChanged, authorization, unsupported, duplicate, replacementRequired, } enum SaveOutcome { created, duplicate } enum BindingState { ready, expired, revoked, unavailable } class DestinationException implements Exception { const DestinationException(this.issue, {this.field}); final DestinationIssue issue; final String? field; } sealed class DestinationDetails { const DestinationDetails(); DestinationType get type; String get display; String get review; } class CryptoDestination extends DestinationDetails { const CryptoDestination({ required this.assetId, required this.networkId, required this.address, this.memo = '', required this.assetLabel, required this.networkLabel, }); final String assetId, networkId, address, memo, assetLabel, networkLabel; @override DestinationType get type => DestinationType.crypto; @override String get display => '$assetLabel · $networkLabel · ${address.length > 14 ? '${address.substring(0, 6)}…${address.substring(address.length - 6)}' : address}'; @override String get review => '$assetLabel · $networkLabel\n$address${memo.isEmpty ? '' : '\n$memo'}'; } /// Secure capture references are short lived, not PAN/CVV or destination IDs. class CardDestination extends DestinationDetails { const CardDestination({ this.capture = '', this.identity = '', required this.brand, required this.last4, required this.expiryMonth, required this.expiryYear, }); final String capture, identity, brand, last4; final int expiryMonth, expiryYear; @override DestinationType get type => DestinationType.card; @override String get display => '$brand · •••• $last4'; @override String get review => display; } class RussianBankDestination extends DestinationDetails { const RussianBankDestination({ required this.firstName, required this.lastName, this.patronymic = '', required this.bank, required this.account, }); final String firstName, lastName, patronymic, account; final Bank bank; String get holder => [firstName, lastName, patronymic].where((s) => s.isNotEmpty).join(' '); @override DestinationType get type => DestinationType.russianBank; @override String get display => '${bank.name} · •••• ${account.length < 4 ? account : account.substring(account.length - 4)}'; @override String get review => '$holder\n${bank.name}\n${bank.bik}\n$account\n${bank.correspondent}'; } class Bank { const Bank({ required this.bik, required this.name, required this.city, required this.correspondent, required this.revision, this.supported = true, }); final String bik, name, city, correspondent, revision; final bool supported; } class CryptoAsset { const CryptoAsset( this.id, this.label, this.networkId, this.networkLabel, { this.memoRequired = false, this.memoSupported = false, }); final String id, label, networkId, networkLabel; final bool memoRequired, memoSupported; } class ProviderBinding { const ProviderBinding({ required this.reference, required this.state, required this.reusable, this.recoverable = false, this.expiresAt, }); final String reference; final BindingState state; final bool reusable, recoverable; final DateTime? expiresAt; } class OwnershipAssessment { const OwnershipAssessment({ this.declaredOwn = true, this.verified = false, this.actionRequired = false, }); final bool declaredOwn, verified, actionRequired; } class SavedDestination { SavedDestination({ required this.id, required this.details, this.nickname = '', this.metadataRevision = 1, this.executionRevision = 1, this.removed = false, this.lastUsed, this.ownership = const OwnershipAssessment(), List bindings = const [], }) : bindings = List.unmodifiable(bindings); final String id, nickname; final DestinationDetails details; final int metadataRevision, executionRevision; final bool removed; final DateTime? lastUsed; final OwnershipAssessment ownership; final List bindings; String get label => nickname.isEmpty ? details.display : nickname; SavedReference get input => SavedReference(id, executionRevision); } sealed class DestinationInput { const DestinationInput(); } class SavedReference extends DestinationInput { const SavedReference(this.id, this.executionRevision); final String id; final int executionRevision; } class OneTimeDestination extends DestinationInput { const OneTimeDestination(this.details); final DestinationDetails details; } class PayoutContext { const PayoutContext(this.source, {this.amount}); final String source; final int? amount; } class PayoutEligibility { const PayoutEligibility(this.state, {this.reason, this.canSave = true}); final EligibilityState state; final DestinationIssue? reason; final bool canSave; bool get eligible => state == EligibilityState.eligible; } class DestinationValidation { const DestinationValidation(this.details); final DestinationDetails details; } class DestinationCapabilities { DestinationCapabilities({ List types = const [], List assets = const [], this.reusableCards = false, this.cardRecovery = false, this.simulated = false, }) : types = List.unmodifiable(types), assets = List.unmodifiable(assets); final List types; final List assets; final bool reusableCards, cardRecovery, simulated; } /// Outcome identity never changes when a record is later renamed or removed. class CreationResult { const CreationResult(this.outcome, this.destinationId); final SaveOutcome outcome; final String destinationId; } class DestinationLease { const DestinationLease({ required this.input, required this.snapshot, required this.context, required this.requirementsRevision, required this.expiresAt, }); final DestinationInput input; final DestinationDetails snapshot; final PayoutContext context; final int requirementsRevision; final DateTime expiresAt; } class DestinationAudit { const DestinationAudit( this.actor, this.reference, this.action, this.outcome, this.at, ); final String actor, reference, action, outcome; final DateTime at; }