diff --git a/.travis.yml b/.travis.yml index 72c616e7d7..358f0cb669 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ matrix: services: - memcached + - redis-server notifications: email: false diff --git a/classes/Redis.php b/classes/Redis.php new file mode 100644 index 0000000000..3ffe1e7367 --- /dev/null +++ b/classes/Redis.php @@ -0,0 +1,17 @@ + REDIS_DB_HOST, + 'port' => REDIS_DB_PORT, + 'db' => REDIS_DB_NUMBER, + ]; + if (REDIS_DB_PASSWORD) { + $redis_args['password'] = REDIS_DB_PASSWORD; + } + parent::__construct($redis_args); + } +} diff --git a/classes/Stripe.php b/classes/Stripe.php new file mode 100644 index 0000000000..f031a8f341 --- /dev/null +++ b/classes/Stripe.php @@ -0,0 +1,32 @@ +init($arg); + $arg = $user; + } + + $this->db = new \ParlDB; + $this->redis = new Redis; + if (defined('TESTING')) { + $this->api = new TestStripe(""); + } else { + $this->api = new Stripe(STRIPE_SECRET_KEY); + } + + if (is_a($arg, 'User')) { + # User object + $this->user = $arg; + $this->redis_prefix = "user:{$this->user->user_id}:quota:" . REDIS_API_NAME; + $q = $this->db->query('SELECT * FROM api_subscription WHERE user_id = :user_id', [ + ':user_id' => $this->user->user_id()]); + if ($q->rows > 0) { + $id = $q->field(0, 'stripe_id'); + } else { + return; + } + } else { + # Assume Stripe ID string + $id = $arg; + $q = $this->db->query('SELECT * FROM api_subscription WHERE stripe_id = :stripe_id', [ + ':stripe_id' => $id]); + if ($q->rows > 0) { + $user = new \USER; + $user->init($q->field(0, 'user_id')); + $this->user = $user; + $this->redis_prefix = "user:{$this->user->user_id}:quota:" . REDIS_API_NAME; + } else { + return; + } + } + + try { + $this->stripe = $this->api->getSubscription([ + 'id' => $id, + 'expand' => ['customer.default_source'], + ]); + } catch (\Stripe\Error\InvalidRequest $e) { + $this->db->query('DELETE FROM api_subscription WHERE stripe_id = :stripe_id', [':stripe_id' => $id]); + $this->delete_from_redis(); + return; + } + + $this->has_payment_data = $this->stripe->customer->default_source; + + $data = $this->stripe; + if ($data->discount && $data->discount->coupon && $data->discount->coupon->percent_off) { + $this->actual_paid = add_vat(floor( + $data->plan->amount * (100 - $data->discount->coupon->percent_off) / 100)); + $data->plan->amount = add_vat($data->plan->amount); + } else { + $data->plan->amount = add_vat($data->plan->amount); + $this->actual_paid = $data->plan->amount; + } + + try { + $this->upcoming = $this->api->getUpcomingInvoice(["customer" => $this->stripe->customer->id]); + } catch (\Stripe\Error\Base $e) { + } + } + + private function update_subscription($form_data) { + if ($form_data['stripeToken']) { + $this->stripe->customer->source = $form_data['stripeToken']; + $this->stripe->customer->save(); + } + + # Update Stripe subscription + $this->stripe->plan = $form_data['plan']; + if ($form_data['coupon']) { + $this->stripe->coupon = $form_data['coupon']; + } elseif ($this->stripe->discount) { + $this->stripe->deleteDiscount(); + } + $this->stripe->metadata = $form_data['metadata']; + $this->stripe->cancel_at_period_end = false; # Needed in Stripe 2018-02-28 + $this->stripe->save(); + } + + private function add_subscription($form_data) { + # Create new Stripe customer and subscription + $cust_params = ['email' => $this->user->email()]; + if ($form_data['stripeToken']) { + $cust_params['source'] = $form_data['stripeToken']; + } + $obj = $this->api->createCustomer($cust_params); + $customer = $obj->id; + + if (!$form_data['stripeToken'] && !($form_data['plan'] == $this::$plans[0] && $form_data['coupon'] == 'charitable100')) { + exit(1); # Should never reach here! + } + + $obj = $this->api->createSubscription([ + 'tax_percent' => 20, + 'customer' => $customer, + 'plan' => $form_data['plan'], + 'coupon' => $form_data['coupon'], + 'metadata' => $form_data['metadata'] + ]); + $stripe_id = $obj->id; + + $this->db->query('INSERT INTO api_subscription (user_id, stripe_id) VALUES (:user_id, :stripe_id)', [ + ':user_id' => $this->user->user_id(), + ':stripe_id' => $stripe_id, + ]); + } + + private function getFields() { + $fields = ['plan', 'charitable_tick', 'charitable', 'charity_number', 'description', 'tandcs_tick', 'stripeToken']; + $this->form_data = []; + foreach ($fields as $field) { + $this->form_data[$field] = get_http_var($field); + } + } + + private function checkValidPlan() { + return ($this->form_data['plan'] && in_array($this->form_data['plan'], $this::$plans)); + } + + private function checkPaymentGivenIfNeeded() { + return ($this->has_payment_data || $this->form_data['stripeToken'] || ( + $this->form_data['plan'] == $this::$plans[0] + && in_array($this->form_data['charitable'], ['c', 'i']) + )); + } + + public function checkForErrors() { + $this->getFields(); + $form_data = &$this->form_data; + + $errors = []; + if ($form_data['charitable'] && !in_array($form_data['charitable'], ['c', 'i', 'o'])) { + $form_data['charitable'] = ''; + } + + if (!$this->checkValidPlan()) { + $errors[] = 'Please pick a plan'; + } + + if (!$this->checkPaymentGivenIfNeeded()) { + $errors[] = 'You need to submit payment'; + } + + if (!$this->stripe && !$form_data['tandcs_tick']) { + $errors[] = 'Please agree to the terms and conditions'; + } + + if (!$form_data['charitable_tick']) { + $form_data['charitable'] = ''; + $form_data['charity_number'] = ''; + $form_data['description'] = ''; + return $errors; + } + + if ($form_data['charitable'] == 'c' && !$form_data['charity_number']) { + $errors[] = 'Please provide your charity number'; + } + if ($form_data['charitable'] == 'i' && !$form_data['description']) { + $errors[] = 'Please provide details of your project'; + } + + return $errors; + } + + public function createOrUpdateFromForm() { + $form_data = $this->form_data; + + $form_data['coupon'] = null; + if (in_array($form_data['charitable'], ['c', 'i'])) { + $form_data['coupon'] = 'charitable50'; + if ($form_data['plan'] == $this::$plans[0]) { + $form_data['coupon'] = 'charitable100'; + } + } + + $form_data['metadata'] = [ + 'charitable' => $form_data['charitable'], + 'charity_number' => $form_data['charity_number'], + 'description' => $form_data['description'], + ]; + + if ($this->stripe) { + $this->update_subscription($form_data); + } else { + $this->add_subscription($form_data); + } + + $this->redis_update_max($form_data['plan']); + } + + public function redis_update_max($plan) { + preg_match('#^twfy-(\d+)k#', $plan, $m); + $max = $m[1] * 1000; + $this->redis->set("$this->redis_prefix:max", $max); + $this->redis->del("$this->redis_prefix:blocked"); + } + + public function redis_reset_quota() { + $count = $this->redis->getset("$this->redis_prefix:count", 0); + if ($count !== null) { + $this->redis->rpush("$this->redis_prefix:history", $count); + } + $this->redis->del("$this->redis_prefix:blocked"); + } + + public function delete_from_redis() { + $this->redis->del("$this->redis_prefix:max"); + $this->redis->del("$this->redis_prefix:count"); + $this->redis->del("$this->redis_prefix:blocked"); + } + + public function quota_status() { + return [ + 'count' => floor($this->redis->get("$this->redis_prefix:count")), + 'blocked' => floor($this->redis->get("$this->redis_prefix:blocked")), + 'quota' => floor($this->redis->get("$this->redis_prefix:max")), + 'history' => $this->redis->lrange("$this->redis_prefix:history", 0, -1), + ]; + } +} diff --git a/classes/TestStripe.php b/classes/TestStripe.php new file mode 100644 index 0000000000..c496c9288b --- /dev/null +++ b/classes/TestStripe.php @@ -0,0 +1,48 @@ + 'sub_123', + 'discount' => [ + 'coupon' => ['percent_off' => 100], + 'end' => null + ], + 'plan' => [ + 'amount' => '2000', + 'id' => 'twfy-1k', + 'name' => 'Some calls per month', + ], + 'cancel_at_period_end' => false, + 'created' => time(), + 'current_period_end' => time(), + 'customer' => [ + 'id' => 'cus_123', + 'account_balance' => 0, + 'default_source' => [], + ], + ], null); + } + return \Stripe\Util\Util::convertToStripeObject([], null); + } + + public function getUpcomingInvoice($args) { + return \Stripe\Util\Util::convertToStripeObject([], null); + } + + public function createCustomer($args) { + return \Stripe\Util\Util::convertToStripeObject([ + 'id' => 'cus_123', + 'email' => 'test@example.org', + ], null); + } + + public function createSubscription($args) { + return \Stripe\Util\Util::convertToStripeObject([ + 'id' => 'sub_123', + ], null); + } +} diff --git a/classes/Utility/Session.php b/classes/Utility/Session.php new file mode 100644 index 0000000000..229e970944 --- /dev/null +++ b/classes/Utility/Session.php @@ -0,0 +1,11 @@ +=5.4", "filp/whoops": "1.*", "ircmaxell/password-compat": "1.0.4", - "facebook/graph-sdk": "^5.6" + "facebook/graph-sdk": "^5.6", + "stripe/stripe-php": "^6.10", + "predis/predis": "^1.1", + "volnix/csrf": "^1.2" }, "require-dev": { "phpunit/phpunit": "4.8.*", diff --git a/composer.lock b/composer.lock index a74e17d9a4..02eb03fc90 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "7b50a3b9797a7057b4fbcf6789da790f", + "content-hash": "02be19e9d21c22e5f077c4a8da83a9bb", "packages": [ { "name": "facebook/graph-sdk", @@ -163,6 +163,151 @@ "password" ], "time": "2014-11-20T16:49:30+00:00" + }, + { + "name": "predis/predis", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2016-06-16T16:22:20+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "be46d55c4a67265d6fe3eaf2ff69f6cfffe0034d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/be46d55c4a67265d6fe3eaf2ff69f6cfffe0034d", + "reference": "be46d55c4a67265d6fe3eaf2ff69f6cfffe0034d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "~0.6.1", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "time": "2018-06-28T15:25:46+00:00" + }, + { + "name": "volnix/csrf", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/volnix/csrf.git", + "reference": "9d1abbf3ce9bda1b34d7f8f70fa0871bb7a5dd6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/volnix/csrf/zipball/9d1abbf3ce9bda1b34d7f8f70fa0871bb7a5dd6b", + "reference": "9d1abbf3ce9bda1b34d7f8f70fa0871bb7a5dd6b", + "shasum": "" + }, + "require": { + "php": ">= 5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Volnix\\CSRF\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nick Volgas", + "email": "nvolgas@ark.org" + } + ], + "description": "CSRF protection library that compares provided token to session token to ensure request validity.", + "homepage": "https://github.com/volnix/csrf", + "time": "2017-09-19T13:24:13+00:00" } ], "packages-dev": [ @@ -549,7 +694,8 @@ { "name": "Kevin Herrera", "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" + "homepage": "http://kevin.herrera.io/", + "role": "Developer" } ], "description": "A library for simplifying JSON linting and validation.", @@ -609,7 +755,8 @@ { "name": "Kevin Herrera", "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" + "homepage": "http://kevin.herrera.io/", + "role": "Developer" } ], "description": "A library for self-updating Phars.", @@ -881,7 +1028,8 @@ "authors": [ { "name": "Kevin Herrera", - "email": "me@kevingh.com" + "email": "me@kevingh.com", + "homepage": "http://www.kevingh.com/" } ], "description": "A parsing and comparison library for semantic versioning.", @@ -1915,7 +2063,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" } ], "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", diff --git a/conf/crontab-example b/conf/crontab-example index fb5d81c6cf..4afc1e0f57 100644 --- a/conf/crontab-example +++ b/conf/crontab-example @@ -12,9 +12,6 @@ MAILTO=cron-twfy@mysociety.org # And remove any things in the index that are no longer present 0 23 * * Sat twfy /data/vhost/www.theyworkforyou.com/theyworkforyou/search/index.pl check cronquiet -# every day, generate a summary of the API the day before -0 1 * * * twfy /data/vhost/www.theyworkforyou.com/theyworkforyou/scripts/daily-api-usage.php - # every week early Sunday grab Wikipedia titles update, only on live site 23 4 * * Sun twfy /data/vhost/www.theyworkforyou.com/theyworkforyou/scripts/wikipedia-update diff --git a/conf/general-example b/conf/general-example index 9b75892fa9..2b5a925b6e 100644 --- a/conf/general-example +++ b/conf/general-example @@ -172,3 +172,12 @@ define('OPTION_SURVEY_SECRET', ''); define('FACEBOOK_APP_ID', ''); define('FACEBOOK_APP_SECRET', ''); + +define('STRIPE_PUBLIC_KEY', ''); +define('STRIPE_SECRET_KEY', ''); +define('STRIPE_ENDPOINT_SECRET', ''); +define('REDIS_DB_HOST', 'localhost'); +define('REDIS_DB_PORT', '6379'); +define('REDIS_DB_NUMBER', '0'); +define('REDIS_DB_PASSWORD', ''); +define('REDIS_API_NAME', 'twfy'); diff --git a/db/0015-add-api-subscription.sql b/db/0015-add-api-subscription.sql new file mode 100644 index 0000000000..276ce994de --- /dev/null +++ b/db/0015-add-api-subscription.sql @@ -0,0 +1,5 @@ +CREATE TABLE `api_subscription` ( + `user_id` int(11) NOT NULL, + `stripe_id` varchar(255) NOT NULL, + PRIMARY KEY (`user_id`) +); diff --git a/db/schema.sql b/db/schema.sql index 6ac9725e19..de2933457b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -498,6 +498,12 @@ CREATE TABLE `api_stats` ( KEY `query_time` (`query_time`) ); +CREATE TABLE `api_subscription` ( + `user_id` int(11) NOT NULL, + `stripe_id` varchar(255) NOT NULL, + PRIMARY KEY (`user_id`) +); + CREATE TABLE `survey` ( `shown` int(11) NOT NULL default '0', `yes` int(11) NOT NULL default '0', diff --git a/scripts/daily-api-usage.php b/scripts/daily-api-usage.php deleted file mode 100755 index 908758857b..0000000000 --- a/scripts/daily-api-usage.php +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/php -q -query('SELECT - api_key.api_key, api_key.commercial, DATE(api_key.created) AS created, api_key.reason, - users.firstname, users.lastname, users.email, - count(distinct(ip_address)) as ip_addresses, count(*) AS count - FROM api_stats, api_key, users - WHERE api_stats.api_key = api_key.api_key - AND users.user_id = api_key.user_id - AND query_time >= subdate(current_date, 1) - AND query_time < current_date - GROUP BY api_key - ORDER BY count DESC -'); - -$keys = array(); -for ($i=0; $i<$q->rows(); $i++) { - $keys[] = $q->field($i, 'api_key'); -} -$keys = join("','", $keys); - -$q2 = $db->query("SELECT - api_key, DATE(MIN(query_time)) AS first_use, COUNT(*) AS count - FROM api_stats - WHERE api_key IN ('$keys') - GROUP BY api_key -"); -$summary = array(); -for ($i=0; $i<$q2->rows(); $i++) { - $row = $q2->row($i); - $summary[$row['api_key']] = array($row['first_use'], $row['count']); -} - -$out = ''; -for ($i=0; $i<$q->rows(); $i++) { - $row = $q->row($i); - $reason = preg_replace("/\r?\n/", ' ', $row['reason']); - $comm = $row['commercial']==1 ? ', commercial' : ''; - $ipa = $row['ip_addresses']!=1 ? 'es' : ''; - $hp = $row['count']!=1 ? 's' : ''; - list($first, $total) = $summary[$row['api_key']]; - $out .= "
$row[count] hit$hp, from $row[ip_addresses] IP address$ipa. $row[firstname] $row[lastname] <$row[email]>
- $error
- Welcome to TheyWorkForYou's API section. The API (Application Programming
+ Welcome to TheyWorkForYou’s API section. The API (Application Programming
Interface) is a way of querying our database for information.
- To use the API you need an API key, and you may need a
- license from us.
-
+ To use the API you need to get an API key.
+
The documentation for each individual API function is linked from this
page: you can read what each function does, and test it out, without
@@ -167,53 +163,36 @@ function api_front_page($error = '') {
- Important note: Politicians' contact details can't be
- obtained via this API. If that's what you're looking for, see
- EveryPolitician instead.
-
+ Important note: Politicians’ contact details can’t be
+ obtained via this API. If that’s what you’re looking for, see
+ EveryPolitician instead.
APIs and datasets for other mySociety services can be found on our
data portal, data.mysociety.org.
- Low volume, charitable use of the API is free. This means direct use by
- registered charities, or individuals pursuing a non-profit project on an
- unpaid basis, with a volume of up to 50,000 calls per year. All other use
- requires a licence, as a contribution towards the costs of providing this
- service. Please email us at =$em?>
- (mentioning TheyWorkForYou) if your usage is likely to fall into the
- non-free bracket: unlicensed users may be blocked without warning.
-
- £500 per year for up to 50,000 calls (free for charitable usage) In addition, we offer a 50% discount on the above rates for charitable usage. Parliamentary material (that's data returned from getDebates, getWrans, and
-getWMS) may be reused under the terms of the
-Open Parliament Licence.
-Our own data – lists of MPs, Lords, constituencies and so on – is
-available under the
-Creative Commons
-Attribution-ShareAlike license version 2.5.
+ In addition, we offer a 50% discount on the above rates for charitable usage.
+This means direct use by registered charities, or individuals pursuing a
+non-profit project on an unpaid basis. Please read our full terms of usage, including
+licence and attribution requirements. Please credit us by linking to TheyWorkForYou
-with wording such as "Data service provided by TheyWorkForYou" on the page
-where the data is used. This attribution is optional if you've paid for use of
-the service.
+
+ To use the API you need to get an API key.
+
All requests are made by GETting a particular URL with a number of
parameters. key is required; output is optional, and
@@ -224,7 +203,7 @@ function api_front_page($error = '') {
The current version of the API is 1.0.0. If we make changes to
- the API functions, we'll increase the version number and make it an
+ the API functions, we’ll increase the version number and make it an
argument so you can still use the old version.
If there's an error, either in the arguments provided or in trying to perform the request,
+ If there’s an error, either in the arguments provided or in trying to perform the request,
this is returned as a top-level error string, ie. in XML it returns
If anyone wishes to write bindings for the API in any language, please
-do so, let us know and we'll link to it here. You might want to
+do so, let us know and we’ll link to it here. You might want to
join our mailing list
to discuss things. TheyWorkForYou API calls require a key, so that we can monitor usage
-of the service, and provide usage stats to you. Please see the
-API overview for how to use your key. It looks like your usage may fall outside of our free-of-charge bracket: if that\'s the case, this key might get blocked, so we\'d advise you to email us at enquiries@mysociety.org to discuss licensing options. Thanks very much! Your subscription has been cancelled. Sign in (or sign up) to get an API key. If you’re sure you wish to cancel your subscription at the end of its
+ current month, please select the button below.
+ You have used up your quota for the = $subscription->stripe->plan->interval ?>.
+ Please upgrade
+ or contact us.
+
+ Your account is currently suspended. Please
+ contact us.
+ Your current plan is = $subscription->stripe->plan->nickname ?>. It costs you £= $subscription->actual_paid ?>/= $subscription->stripe->plan->interval ?>.
+ stripe->discount) { ?>
+ (£= $subscription->stripe->plan->amount ?>/= $subscription->stripe->plan->interval ?> with
+ = $subscription->stripe->discount->coupon->percent_off ?>% discount applied.)
+
+ Your discount will expire on = $subscription->stripe->discount->end ?>. Subscription created on = date('d/m/Y', $subscription->stripe->created) ?>;
+ stripe->cancel_at_period_end) { ?>
+ it expires on = date('d/m/Y', $subscription->stripe->current_period_end) ?>.
+ actual_paid > 0) { ?>
+ your next payment
+ upcoming) {
+ echo 'of £' . number_format($subscription->upcoming->amount_due / 100, 2);
+ }
+ ?> will be taken on = date('d/m/Y', $subscription->stripe->current_period_end) ?>.
+
+ your next invoice date is = date('d/m/Y', $subscription->stripe->current_period_end) ?>.
+
+
+
+
+ This = $subscription->stripe->plan->interval ?>:
+ = number_format($quota_status['count']) ?>
+ 0) { ?>
+ out of = number_format($quota_status['quota']) ?>
+
+ API calls
+ Payment details we hold: = $subscription->stripe->customer->default_source->brand ?>,
+ last four digits We do not currently hold any payment details for you. You are not currently subscribed to a plan. Your plan is curently set to expire on = date('d/m/Y', $stripe->current_period_end) ?>.
+If you update your plan below, it will be reactivated.
+ The TheyWorkForYou API (“API”, the “Service”) is operated by mySociety Ltd, company
+no. 5798215, of 483 Green Lanes, London N13 4BS (“mySociety”, “we” or “us). By using the Service or signing up for an account, you’re agreeing to these Terms,
+which form a legally binding agreement. Please read them carefully - they
+define the conditions under which you’re allowed to use the Service. In order to use the Service, you must be at least 18 years old and legally able to
+enter into contracts. If you use the Service on behalf of a company or organisation
+you warrant that you have the authority to accept these Terms on behalf of that
+company or organisation. mySociety reserves the right to update and change these Terms from time to
+time without notice and at its sole discretion. We reserve the right to block, without warning, usage which violates any
+of the following restrictions. The ‘call limit’ is the number of calls which can be made to the API in any
+one month. Without subscribing to a plan, you may call the API 10 times per day. Call limits are described on our main API
+page. When creating an account you can pick the most appropriate plan for
+your needs. In any one month, if you reach the call limit associated with your
+current plan, you will have to cease your usage until the next month, or
+upgrade to the next pricing level. Low-volume charitable use of the TheyWorkForYou API is free. This means direct use by
+registered charities (not paid contractors working on behalf of a charity) or
+by individuals pursuing a non-profit project on an unpaid basis, with a volume
+of up to 1,000 calls per month. Higher volume charitable users get a 50%
+discount off our standard pricing. You can request charitable status as part of the account creation process,
+and also at a later stage by updating your subscription (but we don’t provide
+the discount retroactively). You will be asked to provide charity registration details (if you represent
+a charity), or (if you're an individual) enough about you and your project to
+show us you meet the conditions above: you agree to provide true and complete
+information, and inform us if your circumstances change. You must provide a valid email address in order to complete the signup
+process. This email address will be used if we need to contact you about
+anything relating to your account or usage - please ensure you keep your
+contact information up to date. You’re responsible for all activity that takes place under your account, so
+you must keep your password secure. mySociety cannot and will not be liable for
+any loss or damage from your failure to maintain the security of your account
+and password. You should immediately notify us if you become aware of any
+unauthorised use of your account. You can terminate this agreement at any time and for any reason by ceasing
+use of the API and cancelling any subscription you have. You may also choose to
+ask us to close your account. mySociety may suspend your access to the API if: We also reserve the right to modify or terminate the API for any reason,
+without notice at any time. Our pricing is posted at /api/ and is subject to change upon 30
+days notice from us. Your plan is billed in advance on a monthly basis and is non-refundable. You
+must at all times ensure we are provided with valid credit or debit card
+information and authorise us to deduct the monthly charges against that
+card. If we’re unable to process your payment we’ll try to contact you by email.
+We reserve the right to limit or suspend your account until we can take
+payment. Nothing in these Terms is intended to transfer the ownership of any
+intellectual property from one party to the other. The API contains
+Parliamentary information licensed under the Open Parliament Licence;
+Ordnance Survey data © Crown copyright and database right 2018;
+NISRA data © Crown copyright;
+Royal Mail data © Royal Mail copyright and database right 2018;
+National Statistics data © Crown copyright and database right 2018.
+ The data is generated from sources outside mySociety and so mySociety
+cannot, and does not, warrant that it is accurate. mySociety accepts no
+liability for any errors. For the same reason, while we will take all
+reasonable steps to ensure that all data that is made available will continue
+to be available in the future, mySociety cannot, and does not, agree to keep
+the data up to date. We may, in our absolute discretion, add new data sources,
+data fields or other forms of information from time to time but we are under no
+obligation to do so. mySociety believes that the data may be used in accordance with these
+licences given by third parties: but mySociety cannot know whether those licences were validly given, and in
+consequence, gives no warranty as to the ownership of the data or the use to
+which it may be put. You must comply with the licence or licences relevant to the data you use,
+and you are responsible for satisfying yourself that any use for which you
+employ the data is lawful and does not infringe anyone’s intellectual property
+rights. Please attribute us by linking to TheyWorkForYou with wording such as “Data
+service provided by TheyWorkForYou” on the page where the data is used. You
+must also give any appropriate attributions required by the data licences. mySociety offers no guarantee as to the availability of the Service, and will not
+be liable if it is unavailable for any reason. If you need to contact us you can do so via our
+contact page. These Terms are subject to the law of England and Wales. They constitute all
+the terms and conditions agreed upon between you and mySociety and supersede
+any prior agreements, whether written or oral. You expressly understand and agree that to the maximum extent permitted by
+law mySociety excludes all liability and responsibility to you for any loss or
+damage resulting, directly or indirectly, from your use of or inability to use
+the API. In any event, our total liability for all claims made about the API in any
+month will be no more than what you paid us for the Service the month before. mySociety shall not be deemed to have waived its right to enforce any of its
+rights under this agreement merely because it has previously failed to enforce
+its rights under this agreement, or permitted you to breach any term of this
+agreement on one or more occasions. If a court, or other competent authority, finds that any term of this
+agreement is illegal or otherwise unenforceable, that term is to be treated as
+if it had been severed from this agreement. Its severance will be treated (as
+far as possible) as having no effect on the remainder of this agreement. You can find our Privacy Policy
+here. We do not use this data for any purpose other than to allow you
to log in to the site. If you register as a paying customer for the API
- we collect your name, email address and a postal address. We use
- these to issue invoices. If you register as a paying customer for the API,
+ your name, contact details, payment and security details are held
+ by the payment processor Stripe, whose usage terms and privacy
+ policy can be seen here: stripe.com/gb/privacy.
+ Your name, email address, the billing address of your card and the
+ last few digits of your card or bank account number are made
+ available to us along with your purchase history. We use these to
+ issue invoices and send you important information about changes or
+ updates to the service. If you contact us by email, your
message will be accessible to our small team of support staff. We
$row[api_key], created $row[created], first use $first$comm, total calls $total
-
$reason
-";
-}
-if (!$out) exit;
-
-$headers =
- "From: TheyWorkForYou <" . CONTACTEMAIL . ">\r\n" .
- "Content-Type: text/html; charset=utf-8\r\n" .
- "MIME-Version: 1.0\r\n" .
- "Content-Transfer-Encoding: 8bit\r\n";
-$subject = 'Daily TheyWorkForYou API usage';
-$to = join(chr(64), array('commercial', 'mysociety.org'));
-mail ($to, $subject, $out, $headers);
-
diff --git a/tests/AcceptApiTest.php b/tests/AcceptApiTest.php
index 4dfa84c40e..cf5850a863 100644
--- a/tests/AcceptApiTest.php
+++ b/tests/AcceptApiTest.php
@@ -11,7 +11,7 @@ class AcceptApiTest extends FetchPageTestCase
*/
public function getDataSet()
{
- return $this->createMySQLXMLDataSet(dirname(__FILE__).'/_fixtures/api.xml');
+ return $this->createMySQLXMLDataSet(dirname(__FILE__) . '/_fixtures/api.xml');
}
private function fetch_page($method, $vars = array())
@@ -20,6 +20,11 @@ private function fetch_page($method, $vars = array())
return $this->base_fetch_page($vars, 'api');
}
+ private function post_page($page, $vars = array())
+ {
+ return $this->base_post_page_user($vars, '1.fbb689a0c092f5534b929d302db2c8a9', 'api', "$page.php");
+ }
+
/**
* Ensure that not providing a key throws the right error
*/
@@ -109,4 +114,20 @@ public function testGetMlasLookup() {
'[{"member_id":"101","house":"3","given_name":"Test1","family_name":"Nimember","constituency":"Belfast West","party":"DUP","entered_house":"2000-01-01","left_house":"9999-12-31","entered_reason":"general_election","left_reason":"still_in_office","person_id":"101","title":"Mr","lastupdate":"2013-08-07 15:06:19","full_name":"Mr Test1 Nimember"}]');
}
+ public function testApiKeySignup() {
+ $page = $this->post_page('key');
+ $this->assertContains('Subscribe to a plan', $page);
+ $page = $this->post_page('update-plan', array(
+ 'plan' => 'twfy-1k',
+ 'charitable_tick' => 'on',
+ 'charitable' => 'c',
+ 'charity_number' => '123456',
+ 'tandcs_tick' => 'on',
+ ));
+ $this->assertEquals('Location: /api/key?updated=1', $page);
+ $page = $this->post_page('key', ['updated' => 1]);
+ $this->assertContains('Your current plan is Some calls per month.', $page);
+ $this->assertContains('It costs you £0/mth.', $page);
+ $this->assertContains('100% discount applied.', $page);
+ }
}
diff --git a/tests/FetchPageTestCase.php b/tests/FetchPageTestCase.php
index 930e9ce739..8507e09020 100644
--- a/tests/FetchPageTestCase.php
+++ b/tests/FetchPageTestCase.php
@@ -9,7 +9,7 @@ abstract class FetchPageTestCase extends TWFY_Database_TestCase
protected function base_fetch_page($vars, $dir, $page = 'index.php', $req_uri = '')
{
foreach ($vars as $k => $v) {
- $vars[$k] = $k . '=' . urlencode($v);
+ $vars[$k] = $k . '=' . urlencode($v);
}
if (!$req_uri) {
@@ -26,7 +26,7 @@ protected function base_fetch_page($vars, $dir, $page = 'index.php', $req_uri =
protected function base_fetch_page_user($vars, $cookie, $dir, $page = 'index.php', $req_uri = '')
{
foreach ($vars as $k => $v) {
- $vars[$k] = $k . '=' . urlencode($v);
+ $vars[$k] = $k . '=' . urlencode($v);
}
if (!$req_uri) {
@@ -39,4 +39,29 @@ protected function base_fetch_page_user($vars, $cookie, $dir, $page = 'index.php
return $page;
}
+
+ protected function base_post_page_user($vars, $cookie, $dir, $page = 'index.php', $req_uri = '')
+ {
+ foreach ($vars as $k => $v) {
+ $vars[$k] = $k . '=' . urlencode($v);
+ }
+
+ if (!$req_uri) {
+ $req_uri = "/$dir/$page";
+ }
+
+ $vars = join('&', $vars);
+ $command = '
+parse_str($argv[1], $_POST);
+$_POST["_csrf_token_645a83a41868941e4692aa31e7235f2"] = "CSRF";
+$_COOKIE["epuser_id"] = "' . $cookie . '";
+session_start(); $_SESSION["_csrf_token_645a83a41868941e4692aa31e7235f2"] = "CSRF";
+include_once("tests/Bootstrap.php");
+chdir("www/docs/' . $dir . '");
+include_once("' . $page . '");
+';
+ $page = `REQUEST_URI=$req_uri REMOTE_ADDR=127.0.0.1 php -e -r '$command' -- '$vars'`;
+
+ return $page;
+ }
}
diff --git a/tests/_fixtures/api.xml b/tests/_fixtures/api.xml
index 24cb34ef6a..b9f8be25ad 100644
--- a/tests/_fixtures/api.xml
+++ b/tests/_fixtures/api.xml
@@ -15,6 +15,8 @@
Overview
Terms of usage
-Pricing
-
- £1,000 per year for up to 100,000 calls
- £2,000 per year for up to 200,000 calls
- £2,500 per year for up to 300,000 calls
- £3,000 per year for up to 500,000 calls
-Credits
-
+
+
+
+
+
+
-Technical documentation
+Technical documentation
Errors
-<twfy><error>ERROR</error></twfy>
;
in JS {"error":"ERROR"}
;
@@ -273,7 +252,7 @@ function with the callback variable, and then that function will be
About API Keys
-
-
-loggedin()) {
- if (get_http_var('create_key') && get_http_var('reason')) {
- $estimated_usage = (int) get_http_var('estimated_usage');
- $commercial = get_http_var('commercial');
- create_key($commercial, get_http_var('reason'), $estimated_usage);
- if ($commercial == '1' || $estimated_usage > 50000) {
- echo 'Your keys
';
+ list_keys($keys);
+ }
+ if ($subscription->stripe) {
+ include_once INCLUDESPATH . 'easyparliament/templates/html/api/key-form.php';
+ }
+} else {
+ logged_out();
+}
+
+$sidebar = api_sidebar();
+$PAGE->stripe_end(array($sidebar));
+$PAGE->page_end();
+
+# ---
+
+function logged_out() {
+ echo 'Your plan and key are tied to your TheyWorkForYou account,
+so if you don’t yet have one, please sign up, then
+return here to get a key.';
+ echo '
';
}
-$sidebar = api_sidebar();
-$PAGE->stripe_end(array($sidebar));
-$PAGE->page_end();
-
-# ---
-
-function create_key($commercial, $reason, $estimated_usage) {
- global $THEUSER;
+function create_key($user) {
$key = auth_ab64_encode(urandom_bytes(16));
$db = new ParlDB;
- if ($commercial=='') $commercial = 0;
$db->query('INSERT INTO api_key (user_id, api_key, commercial, created, reason, estimated_usage) VALUES
- (:user_id, :key, :commercial, NOW(), :reason, :estimated_usage)', array(
- ':user_id' => $THEUSER->user_id(),
+ (:user_id, :key, -1, NOW(), :reason, -1)', [
+ ':user_id' => $user->user_id(),
':key' => $key,
- ':commercial' => $commercial,
- ':reason' => $reason,
- ':estimated_usage' => $estimated_usage
- ));
-}
-
-function api_key_form() {
-?>
-Your keys
';
foreach ($keys as $keyarr) {
- list($key, $commercial, $created, $reason, $estimated_usage) = $keyarr;
+ list($key, $created, $reason) = $keyarr;
echo '
';
- }
- api_key_form();
-} else {
- echo ' The key is tied to your TheyWorkForYou account,
-so if you don\'t yet have one, please sign up, then
-return here to get a key.';
- echo '';
+ echo '
';
- if ($commercial==1) echo 'Commercial key,';
- elseif ($commercial==-1) echo 'Key';
- else echo 'Non-commercial key,';
- echo ' created ', $created, '; ', $reason, '; estimated usage ', $estimated_usage;
+ echo 'Key created ', $created, '; ', $reason;
echo '
Usage statistics: ';
$q = $db->query('SELECT count(*) as count FROM api_stats WHERE api_key="' . $key . '" AND query_time > NOW() - interval 1 day');
$c = $q->field(0, 'count');
@@ -54,62 +97,18 @@
echo "last month: $c";
echo '';
}
- if ($keys) {
- echo '
-Get a new key
-
- '',
+ ]);
+ $r = new \MySociety\TheyWorkForYou\Redis();
+ $r->set("key:$key:api:" . REDIS_API_NAME, $user->user_id());
}
diff --git a/www/docs/api/terms.php b/www/docs/api/terms.php
new file mode 100644
index 0000000000..1404574779
--- /dev/null
+++ b/www/docs/api/terms.php
@@ -0,0 +1,7 @@
+loggedin()) {
+ redirect('/api/');
+}
+
+$subscription = new MySociety\TheyWorkForYou\Subscription($THEUSER);
+if (!$subscription->stripe) {
+ redirect('/api/key');
+}
+
+MySociety\TheyWorkForYou\Utility\Session::start();
+if (!Volnix\CSRF\CSRF::validate($_POST)) {
+ print 'CSRF validation failure!';
+ exit;
+}
+
+$token = get_http_var('stripeToken');
+$sub = $subscription->stripe;
+$sub->customer->source = $token;
+$sub->customer->save();
+redirect('/api/key?updated=1');
diff --git a/www/docs/api/update-plan.php b/www/docs/api/update-plan.php
new file mode 100644
index 0000000000..1a5a2beb00
--- /dev/null
+++ b/www/docs/api/update-plan.php
@@ -0,0 +1,35 @@
+loggedin()) {
+ redirect('/api/');
+}
+
+$subscription = new MySociety\TheyWorkForYou\Subscription($THEUSER);
+$errors = array();
+
+MySociety\TheyWorkForYou\Utility\Session::start();
+if (get_http_var('plan')) {
+ if (!Volnix\CSRF\CSRF::validate($_POST)) {
+ print 'CSRF validation failure!';
+ exit;
+ }
+
+ $errors = $subscription->checkForErrors();
+ if (!$errors) {
+ $subscription->createOrUpdateFromForm();
+ redirect('/api/key?updated=1');
+ }
+}
+
+$this_page = 'api_key';
+$PAGE->page_start();
+$PAGE->stripe_start();
+
+include_once INCLUDESPATH . 'easyparliament/templates/html/api/update.php';
+
+$sidebar = api_sidebar();
+$PAGE->stripe_end(array($sidebar));
+$PAGE->page_end();
diff --git a/www/docs/js/payment.js b/www/docs/js/payment.js
new file mode 100644
index 0000000000..4184954150
--- /dev/null
+++ b/www/docs/js/payment.js
@@ -0,0 +1,119 @@
+document.getElementById('id_charitable_tick').addEventListener('click', function(e) {
+ if (this.checked) {
+ document.getElementById('charitable-qns').style.display = 'block';
+ } else {
+ document.getElementById('charitable-qns').style.display = 'none';
+ }
+});
+document.getElementById('id_charitable_0').addEventListener('change', function(e) {
+ document.getElementById('charitable-neither').style.display = 'none';
+ document.getElementById('charitable-desc').style.display = 'none';
+ document.getElementById('charity-number').style.display = 'block';
+});
+document.getElementById('id_charitable_1').addEventListener('change', function(e) {
+ document.getElementById('charity-number').style.display = 'none';
+ document.getElementById('charitable-neither').style.display = 'none';
+ document.getElementById('charitable-desc').style.display = 'block';
+});
+document.getElementById('id_charitable_2').addEventListener('change', function(e) {
+ document.getElementById('charitable-neither').style.display = 'block';
+ document.getElementById('charity-number').style.display = 'none';
+ document.getElementById('charitable-desc').style.display = 'none';
+});
+
+var stripe_key = document.getElementById('js-payment').getAttribute('data-key');
+var handler = StripeCheckout.configure({
+ key: stripe_key,
+ image: 'https://s3.amazonaws.com/stripe-uploads/acct_19EbqNIbP0iBLddtmerchant-icon-1479145884111-mysociety-wheel-logo.png',
+ locale: 'auto',
+ token: function(token) {
+ var form = document.getElementById('signup_form');
+ form.stripeToken.value = token.id;
+ form.submit();
+ }
+});
+
+var stripeButton = document.getElementById('customButton');
+stripeButton && stripeButton.addEventListener('click', function(e) {
+ // Already got a token from Stripe (so password mismatch error or somesuch)
+ var form = document.getElementById('signup_form');
+ if (form.stripeToken.value) {
+ return;
+ }
+ e.preventDefault();
+
+ function err_highlight(labelElement, err) {
+ var $field = $(labelElement).closest('.row');
+ if (err) {
+ $field.addClass('account-form__field--error');
+ return 1;
+ } else {
+ $field.removeClass('account-form__field--error');
+ return 0;
+ }
+ }
+
+ function err(field, extra) {
+ var f = document.getElementById(field);
+ if (!f) {
+ return 0;
+ }
+ f = f.value;
+ var label = document.querySelector('label[for=' + field + ']');
+ return err_highlight(label, extra !== undefined ? extra && !f : !f);
+ }
+
+ var errors = 0;
+ var plan = document.querySelector('input[name=plan]:checked');
+ errors += err_highlight(document.querySelector('label[for=id_plan_0]'), !plan);
+ var ctick = document.getElementById('id_charitable_tick').checked;
+ var c = document.querySelector('input[name=charitable]:checked');
+ errors += err_highlight(document.querySelector('label[for=id_charitable_0]'), ctick && !c);
+ errors += err('id_charity_number', ctick && c && c.value === 'c');
+ errors += err('id_description', ctick && c && c.value === 'i');
+ var tandcs = document.getElementById('id_tandcs_tick');
+ errors += tandcs && err_highlight(tandcs.parentNode, !tandcs.checked);
+ if (errors) {
+ return;
+ }
+
+ plan = plan.value;
+ var num = 20;
+ if (plan === 'twfy-5k') {
+ num = 50;
+ } else if (plan === 'twfy-10k') {
+ num = 100;
+ } else if (plan === 'twfy-0k') {
+ num = 300;
+ }
+ if (ctick) {
+ c = c.value;
+ if (c === 'c' || c === 'i') {
+ if (num === 20) {
+ num = 0;
+ } else {
+ num = num / 2;
+ }
+ }
+ }
+ if (num === 0 || document.getElementById('js-payment').getAttribute('data-has-payment-data')) {
+ form.submit();
+ return;
+ }
+
+ var email = document.getElementById('js-payment').getAttribute('data-email');
+
+ handler.open({
+ name: 'mySociety',
+ description: 'Subscribing to plan ' + plan,
+ zipCode: true,
+ currency: 'gbp',
+ allowRememberMe: false,
+ email: email,
+ amount: num * 100
+ });
+});
+
+window.addEventListener('popstate', function() {
+ handler.close();
+});
diff --git a/www/docs/style/sass/app.scss b/www/docs/style/sass/app.scss
index b823133d2d..ff52ec277a 100644
--- a/www/docs/style/sass/app.scss
+++ b/www/docs/style/sass/app.scss
@@ -199,6 +199,7 @@ form {
@import "pages/section";
@import "pages/action";
@import "pages/alert";
+@import "pages/api";
@import "pages/home";
@import "pages/static";
@import "pages/about";
diff --git a/www/docs/style/sass/pages/_api.scss b/www/docs/style/sass/pages/_api.scss
new file mode 100644
index 0000000000..fb904ee0ea
--- /dev/null
+++ b/www/docs/style/sass/pages/_api.scss
@@ -0,0 +1,10 @@
+.account-form__field--error {
+ color: #f00;
+ label {
+ color: #f00;
+ }
+ input {
+ border-color: red;
+ }
+}
+
diff --git a/www/docs/user/login/fb.php b/www/docs/user/login/fb.php
index 4472d1ebd4..fe71ff60c6 100644
--- a/www/docs/user/login/fb.php
+++ b/www/docs/user/login/fb.php
@@ -17,9 +17,7 @@
$login = new \MySociety\TheyWorkForYou\FacebookLogin();
# used by the facebook login code for CSRF tokens
-session_start();
-
-global $this_page, $DATA;
+MySociety\TheyWorkForYou\Utility\Session::start();
$this_page = 'topic';
diff --git a/www/docs/user/login/index.php b/www/docs/user/login/index.php
index 47cec12cfc..4c073cdf8f 100644
--- a/www/docs/user/login/index.php
+++ b/www/docs/user/login/index.php
@@ -13,7 +13,7 @@
# we need this for the facebook login code to work as it stores
# some CSFR tokens in the session
-session_start();
+MySociety\TheyWorkForYou\Utility\Session::start();
$this_page = "userlogin";
if (get_http_var("submitted") == "true") {
diff --git a/www/includes/easyparliament/metadata.php b/www/includes/easyparliament/metadata.php
index f940366a27..e9d5d540ff 100644
--- a/www/includes/easyparliament/metadata.php
+++ b/www/includes/easyparliament/metadata.php
@@ -235,7 +235,7 @@
'url' => 'api/'
),
'api_key' => array (
- 'title' => 'API Keys',
+ 'title' => 'Plan and keys',
'parent' => 'api_front',
'url' => 'api/key'
),
diff --git a/www/includes/easyparliament/templates/emails/api_cancelled.txt b/www/includes/easyparliament/templates/emails/api_cancelled.txt
new file mode 100644
index 0000000000..b6581c0c7d
--- /dev/null
+++ b/www/includes/easyparliament/templates/emails/api_cancelled.txt
@@ -0,0 +1,12 @@
+Subject: Your subscription to the TheyWorkForYou API has been cancelled
+Hi,
+
+I'm afraid that when we tried to take payment for your subscription to the
+TheyWorkForYou API, it failed again. We've cancelled your subscription, so
+you are now back on the free tier. If you wish to resubscribe, please just
+log in to TheyWorkForYou and pick a new plan:
+ https://www.theyworkforyou.com/api/key
+
+Yours,
+mySociety
+
diff --git a/www/includes/easyparliament/templates/emails/api_payment_failed.txt b/www/includes/easyparliament/templates/emails/api_payment_failed.txt
new file mode 100644
index 0000000000..82652bd7cf
--- /dev/null
+++ b/www/includes/easyparliament/templates/emails/api_payment_failed.txt
@@ -0,0 +1,13 @@
+Subject: Your payment for the TheyWorkForYou API has failed
+Hi,
+
+I'm afraid that when we tried to take payment for your subscription to the
+TheyWorkForYou API, it failed. We'll try again in three days time, but
+please could you log in to TheyWorkForYou and check that your payment details
+are up to date:
+ https://www.theyworkforyou.com/api/key
+
+In the meantime, your quota has not been reset.
+
+Yours,
+mySociety
diff --git a/www/includes/easyparliament/templates/html/api/cancel.php b/www/includes/easyparliament/templates/html/api/cancel.php
new file mode 100644
index 0000000000..f3857838fa
--- /dev/null
+++ b/www/includes/easyparliament/templates/html/api/cancel.php
@@ -0,0 +1,11 @@
+Cancel subscription
+
+ Get a new key
+
+
diff --git a/www/includes/easyparliament/templates/html/api/subscription_detail.php b/www/includes/easyparliament/templates/html/api/subscription_detail.php
new file mode 100644
index 0000000000..5268db613f
--- /dev/null
+++ b/www/includes/easyparliament/templates/html/api/subscription_detail.php
@@ -0,0 +1,130 @@
+quota_status();
+$account_balance = $subscription->stripe->customer->account_balance;
+if ($subscription->upcoming) {
+ if ($subscription->upcoming->total < 0) {
+ # Going to be credited
+ $account_balance += $subscription->upcoming->total;
+ }
+}
+
+?>
+
+ Subscription
+
+
+ 0 && $quota_status['count'] > $quota_status['quota']) { ?>
+
Your account has a balance of £= number_format(-$account_balance / 100, 2); ?>.
+
+
+
+
+
+
+ Your usage
+
+
+
+ Your payment details
+
+ stripe->customer->default_source) { ?>
+ = $subscription->stripe->customer->default_source->last4 ?>
.= $stripe ? "Change plan" : "Subscribe to a plan" ?>
+
+
+ Terms and conditions
+
+
+Introduction
+Usage
+Call limits
+Charitable use
+Accounts
+Termination, cancellation and suspension
+
+
+Payment
+Copyright
+
+Licensing
+
+
+
+Attribution
+SLA and support
+Small print
+Privacy
+