simplefin_dart 0.1.0 copy "simplefin_dart: ^0.1.0" to clipboard
simplefin_dart: ^0.1.0 copied to clipboard

Dart client for the SimpleFIN API bridge (https://www.simplefin.org/protocol.html)

example/main.dart

import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:csv/csv.dart';
import 'package:dotenv/dotenv.dart' as dotenv;
import 'package:simplefin_dart/simplefin_dart.dart';

Future<void> main(List<String> arguments) async {
  final parsers = _ParserBundle.build();
  final parser = parsers.root;

  ArgResults results;
  try {
    results = parser.parse(arguments);
  } on FormatException catch (error) {
    stderr.writeln(error.message);
    _printTopLevelUsage(parsers);
    exitCode = 64;
    return;
  }

  if (results['help'] as bool) {
    _printTopLevelUsage(parsers);
    return;
  }

  final command = results.command;
  if (command == null) {
    stderr.writeln('No command provided.');
    _printTopLevelUsage(parsers);
    exitCode = 64;
    return;
  }

  final scriptDir = File.fromUri(Platform.script).parent;

  switch (command.name) {
    case 'claim':
    case 'c':
      await _handleClaim(command, parsers, scriptDir);
    case 'info':
    case 'i':
      await _handleInfo(command, parsers);
    case 'accounts':
    case 'a':
      await _handleAccounts(command, parsers, scriptDir);
    case 'organizations':
    case 'o':
      await _handleOrganizations(command, parsers, scriptDir);
    case 'transactions':
    case 't':
      await _handleTransactions(command, parsers, scriptDir);
    default:
      stderr.writeln('Unknown command "${command.name}".');
      _printTopLevelUsage(parsers);
      exitCode = 64;
  }
}

Future<void> _handleClaim(
  ArgResults command,
  _ParserBundle parsers,
  Directory scriptDir,
) async {
  if (command['help'] as bool) {
    _printClaimUsage(parsers);
    return;
  }

  final rest = command.rest;
  if (rest.isEmpty) {
    stderr.writeln('Missing setup token.');
    _printClaimUsage(parsers);
    exitCode = 64;
    return;
  }

  final setupToken = rest.first;
  final bridgeRoot = command['bridge'] as String;
  final envPathDisplay = '${scriptDir.path}/.env';

  final client = SimplefinBridgeClient(root: Uri.parse(bridgeRoot));
  stdout.writeln('Claiming access URL from setup token...');
  try {
    final credentials = await client.claimAccessCredentials(setupToken);
    stdout
      ..writeln('SIMPLEFIN_ACCESS_URL=${credentials.accessUrl}')
      ..writeln(
        'Redirect or copy the line above into your .env file '
        '(e.g. >> $envPathDisplay).',
      );
  } on SimplefinException catch (error) {
    stderr.writeln('Failed to claim access URL: $error');
    exitCode = 1;
  } finally {
    client.close();
  }
}

Future<void> _handleInfo(ArgResults command, _ParserBundle parsers) async {
  if (command['help'] as bool) {
    _printInfoUsage(parsers);
    return;
  }

  final bridgeRoot = command['bridge'] as String;
  final client = SimplefinBridgeClient(root: Uri.parse(bridgeRoot));
  try {
    final info = await client.getInfo();
    if (info.versions.isEmpty) {
      stdout.writeln('No protocol versions reported by the bridge.');
    } else {
      stdout.writeln('Bridge supports the following protocol versions:');
      for (final version in info.versions) {
        stdout.writeln('- $version');
      }
    }
  } on SimplefinException catch (error) {
    stderr.writeln('Failed to fetch bridge info: $error');
    exitCode = 1;
  } finally {
    client.close();
  }
}

Future<void> _handleAccounts(
  ArgResults command,
  _ParserBundle parsers,
  Directory scriptDir,
) async {
  if (command['help'] as bool) {
    _printAccountsUsage(parsers);
    return;
  }

  _OutputFormat format;
  try {
    format = _parseOutputFormat(command['output-format'] as String?);
  } on FormatException catch (error) {
    stderr.writeln(error.message);
    exitCode = 64;
    return;
  }
  final accessUrlOverride = (command['url'] as String?)
      ?.trim()
      .maybeEmptyToNull();
  final envContext = _loadEnvContext(scriptDir);
  final accessUrl = accessUrlOverride ?? envContext.accessUrl;
  if (accessUrl == null) {
    stderr
      ..writeln('No access URL provided.')
      ..writeln(
        'Set SIMPLEFIN_ACCESS_URL in ${envContext.displayPath} or pass --url.',
      );
    exitCode = 64;
    return;
  }

  final orgIdFilter = (command['org-id'] as String?)
      ?.trim()
      .maybeEmptyToNull();
  final credentials = SimplefinAccessCredentials.parse(accessUrl);
  final client = SimplefinAccessClient(credentials: credentials);
  try {
    final accountSet = await client.getAccounts(
      balancesOnly: true,
    );

    _printBridgeErrors(accountSet.errors);

    final List<SimplefinAccount> accounts;
    if (orgIdFilter == null) {
      accounts = accountSet.accounts;
    } else {
      accounts = accountSet.accounts
          .where((account) {
            final orgId = account.org.id;
            return orgId != null && orgId == orgIdFilter;
          })
          .toList();
    }

    if (accounts.isEmpty) {
      stdout.writeln(
        orgIdFilter == null
            ? 'No accounts returned by the bridge.'
            : 'No accounts found for organization "$orgIdFilter".',
      );
      return;
    }

    switch (format) {
      case _OutputFormat.text:
        _printAccountsMarkdown(accounts);
      case _OutputFormat.json:
        stdout.writeln(
          _jsonEncoder.convert(
            accounts.map(_accountSummaryJson).toList(),
          ),
        );
      case _OutputFormat.csv:
        stdout.writeln(
          _accountsCsv(accounts, includeTransactions: false).trimRight(),
        );
    }
  } on SimplefinException catch (error) {
    stderr.writeln('Failed to fetch accounts: $error');
    exitCode = 1;
  } finally {
    client.close();
  }
}

Future<void> _handleOrganizations(
  ArgResults command,
  _ParserBundle parsers,
  Directory scriptDir,
) async {
  if (command['help'] as bool) {
    _printOrganizationsUsage(parsers);
    return;
  }

  _OutputFormat format;
  try {
    format = _parseOutputFormat(command['output-format'] as String?);
  } on FormatException catch (error) {
    stderr.writeln(error.message);
    exitCode = 64;
    return;
  }

  final accessUrlOverride = (command['url'] as String?)
      ?.trim()
      .maybeEmptyToNull();
  final envContext = _loadEnvContext(scriptDir);
  final accessUrl = accessUrlOverride ?? envContext.accessUrl;
  if (accessUrl == null) {
    stderr
      ..writeln('No access URL provided.')
      ..writeln(
        'Set SIMPLEFIN_ACCESS_URL in ${envContext.displayPath} or pass --url.',
      );
    exitCode = 64;
    return;
  }

  final credentials = SimplefinAccessCredentials.parse(accessUrl);
  final client = SimplefinAccessClient(credentials: credentials);
  try {
    final accountSet = await client.getAccounts(balancesOnly: true);

    _printBridgeErrors(accountSet.errors);

    final organizationsMap = <String, SimplefinOrganization>{};
    for (final account in accountSet.accounts) {
      final org = account.org;
      organizationsMap.putIfAbsent(_organizationKey(org), () => org);
    }

    if (organizationsMap.isEmpty) {
      stdout.writeln('No organizations returned by the bridge.');
      return;
    }

    final organizations = organizationsMap.values.toList()
      ..sort(
        (a, b) =>
            _organizationDisplayName(a).compareTo(_organizationDisplayName(b)),
      );

    switch (format) {
      case _OutputFormat.text:
        _printOrganizationsMarkdown(organizations);
      case _OutputFormat.json:
        stdout.writeln(
          _jsonEncoder.convert(
            organizations.length == 1
                ? _organizationJson(organizations.first)
                : organizations.map(_organizationJson).toList(),
          ),
        );
      case _OutputFormat.csv:
        stdout.writeln(_organizationsCsv(organizations).trimRight());
    }
  } on SimplefinException catch (error) {
    stderr.writeln('Failed to fetch organizations: $error');
    exitCode = 1;
  } finally {
    client.close();
  }
}

Future<void> _handleTransactions(
  ArgResults command,
  _ParserBundle parsers,
  Directory scriptDir,
) async {
  if (command['help'] as bool) {
    _printTransactionsUsage(parsers);
    return;
  }

  final accountId = (command['account'] as String?)?.trim().maybeEmptyToNull();

  _OutputFormat format;
  try {
    format = _parseOutputFormat(command['output-format'] as String?);
  } on FormatException catch (error) {
    stderr.writeln(error.message);
    exitCode = 64;
    return;
  }
  final accessUrlOverride = (command['url'] as String?)
      ?.trim()
      .maybeEmptyToNull();
  final envContext = _loadEnvContext(scriptDir);
  final accessUrl = accessUrlOverride ?? envContext.accessUrl;
  if (accessUrl == null) {
    stderr
      ..writeln('No access URL provided.')
      ..writeln(
        'Set SIMPLEFIN_ACCESS_URL in ${envContext.displayPath} or pass --url.',
      );
    exitCode = 64;
    return;
  }

  DateTime? startDate;
  DateTime? endDate;
  try {
    startDate = _parseDateOption(command['start-date'] as String?);
    endDate = _parseDateOption(command['end-date'] as String?);
  } on FormatException catch (error) {
    stderr.writeln(error.message);
    exitCode = 64;
    return;
  }

  final includePending = command['pending'] as bool;

  startDate ??= DateTime.now().toUtc().subtract(const Duration(days: 30));

  final credentials = SimplefinAccessCredentials.parse(accessUrl);
  final client = SimplefinAccessClient(credentials: credentials);
  try {
    final accountSet = await client.getAccounts(
      startDate: startDate,
      endDate: endDate,
      includePending: includePending,
      accountIds: accountId == null ? null : [accountId],
      balancesOnly: false,
    );

    _printBridgeErrors(accountSet.errors);

    if (accountSet.accounts.isEmpty) {
      switch (format) {
        case _OutputFormat.text:
          stdout.writeln(
            accountId == null
                ? 'No transactions returned by the bridge.'
                : 'No account returned for ID "$accountId".',
          );
          return;
        case _OutputFormat.json:
          stdout.writeln('[]');
          return;
        case _OutputFormat.csv:
          stdout.writeln(
            _transactionsCsv(const <SimplefinAccount>[]).trimRight(),
          );
          return;
      }
    }

    final accounts = accountSet.accounts;
    final transactions = accounts
        .expand(
          (account) => account.transactions.map(
            (transaction) => (account: account, transaction: transaction),
          ),
        )
        .toList();

    if (transactions.isEmpty) {
      switch (format) {
        case _OutputFormat.text:
          stdout.writeln('No transactions returned.');
          return;
        case _OutputFormat.json:
          stdout.writeln('[]');
          return;
        case _OutputFormat.csv:
          stdout.writeln(
            _transactionsCsv(const <SimplefinAccount>[]).trimRight(),
          );
          return;
      }
    }

    switch (format) {
      case _OutputFormat.text:
        _printTransactionsMarkdown(transactions);
      case _OutputFormat.json:
        stdout.writeln(
          _jsonEncoder.convert(
            transactions
                .map((pair) => _transactionJson(pair.transaction, pair.account))
                .toList(),
          ),
        );
      case _OutputFormat.csv:
        stdout.writeln(_transactionsCsv(accounts).trimRight());
    }
  } on SimplefinException catch (error) {
    stderr.writeln('Failed to fetch transactions: $error');
    exitCode = 1;
  } finally {
    client.close();
  }
}

void _printAccountsMarkdown(Iterable<SimplefinAccount> accounts) {
  for (final account in accounts) {
    stdout
      ..writeln('# Account: ${account.name}')
      ..writeln('- ID: ${account.id}')
      ..writeln('- Balance: ${account.balance}')
      ..writeln('- Currency: ${account.currency}')
      ..writeln(
        '- Balance Date: ${account.balanceDate.toUtc().toIso8601String()}',
      );
    if (account.availableBalance != null) {
      stdout.writeln('- Available Balance: ${account.availableBalance}');
    }
    stdout.writeln('- Organization ID: ${account.org.id ?? ''}');

    stdout.writeln();
  }
}

void _printOrganizationsMarkdown(List<SimplefinOrganization> organizations) {
  for (final org in organizations) {
    stdout
      ..writeln('# Organization: ${_organizationDisplayName(org)}')
      ..writeln('- ID: ${org.id ?? ''}')
      ..writeln('- Domain: ${org.domain ?? ''}')
      ..writeln('- URL: ${org.url?.toString() ?? ''}')
      ..writeln('- SimpleFIN URL: ${org.sfinUrl}')
      ..writeln();
  }
}

void _printTransactionsMarkdown(
  List<({SimplefinAccount account, SimplefinTransaction transaction})>
  transactions,
) {
  for (final pair in transactions) {
    final account = pair.account;
    final transaction = pair.transaction;
    stdout
      ..writeln('# Transaction: ${transaction.description}')
      ..writeln('- Account ID: ${account.id}')
      ..writeln('- Transaction ID: ${transaction.id}')
      ..writeln('- Amount: ${transaction.amount}')
      ..writeln('- Posted: ${transaction.posted.toUtc().toIso8601String()}');
    if (transaction.transactedAt != null) {
      final transactedAt = transaction.transactedAt!
          .toUtc()
          .toIso8601String();
      stdout.writeln('- Transacted At: $transactedAt');
    }
    stdout
      ..writeln('- Pending: ${transaction.pending ? 'yes' : 'no'}')
      ..writeln();
  }
}

String _accountsCsv(
  Iterable<SimplefinAccount> accounts, {
  required bool includeTransactions,
}) {
  final rows = <List<dynamic>>[
    if (includeTransactions)
      [
        'record_type',
        'account_id',
        'account_name',
        'currency',
        'balance',
        'available_balance',
        'balance_date',
        'org_id',
        'transaction_id',
        'posted',
        'amount',
        'description',
        'pending',
        'transacted_at',
      ]
    else
      [
        'account_id',
        'account_name',
        'currency',
        'balance',
        'available_balance',
        'balance_date',
        'org_id',
      ],
  ];

  for (final account in accounts) {
    if (includeTransactions) {
      rows.add([
        'account',
        account.id,
        account.name,
        account.currency,
        account.balance.toString(),
        account.availableBalance?.toString() ?? '',
        account.balanceDate.toUtc().toIso8601String(),
        account.org.id ?? '',
        '',
        '',
        '',
        '',
        '',
        '',
      ]);
    } else {
      rows.add([
        account.id,
        account.name,
        account.currency,
        account.balance.toString(),
        account.availableBalance?.toString() ?? '',
        account.balanceDate.toUtc().toIso8601String(),
        account.org.id ?? '',
      ]);
      continue;
    }

    for (final transaction in account.transactions) {
      rows.add([
        'transaction',
        account.id,
        account.name,
        account.currency,
        '',
        '',
        '',
        '',
        '',
        '',
        transaction.id,
        transaction.posted.toUtc().toIso8601String(),
        transaction.amount.toString(),
        transaction.description,
        transaction.pending,
        transaction.transactedAt?.toUtc().toIso8601String() ?? '',
      ]);
    }
  }

  return const ListToCsvConverter().convert(rows);
}

Map<String, dynamic> _accountSummaryJson(SimplefinAccount account) =>
    {
      'id': account.id,
      'name': account.name,
      'balance': account.balance.toString(),
      'available-balance':
          account.availableBalance?.toString() ?? account.balance.toString(),
      'currency': account.currency,
      'balance-date': account.balanceDate.toUtc().toIso8601String(),
      'org-id': account.org.id,
    }..removeWhere(
      (_, value) => value == null || (value is String && value.isEmpty),
    );

String _organizationKey(SimplefinOrganization organization) =>
    organization.id ?? organization.domain ?? organization.sfinUrl.toString();

String _organizationDisplayName(SimplefinOrganization organization) =>
    organization.name ??
    organization.domain ??
    organization.id ??
    organization.sfinUrl.toString();

Map<String, dynamic> _organizationJson(SimplefinOrganization organization) {
  final json = <String, dynamic>{
    'id': organization.id,
    'name': organization.name,
    'domain': organization.domain,
    'url': organization.url?.toString(),
    'sfin-url': organization.sfinUrl.toString(),
  };
  json.removeWhere(
    (_, value) => value == null || (value is String && value.isEmpty),
  );
  return json;
}

String _transactionsCsv(List<SimplefinAccount> accounts) {
  final rows = <List<dynamic>>[
    [
      'account_id',
      'transaction_id',
      'posted',
      'amount',
      'description',
      'pending',
      'transacted_at',
    ],
  ];

  for (final account in accounts) {
    for (final transaction in account.transactions) {
      rows.add([
        account.id,
        transaction.id,
        transaction.posted.toUtc().toIso8601String(),
        transaction.amount.toString(),
        transaction.description,
        transaction.pending,
        transaction.transactedAt?.toUtc().toIso8601String() ?? '',
      ]);
    }
  }

  return const ListToCsvConverter().convert(rows);
}

Map<String, dynamic> _transactionJson(
  SimplefinTransaction transaction,
  SimplefinAccount account,
) => {
  'account-id': account.id,
  'transaction-id': transaction.id,
  'posted': transaction.posted.toUtc().toIso8601String(),
  'amount': transaction.amount.toString(),
  'description': transaction.description,
  'pending': transaction.pending,
  if (transaction.transactedAt != null)
    'transacted-at': transaction.transactedAt!.toUtc().toIso8601String(),
};

String _organizationsCsv(List<SimplefinOrganization> organizations) {
  final rows = <List<dynamic>>[
    ['id', 'name', 'domain', 'url', 'sfin_url'],
  ];

  for (final org in organizations) {
    rows.add([
      org.id ?? '',
      org.name ?? '',
      org.domain ?? '',
      org.url?.toString() ?? '',
      org.sfinUrl.toString(),
    ]);
  }

  return const ListToCsvConverter().convert(rows);
}

void _printTopLevelUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart <command> [arguments]')
    ..writeln()
    ..writeln('Available commands:')
    ..writeln('  claim         Exchange a setup token for an access URL.')
    ..writeln(
      '  info          Query the SimpleFIN bridge for supported versions.',
    )
    ..writeln('  accounts      Retrieve account balances.')
    ..writeln('  organizations List organizations referenced by accounts.')
    ..writeln('  transactions  Retrieve transactions for a specific account.')
    ..writeln()
    ..writeln('Run `dart run example/main.dart <command> --help` for details.');
}

