Business-Monzo/monzo_emulator.pl
#!perl
use strict;
use warnings;
use Mojolicious::Lite;
use Mojo::JSON;
$ENV{MOJO_LOG_LEVEL} = 'debug';
plugin 'OAuth2::Server' => {
access_token_ttl => 21600,
authorize_route => '/',
access_token_route => '/oauth2/token',
jwt_secret => "ThisIsMyMonzoJWTSecret",
clients => {
test_client => {
client_secret => 'test_client_secret',
scopes => {},
},
},
users => {
leejo => "Weeeee",
}
};
group {
# all routes must have an access token
under '/' => sub {
my ( $c ) = @_;
return 1 if $c->oauth;
$c->render( status => 401, text => 'Unauthorized' );
return undef;
};
post '/ping/whoami' => sub {
my ( $c ) = @_;
$c->render( json => {
authenticated => Mojo::JSON::true,
client_id => $c->oauth->{client},
user_id => $c->oauth->{user_id},
} );
};
get '/accounts' => sub {
my ( $c ) = @_;
$c->render( json => {
accounts => [
{
id => "acc_00009237aqC8c5umZmrRdh",
description => "Peter Pan's Account",
created => "2015-11-13T12:17:42Z",
},
],
} );
};
get '/pots/listV1' => sub {
my ( $c ) = @_;
$c->render( json => {
pots => [
{
id => "pot_0000778xxfgh4iu8z83nWb",
name => "Peter Pan's Savings",
style => "piggy_bank",
balance => 133700,
currency => "GBP",
created => "2017-11-09T12:30:53.695Z",
updated => "2017-11-09T12:30:53.695Z",
deleted => Mojo::JSON::false
},
{
id => "pot_0000779h4iuxxnWfg8z83b",
name => "Tinker Bell's Break",
style => "beach_ball",
balance => 33700,
currency => "GBP",
created => "2017-11-09T12:30:53.695Z",
updated => "2017-11-09T12:30:53.695Z",
deleted => Mojo::JSON::false
},
],
} );
};
get '/balance' => sub {
my ( $c ) = @_;
my $account_id = $c->param( 'account_id' )
|| return $c->render( status => 400, text => "account_id required" );
$c->render( json => {
balance => 5000,
currency => "GBP",
spend_today => 0,
} );
};
get '/transactions/:transaction_id' => sub {
my ( $c ) = @_;
my $tid = $c->param( 'transaction_id' );
$c->render( json => {
"transaction" => _transactions( $c->param( 'expand[]' ) )->[1],
} );
};
patch '/transactions/:transaction_id' => sub {
my ( $c ) = @_;
my $tid = $c->param( 'transaction_id' );
my $metadata = _convert_map_params_to_hash( $c,'metadata' );
$c->render( json => {
"transaction" => _transactions( undef,$metadata )->[$tid - 1],
} );
};
get '/transactions' => sub {
my ( $c ) = @_;
my $account_id = $c->param( 'account_id' )
|| return $c->render( status => 400, text => "account_id required" );
$c->render( json => {
"transactions" => _transactions(),
} );
};
post '/feed' => sub {
my ( $c ) = @_;
my $account_id = $c->param( 'account_id' )
|| return $c->render( status => 400, text => "account_id required" );
my $type = $c->param( 'type' )
|| return $c->render( status => 400, text => "type required" );
my $params = _convert_map_params_to_hash( $c,'params' )
|| return $c->render( status => 400, text => "params required" );
foreach my $required_param ( qw/ title image_url / ) {
defined $params->{$required_param}
|| return $c->render( status => 400, text => "params[$required_param] required" );
}
# no-op at present
$c->render( json => {} );
};
post '/webhooks' => sub {
my ( $c ) = @_;
my $account_id = $c->param( 'account_id' )
|| return $c->render( status => 400, text => "account_id required" );
my $url = $c->param( 'url' )
|| return $c->render( status => 400, text => "url required" );
$c->render( json => {
webhook => {
account_id => $account_id,
url => $url,
id => time,
}
} );
};
get '/webhooks' => sub {
my ( $c ) = @_;
my $account_id = $c->param( 'account_id' )
|| return $c->render( status => 400, text => "account_id required" );
$c->render( json => {
webhooks => _webhooks( $account_id ),
} );
};
del '/webhooks/:webhook_id' => sub {
shift->render( json => {} );
};
post '/attachment/upload' => sub {
my ( $c ) = @_;
my $file_name = $c->param( 'file_name' )
|| return $c->render( status => 400, text => "file_name required" );
my $file_type = $c->param( 'file_type' )
|| return $c->render( status => 400, text => "file_type required" );
my $url = $c->req->url;
my $entity_id = "user_00009237hliZellUicKuG1";
$c->render( json => {
"file_url" => $url->base . "/file/$entity_id/LcCu4ogv1xW28OCcvOTL-foo.png",
"upload_url" => $url->base . "/upload/$entity_id/LcCu4ogv1xW28OCcvOTL-foo.png?AWSAccessKeyId=AKIAIR3IFH6UCTCXB5PQ\u0026Expires=1447353431\u0026Signature=k2QeDCCQQHaZeynzYKckejqXRGU%!D(MISSING)"
} );
};
post '/attachment/register' => sub {
my ( $c ) = @_;
my $external_id = $c->param( 'external_id' )
|| return $c->render( status => 400, text => "external_id required" );
my $file_url = $c->param( 'file_url' )
|| return $c->render( status => 400, text => "file_url required" );
my $file_type = $c->param( 'file_type' )
|| return $c->render( status => 400, text => "file_type required" );
$c->render( json => {
"attachment" => {
"id" => "attach_00009238aOAIvVqfb9LrZh",
"user_id" => "user_00009238aMBIIrS5Rdncq9",
"external_id" => $external_id,
"file_url" => $file_url,
"file_type" => $file_type,
"created" => "2015-11-12T18:37:02Z"
}
} );
};
post '/attachment/deregister' => sub {
my ( $c ) = @_;
my $id = $c->param( 'id' )
|| return $c->render( status => 400, text => "id required" );
$c->render( json => {} );
}
};
# convenience methods for file upload emulation, these endpoints
# do not exist in the Monzo API, they are here to fake uploads
get '/file/:entity_id/:file_name' => sub {
my ( $c ) = @_;
$c->render( text => "OK" );
};
post '/upload/:entity_id/:file_name' => sub {
my ( $c ) = @_;
$c->render( text => "OK" );
};
sub _convert_map_params_to_hash {
my ( $c,$prefix ) = @_;
# converts { params[foo] => bar } to { foo => bar }
# (this is horrible! why not just send JSON in the request body?)
my $params = $c->req->params->to_hash;
my %extracted_params =
map { my $v = $params->{$_}; s/^$prefix\[//g; chop; $_ => $v }
grep { /^$prefix\[[^\[]+\]$/ } keys %{ $params // {} };
return \%extracted_params;
}
sub _transactions {
my ( $expand,$metadata ) = @_;
$expand //= 'none';
$metadata //= {
stuff => 'yes',
more_stuff => 'yep',
};
my $attachment = {
"created" => "2016-04-23T12:46:41Z",
"external_id" => "tx_0000...",
"file_type" => "image/jpeg",
"file_url" => "https://...",
"id" => "attach_0000...",
"type" => "image/jpeg",
"url" => "https://...",
"user_id" => "user_0000..."
};
return [
{
"currency" => "GBP",
"merchant" => undef,
"counterparty" => {},
"local_amount" => 10000,
"id" => "1",
"created" => "2016-04-22T12:35:55.563Z",
"is_load" => Mojo::JSON::true,
"updated" => "2016-04-28T20:15:35.043Z",
"notes" => "",
"dedupe_id" => "5529515640563",
"description" => "Initial top up",
"attachments" => [],
"metadata" => $metadata,
"account_balance" => 10000,
"originator" => Mojo::JSON::false,
"scheme" => "gps_mastercard",
"amount" => 10000,
"settled" => "2016-04-22T12:35:55.563Z",
"account_id" => "acc_0000000000000000000001",
"local_currency" => "GBP",
"category" => "monzo"
},
{
"amount" => -1433,
"settled" => "2016-04-24T23:00:00.5Z",
"originator" => Mojo::JSON::false,
"account_balance" => 8565,
"scheme" => "gps_mastercard",
"category" => "cash",
"account_id" => "acc_0000000000000000000001",
"local_currency" => "CHF",
"id" => "2",
"created" => "2016-04-23T09:22:44.12Z",
"currency" => "GBP",
"counterparty" => {},
"merchant" => $expand eq 'merchant'
? _merchant()
: "merch_0000000000000000000001",
"local_amount" => -2000,
"description" => "BCV VILLARS/OLL. 2 Villars-sur-O CHE",
"dedupe_id" => "565726953219015109",
"attachments" => [ $attachment,$attachment ],
"metadata" => $metadata,
"is_load" => Mojo::JSON::false,
"updated" => "2016-04-25T09:50:56.605Z",
"notes" => ""
},
];
}
sub _merchant {
return {
"merchant" => {
"emoji" => "💵",
"updated" => "2016-04-23T09:22:45.005Z",
"online" => Mojo::JSON::false,
"category" => "cash",
"metadata" => {
"suggested_tags" => "#money #ATM #cashpoint #cash ",
"google_places_id" => "ChIJzXdG2omVjkcRqXc-9o1QxZI",
"foursquare_category" => "ATM",
"foursquare_id" => "",
"foursquare_website" => "",
"suggested_name" => "Caixa 24 Horas",
"foursquare_category_icon" => "https://ss3.4sqi.net/img/categories_v2/shops/financial_88.png",
"google_places_icon" => "https://maps.gstatic.com/mapfiles/place_api/icons/bank_dollar-71.png",
"google_places_name" => "UBS",
"created_for_merchant" => "merch_0000000000000000000001",
"created_for_transaction" => "1",
"twitter_id" => "",
"website" => ""
},
"disable_feedback" => Mojo::JSON::false,
"atm" => Mojo::JSON::true,
"logo" => "",
"group_id" => "grp_0000000000000000000001",
"id" => "merch_0000000000000000000001",
"name" => "ATM",
"created" => "2016-04-23T09:22:45.005Z",
"address" => {
"country" => "CHE",
"city" => "Villars-sur-o",
"longitude" => 7.076864,
"address" => "",
"region" => "",
"formatted" => "Villars-sur-o, 1884, Switzerland",
"latitude" => 46.3118929,
"approximate" => Mojo::JSON::false,
"zoom_level" => 17,
"short_formatted" => "Villars-sur-o, 1884, Switzerland",
"postcode" => "1884"
}
},
};
}
sub _webhooks {
my ( $account_id ) = @_;
$account_id //= "acc_000091yf79yMwNaZHhHGzp";
return [
{
"account_id" => $account_id,
"id" => "webhook_000091yhhOmrXQaVZ1Irsv",
"url" => "http://example.com/callback"
},
{
"account_id" => $account_id,
"id" => "webhook_000091yhhzvJSxLYGAceC9",
"url" => "http://example2.com/anothercallback"
},
];
}
app->start;
# vim: ts=4:sw=4:et