From 695895e26618cbfaa89183848cbeadd7ab9a0d5b Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sat, 23 Jun 2018 19:59:49 -0400 Subject: initial implementation --- lib/WWW/YNAB.pm | 165 ++++++++++++++++++++++++++ lib/WWW/YNAB/Account.pm | 72 +++++++++++ lib/WWW/YNAB/Budget.pm | 263 +++++++++++++++++++++++++++++++++++++++++ lib/WWW/YNAB/Category.pm | 53 +++++++++ lib/WWW/YNAB/CategoryGroup.pm | 33 ++++++ lib/WWW/YNAB/ModelHelpers.pm | 29 +++++ lib/WWW/YNAB/Month.pm | 33 ++++++ lib/WWW/YNAB/Payee.pm | 28 +++++ lib/WWW/YNAB/SubTransaction.pm | 48 ++++++++ lib/WWW/YNAB/Transaction.pm | 95 +++++++++++++++ lib/WWW/YNAB/UA.pm | 87 ++++++++++++++ lib/WWW/YNAB/User.pm | 13 ++ 12 files changed, 919 insertions(+) create mode 100644 lib/WWW/YNAB/Account.pm create mode 100644 lib/WWW/YNAB/Budget.pm create mode 100644 lib/WWW/YNAB/Category.pm create mode 100644 lib/WWW/YNAB/CategoryGroup.pm create mode 100644 lib/WWW/YNAB/ModelHelpers.pm create mode 100644 lib/WWW/YNAB/Month.pm create mode 100644 lib/WWW/YNAB/Payee.pm create mode 100644 lib/WWW/YNAB/SubTransaction.pm create mode 100644 lib/WWW/YNAB/Transaction.pm create mode 100644 lib/WWW/YNAB/UA.pm create mode 100644 lib/WWW/YNAB/User.pm diff --git a/lib/WWW/YNAB.pm b/lib/WWW/YNAB.pm index e69de29..b44e004 100644 --- a/lib/WWW/YNAB.pm +++ b/lib/WWW/YNAB.pm @@ -0,0 +1,165 @@ +package WWW::YNAB; +use Moose; + +# ABSTRACT: Wrapper for the YNAB API + +use WWW::YNAB::Account; +use WWW::YNAB::Budget; +use WWW::YNAB::CategoryGroup; +use WWW::YNAB::Category; +use WWW::YNAB::Month; +use WWW::YNAB::Payee; +use WWW::YNAB::SubTransaction; +use WWW::YNAB::Transaction; +use WWW::YNAB::UA; +use WWW::YNAB::User; + +with 'WWW::YNAB::ModelHelpers'; + +has access_token => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has base_uri => ( + is => 'ro', + isa => 'Str', + default => 'https://api.youneedabudget.com/v1/', +); + +has ua => ( + is => 'ro', + isa => 'HTTP::Tiny', + lazy => 1, + default => sub { HTTP::Tiny->new }, +); + +has _ua => ( + is => 'ro', + isa => 'WWW::YNAB::UA', + lazy => 1, + default => sub { + my $self = shift; + WWW::YNAB::UA->new( + access_token => $self->access_token, + base_uri => $self->base_uri, + ua => $self->ua, + ) + }, +); + +sub user { + my $self = shift; + + my $data = $self->_ua->get('/user'); + my $user = $data->{data}{user}; + $self->model_from_data('WWW::YNAB::User', $user); +} + +sub budgets { + my $self = shift; + + my $data = $self->_ua->get('/budgets'); + map { + $self->model_from_data('WWW::YNAB::Budget', $_) + } @{ $data->{data}{budgets} }; +} + +sub budget { + my $self = shift; + my ($id, $server_knowledge) = @_; + + my $params; + if (defined $server_knowledge) { + $params = { + last_knowledge_of_server => $server_knowledge, + } + } + + my $data = $self->_ua->get("/budgets/$id", $params); + my $budget = $data->{data}{budget}; + my %budget = %$budget; + + my @accounts = map { + $self->model_from_data('WWW::YNAB::Account', $_) + } @{ $budget{accounts} }; + $budget{accounts} = \@accounts; + + my @payees = map { + $self->model_from_data('WWW::YNAB::Payee', $_) + } @{ $budget{payees} }; + $budget{payees} = \@payees; + + my @category_groups = map { + my %category_group = %$_; + $category_group{categories} = [ + map { + $self->model_from_data('WWW::YNAB::Category', $_) + } grep { + $_->{category_group_id} eq $category_group{id} + } @{ $budget{categories} } + ]; + $self->model_from_data('WWW::YNAB::CategoryGroup', \%category_group) + } @{ $budget{category_groups} }; + $budget{category_groups} = \@category_groups; + + my @months = map { + my %month = %$_; + $month{categories} = [ + map { + $self->model_from_data('WWW::YNAB::Category', $_) + } @{ $month{categories} } + ]; + $self->model_from_data('WWW::YNAB::Month', \%month) + } @{ $budget{months} }; + $budget{months} = \@months; + + my @transactions = map { + my %transaction = %$_; + $transaction{subtransactions} = [ + map { + $self->model_from_data('WWW::YNAB::SubTransaction', $_) + } grep { + $_->{transaction_id} eq $transaction{id} + } @{ $budget{subtransactions} } + ]; + $self->model_from_data('WWW::YNAB::Transaction', \%transaction) + } @{ $budget{transactions} }; + $budget{transactions} = \@transactions; + + $self->model_from_data( + 'WWW::YNAB::Budget', + \%budget, + $data->{data}{server_knowledge}, + ); +} + +sub rate_limit { + my $self = shift; + + $self->_ua->rate_limit +} + +sub knows_rate_limit { + my $self = shift; + + $self->_ua->knows_rate_limit +} + +sub total_rate_limit { + my $self = shift; + + $self->_ua->total_rate_limit +} + +sub knows_total_rate_limit { + my $self = shift; + + $self->_ua->knows_total_rate_limit +} + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/Account.pm b/lib/WWW/YNAB/Account.pm new file mode 100644 index 0000000..6ce05bc --- /dev/null +++ b/lib/WWW/YNAB/Account.pm @@ -0,0 +1,72 @@ +package WWW::YNAB::Account; +use Moose; + +use Moose::Util::TypeConstraints qw(enum); + +with 'WWW::YNAB::ModelHelpers'; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has name => ( + is => 'ro', + isa => 'Str', +); + +has type => ( + is => 'ro', + isa => enum([ + qw(checking savings cash creditCard lineOfCredit + otherAsset otherLiability payPal merchantAccount + investmentAccount mortgage) + ]), +); + +has on_budget => ( + is => 'ro', + isa => 'Bool', +); + +has closed => ( + is => 'ro', + isa => 'Bool', +); + +has note => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has balance => ( + is => 'ro', + isa => 'Int', +); + +has cleared_balance => ( + is => 'ro', + isa => 'Int', +); + +has uncleared_balance => ( + is => 'ro', + isa => 'Int', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +has _ua => ( + is => 'ro', + isa => 'WWW::YNAB::UA', + required => 1, +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/Budget.pm b/lib/WWW/YNAB/Budget.pm new file mode 100644 index 0000000..97d0d84 --- /dev/null +++ b/lib/WWW/YNAB/Budget.pm @@ -0,0 +1,263 @@ +package WWW::YNAB::Budget; +use Moose; + +use Carp; +use Moose::Util::TypeConstraints qw(find_type_constraint); + +with 'WWW::YNAB::ModelHelpers'; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has name => ( + is => 'ro', + isa => 'Str', +); + +has last_modified_on => ( + is => 'ro', + isa => 'Str', +); + +has first_month => ( + is => 'ro', + isa => 'Str', +); + +has last_month => ( + is => 'ro', + isa => 'Str', +); + +has server_knowledge => ( + is => 'ro', + isa => 'Int', +); + +has _accounts => ( + traits => ['Array'], + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Account]', + init_arg => 'accounts', + lazy => 1, + builder => '_build_accounts', + handles => { + accounts => 'elements', + } +); + +has _payees => ( + traits => ['Array'], + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Payee]', + init_arg => 'payees', + lazy => 1, + builder => '_build_payees', + handles => { + payees => 'elements', + } +); + +has _category_groups => ( + traits => ['Array'], + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::CategoryGroup]', + init_arg => 'category_groups', + lazy => 1, + builder => '_build_categories', + handles => { + categories => 'elements', + category_groups => 'elements', + } +); + +has _months => ( + traits => ['Array'], + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Month]', + init_arg => 'months', + lazy => 1, + builder => '_build_months', + handles => { + months => 'elements', + } +); + +has _transactions => ( + traits => ['Array'], + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Transaction]', + init_arg => 'transactions', + lazy => 1, + builder => '_build_transactions', + handles => { + transactions => 'elements', + } +); + +has _ua => ( + is => 'ro', + isa => 'WWW::YNAB::UA', + required => 1, +); + +sub _build_accounts { + my $self = shift; + + my $data = $self->_ua->get("/budgets/${\$self->id}/accounts"); + [ + map { + $self->model_from_data('WWW::YNAB::Account', $_) + } @{ $data->{data}{accounts} } + ] +} + +sub account { + my $self = shift; + my ($id) = @_; + + my $data = $self->_ua->get("/budgets/${\$self->id}/accounts/$id"); + my $account = $data->{data}{account}; + $self->model_from_data('WWW::YNAB::Account', $account); +} + +sub _build_categories { + my $self = shift; + + my $data = $self->_ua->get("/budgets/${\$self->id}/categories"); + [ + map { + my %category_group = %$_; + my @categories = map { + $self->model_from_data('WWW::YNAB::Category', $_) + } @{ $category_group{categories} }; + $category_group{categories} = \@categories; + $self->model_from_data('WWW::YNAB::CategoryGroup', \%category_group) + } @{ $data->{data}{category_groups} } + ] +} + +sub category { + my $self = shift; + my ($id) = @_; + + my $data = $self->_ua->get("/budgets/${\$self->id}/categories/$id"); + my $category = $data->{data}{category}; + $self->model_from_data('WWW::YNAB::Category', $category); +} + +sub _build_payees { + my $self = shift; + + my $data = $self->_ua->get("/budgets/${\$self->id}/payees"); + [ + map { + $self->model_from_data('WWW::YNAB::Payee', $_) + } @{ $data->{data}{payees} } + ] +} + +sub payee { + my $self = shift; + my ($id) = @_; + + my $data = $self->_ua->get("/budgets/${\$self->id}/payees/$id"); + my $payee = $data->{data}{payee}; + $self->model_from_data('WWW::YNAB::Payee', $payee); +} + +sub _build_months { + my $self = shift; + + my $data = $self->_ua->get("/budgets/${\$self->id}/months"); + [ + map { + $self->model_from_data('WWW::YNAB::Month', $_) + } @{ $data->{data}{months} } + ] +} + +sub month { + my $self = shift; + my ($id) = @_; + + my $data = $self->_ua->get("/budgets/${\$self->id}/months/$id"); + my $month = $data->{data}{month}; + my %month = %$month; + my @categories = map { + $self->model_from_data('WWW::YNAB::Category', $_) + } @{ $month{categories} }; + $month{categories} = \@categories; + $self->model_from_data('WWW::YNAB::Month', \%month); +} + +sub find_transactions { + my $self = shift; + my %query = @_; + + if ((grep { defined } @query{qw(account category payee type)}) > 1) { + croak "You can only query transactions by at most one of account, category, payee, or type"; + } + + my $path; + if ($query{account}) { + $path = "/budgets/${\$self->id}/accounts/$query{account}/transactions"; + } + elsif ($query{category}) { + $path = "/budgets/${\$self->id}/categories/$query{category}/transactions"; + } + elsif ($query{payee}) { + $path = "/budgets/${\$self->id}/payees/$query{payee}/transactions"; + } + else { + $path = "/budgets/${\$self->id}/transactions"; + } + + my $params; + if ($query{type}) { + $params ||= {}; + $params->{type} = $query{type}; + } + if ($query{since_date}) { + $params ||= {}; + $params->{since_date} = $query{since_date}; + } + + my $data = $self->_ua->get($path, $params); + map { + my %transaction = %$_; + my @subtransactions = map { + $self->model_from_data('WWW::YNAB::SubTransaction', $_) + } @{ $transaction{subtransactions} }; + $transaction{subtransactions} = \@subtransactions; + $self->model_from_data('WWW::YNAB::Transaction', \%transaction) + } @{ $data->{data}{transactions} }; +} + +sub _build_transactions { + my $self = shift; + + $self->find_transactions +} + +sub transaction { + my $self = shift; + my ($id) = @_; + + my $data = $self->_ua->get("/budgets/${\$self->id}/transactions/$id"); + my $transaction = $data->{data}{transaction}; + my %transaction = %$transaction; + my @subtransactions = map { + $self->model_from_data('WWW::YNAB::SubTransaction', $_) + } @{ $transaction{subtransactions} }; + $transaction{subtransactions} = \@subtransactions; + $self->model_from_data('WWW::YNAB::Transaction', \%transaction); +} + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/Category.pm b/lib/WWW/YNAB/Category.pm new file mode 100644 index 0000000..2fa99ef --- /dev/null +++ b/lib/WWW/YNAB/Category.pm @@ -0,0 +1,53 @@ +package WWW::YNAB::Category; +use Moose; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has category_group_id => ( + is => 'ro', + isa => 'Str', +); + +has name => ( + is => 'ro', + isa => 'Str', +); + +has hidden => ( + is => 'ro', + isa => 'Bool', +); + +has note => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has budgeted => ( + is => 'ro', + isa => 'Int', +); + +has activity => ( + is => 'ro', + isa => 'Int', +); + +has balance => ( + is => 'ro', + isa => 'Int', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/CategoryGroup.pm b/lib/WWW/YNAB/CategoryGroup.pm new file mode 100644 index 0000000..d266a3e --- /dev/null +++ b/lib/WWW/YNAB/CategoryGroup.pm @@ -0,0 +1,33 @@ +package WWW::YNAB::CategoryGroup; +use Moose; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has name => ( + is => 'ro', + isa => 'Str', +); + +has hidden => ( + is => 'ro', + isa => 'Bool', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +has categories => ( + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Category]', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/ModelHelpers.pm b/lib/WWW/YNAB/ModelHelpers.pm new file mode 100644 index 0000000..e61aa12 --- /dev/null +++ b/lib/WWW/YNAB/ModelHelpers.pm @@ -0,0 +1,29 @@ +package WWW::YNAB::ModelHelpers; +use Moose::Role; + +sub model_from_data { + my $self = shift; + my ($class, $data, $server_knowledge) = @_; + + my @init_args = grep { + $_ !~ /^_/ + } map { + $_->init_arg + } $class->meta->get_all_attributes; + + my %args = map { + $_ => $data->{$_} + } grep { + exists $data->{$_} + } @init_args; + + if (defined $server_knowledge) { + $args{server_knowledge} = $server_knowledge; + } + + $class->new(%args, _ua => $self->_ua); +} + +no Moose::Role; + +1; diff --git a/lib/WWW/YNAB/Month.pm b/lib/WWW/YNAB/Month.pm new file mode 100644 index 0000000..4e7412f --- /dev/null +++ b/lib/WWW/YNAB/Month.pm @@ -0,0 +1,33 @@ +package WWW::YNAB::Month; +use Moose; + +has month => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has note => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has to_be_budgeted => ( + is => 'ro', + isa => 'Maybe[Int]', +); + +has age_of_money => ( + is => 'ro', + isa => 'Maybe[Int]', +); + +has categories => ( + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::Category]', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/Payee.pm b/lib/WWW/YNAB/Payee.pm new file mode 100644 index 0000000..d25ef9e --- /dev/null +++ b/lib/WWW/YNAB/Payee.pm @@ -0,0 +1,28 @@ +package WWW::YNAB::Payee; +use Moose; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has name => ( + is => 'ro', + isa => 'Str', +); + +has transfer_account_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/SubTransaction.pm b/lib/WWW/YNAB/SubTransaction.pm new file mode 100644 index 0000000..9c761aa --- /dev/null +++ b/lib/WWW/YNAB/SubTransaction.pm @@ -0,0 +1,48 @@ +package WWW::YNAB::SubTransaction; +use Moose; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has transaction_id => ( + is => 'ro', + isa => 'Str', +); + +has amount => ( + is => 'ro', + isa => 'Int', +); + +has memo => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has payee_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has category_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has transfer_account_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/Transaction.pm b/lib/WWW/YNAB/Transaction.pm new file mode 100644 index 0000000..ab98642 --- /dev/null +++ b/lib/WWW/YNAB/Transaction.pm @@ -0,0 +1,95 @@ +package WWW::YNAB::Transaction; +use Moose; + +use Moose::Util::TypeConstraints qw(enum maybe_type); + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has date => ( + is => 'ro', + isa => 'Str', +); + +has amount => ( + is => 'ro', + isa => 'Int', +); + +has memo => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has cleared => ( + is => 'ro', + isa => enum([qw(cleared uncleared reconciled)]), +); + +has approved => ( + is => 'ro', + isa => 'Bool', +); + +has flag_color => ( + is => 'ro', + isa => maybe_type(enum([qw(red orange yellow green blue purple)])), +); + +has account_id => ( + is => 'ro', + isa => 'Str', +); + +has payee_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has category_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has transfer_account_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has import_id => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has deleted => ( + is => 'ro', + isa => 'Bool', +); + +has account_name => ( + is => 'ro', + isa => 'Str', +); + +has payee_name => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has category_name => ( + is => 'ro', + isa => 'Maybe[Str]', +); + +has subtransactions => ( + is => 'ro', + isa => 'ArrayRef[WWW::YNAB::SubTransaction]', +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/UA.pm b/lib/WWW/YNAB/UA.pm new file mode 100644 index 0000000..8e8c980 --- /dev/null +++ b/lib/WWW/YNAB/UA.pm @@ -0,0 +1,87 @@ +package WWW::YNAB::UA; +use Moose; + +use HTTP::Tiny; +use IO::Socket::SSL; # Necessary for https URLs on HTTP::Tiny. +use JSON::PP; +use Carp; + +has access_token => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has base_uri => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has ua => ( + is => 'ro', + isa => 'HTTP::Tiny', + required => 1, +); + +has rate_limit => ( + is => 'ro', + isa => 'Int', + writer => '_set_rate_limit', + predicate => 'knows_rate_limit', +); + +has total_rate_limit => ( + is => 'ro', + isa => 'Int', + writer => '_set_total_rate_limit', + predicate => 'knows_total_rate_limit', +); + +sub get { + my $self = shift; + $self->_request('get', @_); +} + +sub post { + my $self = shift; + $self->_request('post', @_); +} + +sub _request { + my $self = shift; + my ($method, $path, $params) = @_; + + warn "\U$method\E $path"; + + my $base = $self->base_uri; + $base =~ s{/$}{}; + $path =~ s{^/}{}; + my $uri = $self->base_uri . '/' . $path; + + my $response = $self->ua->$method( + $uri, + { + ($params ? (content => encode_json($params)) : ()), + headers => { + 'Content-Type' => 'application/json; charset=UTF-8', + 'X-Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $self->access_token, + }, + }, + ); + croak "Request for $uri failed ($response->{status}): $response->{content}" + unless $response->{success}; + + my $rate_limit = $response->{headers}{'x-rate-limit'}; + my ($current, $total) = split '/', $rate_limit; + $self->_set_rate_limit($current); + $self->_set_total_rate_limit($total); + + return decode_json($response->{content}); +} + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; diff --git a/lib/WWW/YNAB/User.pm b/lib/WWW/YNAB/User.pm new file mode 100644 index 0000000..2915fca --- /dev/null +++ b/lib/WWW/YNAB/User.pm @@ -0,0 +1,13 @@ +package WWW::YNAB::User; +use Moose; + +has id => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +__PACKAGE__->meta->make_immutable; +no Moose; + +1; -- cgit v1.2.3-54-g00ecf