void _printClaimUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart claim [options] <setup_token>')
    ..writeln()
    ..writeln(parsers.claim.usage);
}

void _printInfoUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart info [options]')
    ..writeln()
    ..writeln(parsers.info.usage);
}

void _printAccountsUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart accounts [options]')
    ..writeln()
    ..writeln(parsers.accounts.usage);
}

void _printOrganizationsUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart organizations [options]')
    ..writeln()
    ..writeln(parsers.organizations.usage);
}

void _printTransactionsUsage(_ParserBundle parsers) {
  stdout
    ..writeln('Usage: dart run example/main.dart transactions [options]')
    ..writeln()
    ..writeln(parsers.transactions.usage);
}

void _printBridgeErrors(List<String> errors) {
  if (errors.isEmpty) {
    return;
  }
  stderr.writeln('Bridge reported the following messages:');
  for (final error in errors) {
    stderr.writeln('- $error');
  }
}

DateTime? _parseDateOption(String? rawValue) {
  final value = rawValue?.trim().maybeEmptyToNull();
  if (value == null) {
    return null;
  }
  final asInt = int.tryParse(value);
  if (asInt != null) {
    return DateTime.fromMillisecondsSinceEpoch(asInt * 1000, isUtc: true);
  }
  try {
    return DateTime.parse(value).toUtc();
  } on FormatException {
    throw FormatException(
      'Unable to parse date "$value". '
      'Use ISO-8601 (e.g. 2024-01-31T00:00:00Z) or epoch seconds.',
    );
  }
}

