Add saved payout destination UX and service contracts (#54)
This commit is contained in:
1 parent
a5689876ff
commit
24af794112
49 files changed
+4687
-62
No files matched your search
@@ -0,0 +1,255 @@
|
||||
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<ProviderBinding> 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<ProviderBinding> 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<DestinationType> types = const [],
|
||||
List<CryptoAsset> assets = const [],
|
||||
this.reusableCards = false,
|
||||
this.cardRecovery = false,
|
||||
this.simulated = false,
|
||||
}) : types = List.unmodifiable(types),
|
||||
assets = List.unmodifiable(assets);
|
||||
final List<DestinationType> types;
|
||||
final List<CryptoAsset> 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;
|
||||
}
|
||||
Reference in new issue
Block a user