summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2018-06-23 19:59:49 -0400
committerJesse Luehrs <doy@tozt.net>2018-06-23 19:59:49 -0400
commit695895e26618cbfaa89183848cbeadd7ab9a0d5b (patch)
treed6ea2d037365c212b61581ff3734673f9e6a488f
parent84b1fc5ec0a38eef8c82e7e403da2378b37095a6 (diff)
downloadwww-ynab-695895e26618cbfaa89183848cbeadd7ab9a0d5b.tar.gz
www-ynab-695895e26618cbfaa89183848cbeadd7ab9a0d5b.zip
initial implementation
-rw-r--r--lib/WWW/YNAB.pm165
-rw-r--r--lib/WWW/YNAB/Account.pm72
-rw-r--r--lib/WWW/YNAB/Budget.pm263
-rw-r--r--lib/WWW/YNAB/Category.pm53
-rw-r--r--lib/WWW/YNAB/CategoryGroup.pm33
-rw-r--r--lib/WWW/YNAB/ModelHelpers.pm29
-rw-r--r--lib/WWW/YNAB/Month.pm33
-rw-r--r--lib/WWW/YNAB/Payee.pm28
-rw-r--r--lib/WWW/YNAB/SubTransaction.pm48
-rw-r--r--lib/WWW/YNAB/Transaction.pm95
-rw-r--r--lib/WWW/YNAB/UA.pm87
-rw-r--r--lib/WWW/YNAB/User.pm13
12 files changed, 919 insertions, 0 deletions
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;