_OutputFormat _parseOutputFormat(String? rawValue) {
  final value = (rawValue ?? 'text').toLowerCase();
  switch (value) {
    case 'text':
      return _OutputFormat.text;
    case 'json':
      return _OutputFormat.json;
    case 'csv':
      return _OutputFormat.csv;
    default:
      throw FormatException('Unknown output format "$rawValue".');
  }
}

_EnvContext _loadEnvContext(Directory scriptDir) {
  final envFile = _locateEnvFile(scriptDir);
  if (envFile == null) {
    return _EnvContext(null, '${scriptDir.path}/.env', null);
  }
  final env = dotenv.DotEnv(includePlatformEnvironment: true);
  env.load(<String>[envFile.path]);
  final accessUrl = env['SIMPLEFIN_ACCESS_URL']?.trim().maybeEmptyToNull();
  return _EnvContext(envFile, envFile.path, accessUrl);
}

File? _locateEnvFile(Directory scriptDir) {
  final candidate = File('${scriptDir.path}/.env');
  if (candidate.existsSync()) {
    return candidate;
  }
  return null;
}

class _ParserBundle {
  _ParserBundle({
    required this.root,
    required this.claim,
    required this.info,
    required this.accounts,
    required this.organizations,
    required this.transactions,
  });

  factory _ParserBundle.build() {
    final root = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information.',
      );

    final claim = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information for claim.',
      )
      ..addOption(
        'bridge',
        abbr: 'b',
        help: 'SimpleFIN bridge root URL (default: $defaultBridgeRootUrl).',
        defaultsTo: defaultBridgeRootUrl,
      );
    root
      ..addCommand('claim', claim)
      ..addCommand('c', claim);

    final info = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information for info.',
      )
      ..addOption(
        'bridge',
        abbr: 'b',
        help: 'SimpleFIN bridge root URL (default: $defaultBridgeRootUrl).',
        defaultsTo: defaultBridgeRootUrl,
      );
    root
      ..addCommand('info', info)
      ..addCommand('i', info);

    final accounts = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information for accounts.',
      )
      ..addOption(
        'url',
        abbr: 'u',
        help:
            'SimpleFIN access URL (default: value from example/.env if present).',
      )
      ..addOption(
        'org-id',
        abbr: 'o',
        help: 'Restrict results to a specific organization ID.',
        valueHelp: 'ORG_ID',
      )
      ..addOption(
        'output-format',
        abbr: 'f',
        defaultsTo: 'text',
        allowed: const ['text', 'json', 'csv'],
        help: 'Output format: text, json, or csv (default: text).',
      );
    root
      ..addCommand('accounts', accounts)
      ..addCommand('a', accounts);

    final organizations = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information for organizations.',
      )
      ..addOption(
        'url',
        abbr: 'u',
        help:
            'SimpleFIN access URL (default: value from example/.env if present).',
      )
      ..addOption(
        'output-format',
        abbr: 'f',
        defaultsTo: 'text',
        allowed: const ['text', 'json', 'csv'],
        help: 'Output format: text, json, or csv (default: text).',
      );
    root
      ..addCommand('organizations', organizations)
      ..addCommand('o', organizations);

    final transactions = ArgParser()
      ..addFlag(
        'help',
        abbr: 'h',
        negatable: false,
        help: 'Show usage information for transactions.',
      )
      ..addOption(
        'url',
        abbr: 'u',
        help:
            'SimpleFIN access URL (default: value from example/.env if present).',
      )
      ..addOption(
        'start-date',
        abbr: 's',
        help:
            'Include transactions on/after this date '
            '(ISO-8601 or epoch seconds). '
            'Default: 30 days ago when omitted.',
      )
      ..addOption(
        'end-date',
        abbr: 'e',
        help:
            'Include transactions before this date '
            '(ISO-8601 or epoch seconds). '
            'Default: now.',
      )
      ..addFlag(
        'pending',
        negatable: false,
        help: 'Include pending transactions when supported (default: off).',
      )
      ..addOption(
        'account',
        abbr: 'a',
        help: 'Filter to a specific account ID.',
        valueHelp: 'ID',
      )
      ..addOption(
        'output-format',
        abbr: 'f',
        defaultsTo: 'text',
        allowed: const ['text', 'json', 'csv'],
        help: 'Output format: text, json, or csv (default: text).',
      );
    root
      ..addCommand('transactions', transactions)
      ..addCommand('t', transactions);

    return _ParserBundle(
      root: root,
      claim: claim,
      info: info,
      accounts: accounts,
      organizations: organizations,
      transactions: transactions,
    );
  }

  final ArgParser root;
  final ArgParser claim;
  final ArgParser info;
  final ArgParser accounts;
  final ArgParser organizations;
  final ArgParser transactions;
}

class _EnvContext {
  _EnvContext(this.file, this.displayPath, this.accessUrl);

  final File? file;
  final String displayPath;
  final String? accessUrl;
}

const JsonEncoder _jsonEncoder = JsonEncoder.withIndent('  ');

enum _OutputFormat { text, json, csv }

extension on String? {
  String? maybeEmptyToNull() {
    if (this == null) return null;
    final trimmed = this!.trim();
    return trimmed.isEmpty ? null : trimmed;
  }
}
1
likes
160
points
66
downloads

Publisher

unverified uploader

Weekly Downloads

Dart client for the SimpleFIN API bridge (https://www.simplefin.org/protocol.html)

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

args, csv, decimal, dotenv, http

More

Packages that depend on simplefin_dart