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]> -
$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 @@ + + @@ -202,6 +204,14 @@ + + 1 + Test + User + user@example.org + $2y$10$UNelQZqpPpO1jT.f7DLgeOdp.WBT81c5ECvOeTMFeQTBTyq3aCh8q + 1 + diff --git a/www/docs/api/cancel-plan.php b/www/docs/api/cancel-plan.php new file mode 100644 index 0000000000..48e04460ab --- /dev/null +++ b/www/docs/api/cancel-plan.php @@ -0,0 +1,34 @@ +loggedin()) { + redirect('/api/'); +} + +$subscription = new MySociety\TheyWorkForYou\Subscription($THEUSER); +if (!$subscription->stripe) { + redirect('/api/key'); +} + +MySociety\TheyWorkForYou\Utility\Session::start(); +if (get_http_var('cancel')) { + if (!Volnix\CSRF\CSRF::validate($_POST)) { + print 'CSRF validation failure!'; + exit; + } + + $subscription->stripe->cancel(['at_period_end' => true]); + redirect('/api/key?cancelled=1'); +} + +$this_page = 'api_key'; +$PAGE->page_start(); +$PAGE->stripe_start(); + +include_once INCLUDESPATH . 'easyparliament/templates/html/api/cancel.php'; + +$sidebar = api_sidebar(); +$PAGE->stripe_end(array($sidebar)); +$PAGE->page_end(); diff --git a/www/docs/api/hook.php b/www/docs/api/hook.php new file mode 100644 index 0000000000..d1b14dc839 --- /dev/null +++ b/www/docs/api/hook.php @@ -0,0 +1,82 @@ +data->object; +if ($event->type == 'customer.subscription.deleted') { + $db = new ParlDB; + $sub = new \MySociety\TheyWorkForYou\Subscription($obj->id); + if ($sub->stripe) { + $sub->delete_from_redis(); + $db->query('DELETE FROM api_subscription WHERE stripe_id = :stripe_id', [':stripe_id' => $obj->id]); + } +} elseif ($event->type == 'customer.subscription.updated') { + $sub = new \MySociety\TheyWorkForYou\Subscription($obj->id); + if ($sub->stripe) { + $sub->redis_update_max($obj->plan->id); + } +} elseif ($event->type == 'invoice.payment_failed' && stripe_twfy_sub($obj)) { + $customer = \Stripe\Customer::retrieve($obj->customer); + $email = $customer->email; + if ($obj->next_payment_attempt) { + send_template_email(array('template' => 'api_payment_failed', 'to' => $email), array()); + } else { + send_template_email(array('template' => 'api_cancelled', 'to' => $email), array()); + } +} elseif ($event->type == 'invoice.payment_succeeded' && stripe_twfy_sub($obj)) { + stripe_reset_quota($obj->subscription); + try { + # Update the invoice's charge to say it came from TWFY (for CSV export) + $charge = \Stripe\Charge::retrieve($obj->charge); + $charge->description = 'TheyWorkForYou'; + $charge->save(); + } catch (\Stripe\Error\Base $e) { + } +} elseif ($event->type == 'invoice.updated' && stripe_twfy_sub($obj)) { + if ($obj->forgiven && property_exists($event->data, 'previous_attributes')) { + $previous = $event->data->previous_attributes; + if (array_key_exists('forgiven', $previous) && !$previous['forgiven']) { + stripe_reset_quota($obj->subscription); + } + } +} + +http_response_code(200); + +# --- + +function stripe_twfy_sub($invoice) { + # If the invoice doesn't have a subscription, ignore it + if (!$invoice->subscription) { + return false; + } + $stripe_sub = \Stripe\Subscription::retrieve($invoice->subscription); + return substr($stripe_sub->plan->id, 0, 4) == 'twfy'; +} + +function stripe_reset_quota($subscription) { + $sub = new \MySociety\TheyWorkForYou\Subscription($subscription); + if ($sub->stripe) { + $sub->redis_reset_quota(); + } else { + $subject = "Someone's subscription was not renewed properly"; + $message = "TheyWorkForYou tried to reset the quota for subscription $subscription but couldn't find it"; + send_email(CONTACTEMAIL, $subject, $message); + } +} diff --git a/www/docs/api/index.php b/www/docs/api/index.php index a31367e8ec..d1b48d9ae9 100644 --- a/www/docs/api/index.php +++ b/www/docs/api/index.php @@ -147,19 +147,15 @@ function api_front_page($error = '') { print "

$error

"; } -$em = join('@', array('enquiries', 'mysociety.org')); - ?> -

Overview

- 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.

-

Terms of usage

-

- 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 - (mentioning TheyWorkForYou) if your usage is likely to fall into the - non-free bracket: unlicensed users may be blocked without warning. -

-

Pricing

-

- £500 per year for up to 50,000 calls (free for charitable usage)
- £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
-

-

In addition, we offer a 50% discount on the above rates for charitable usage.

-

Credits

-

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. +

+
-

Technical documentation

+

Technical documentation

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.

@@ -240,14 +219,14 @@ function api_front_page($error = '') {
  • js. A JavaScript object. You can provide a callback function with the callback variable, and then that function will be called with the data as its argument.
  • -
  • rabx. "RPC over Anything But XML".
  • +
  • rabx. “RPC over Anything But XML”.
  • Errors

    -

    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 <twfy><error>ERROR</error></twfy>; in JS {"error":"ERROR"}; @@ -273,7 +252,7 @@ function with the callback variable, and then that function will be

    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.

    diff --git a/www/docs/api/key.php b/www/docs/api/key.php index e99f1e6c1c..d3476318bf 100644 --- a/www/docs/api/key.php +++ b/www/docs/api/key.php @@ -4,44 +4,87 @@ include_once './api_functions.php'; include_once INCLUDESPATH . '../../commonlib/phplib/auth.php'; -$a = auth_ab64_encode(urandom_bytes(32)); +MySociety\TheyWorkForYou\Utility\Session::start(); $this_page = 'api_key'; $PAGE->page_start(); $PAGE->stripe_start(); -?> -

    About API Keys

    -

    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.

    - -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 '

    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.

    '; + if (get_http_var('create_key')) { + if (!Volnix\CSRF\CSRF::validate($_POST)) { + print 'CSRF validation failure!'; + exit; } + create_key($THEUSER); } - $db = new ParlDB; - $q = $db->query('SELECT api_key, commercial, created, reason, estimated_usage FROM api_key WHERE user_id=' . $THEUSER->user_id()); - $keys = array(); - for ($i=0; $i<$q->rows(); $i++) { - $keys[] = array($q->field($i, 'api_key'), $q->field($i, 'commercial'), $q->field($i, 'created'), $q->field($i, 'reason'), $q->field($i, 'estimated_usage')); + + if (get_http_var('updated')) { + print '

    Thanks very much!

    '; + if (has_no_keys($THEUSER)) { + create_key($THEUSER); + } + } + if (get_http_var('cancelled')) { + print '

    Your subscription has been cancelled.

    '; } + + $subscription = new MySociety\TheyWorkForYou\Subscription($THEUSER); + $errors = []; + + if ($subscription->stripe) { + include_once INCLUDESPATH . 'easyparliament/templates/html/api/subscription_detail.php'; + } else { + include_once INCLUDESPATH . 'easyparliament/templates/html/api/update.php'; + } + + $keys = get_keys($THEUSER); if ($keys) { - echo '

    Your keys

    '; } -$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() { -?> -
    -

    Get a new key

    -
    -

    About you:
    -
    -
    - -

    -

    -
    - -

    -

    What's your estimated annual API call volume?
    -
    -
    -
    -
    -
    - -

    -

    - -

    - '', + ]); + $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

    + +

    If you’re sure you wish to cancel your subscription at the end of its + current month, please select the button below.

    + +
    + + +
    +
    diff --git a/www/includes/easyparliament/templates/html/api/key-form.php b/www/includes/easyparliament/templates/html/api/key-form.php new file mode 100644 index 0000000000..fd2e2a08a1 --- /dev/null +++ b/www/includes/easyparliament/templates/html/api/key-form.php @@ -0,0 +1,7 @@ +

    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']) { ?> +

    + You have used up your quota for the stripe->plan->interval ?>. + Please upgrade + or contact us. +

    + +

    + Your account is currently suspended. Please + contact us. +

    + + + + stripe->plan) { ?> + +

    Your current plan is stripe->plan->nickname ?>.

    + +

    It costs you £actual_paid ?>/stripe->plan->interval ?>. + stripe->discount) { ?> + (£stripe->plan->amount ?>/stripe->plan->interval ?> with + stripe->discount->coupon->percent_off ?>% discount applied.) + +

    + + stripe->discount && $subscription->stripe->discount->end) { ?> +

    Your discount will expire on stripe->discount->end ?>.

    + + +

    Subscription created on stripe->created) ?>; + stripe->cancel_at_period_end) { ?> + it expires on 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 stripe->current_period_end) ?>. + + your next invoice date is stripe->current_period_end) ?>. + + + +
    Your account has a balance of £. + +

    + + + +
    + +

    Your usage

    + +

    + This stripe->plan->interval ?>: + + 0) { ?> + out of + + API calls +

    + + 0) { ?> + + out of + API calls + + + +
    + +

    Your payment details

    + + stripe->customer->default_source) { ?> +

    Payment details we hold: stripe->customer->default_source->brand ?>, + last four digits stripe->customer->default_source->last4 ?>.

    + +

    We do not currently hold any payment details for you.

    + + +
    + + +
    + + +

    You are not currently subscribed to a plan.

    +

    + Subscribe to a plan

    +

    + + + +
    diff --git a/www/includes/easyparliament/templates/html/api/update.php b/www/includes/easyparliament/templates/html/api/update.php new file mode 100644 index 0000000000..f63b87575e --- /dev/null +++ b/www/includes/easyparliament/templates/html/api/update.php @@ -0,0 +1,124 @@ +stripe; +$stripeToken = get_http_var('stripeToken'); +$charitable_tick = $stripe ? $stripe->discount : ''; +$charitable = $stripe ? $stripe->metadata['charitable'] : ''; +$charity_number = $stripe ? $stripe->metadata['charity_number'] : ''; +$description = $stripe ? $stripe->metadata['description'] : ''; + +function rdio($name, $value, $text, $id, $required = false, $checked = false) { +?> +
  • + + +

    + + +
    + We have safely stored your payment information, and you have not yet been charged. + Please correct the issues below, you will not need to reenter your card details. +
    + + +"; + foreach ($errors as $error) { + print "
  • $error
  • "; + } + print ""; +} +?> + +cancel_at_period_end) { ?> +

    Your plan is curently set to expire on current_period_end) ?>. +If you update your plan below, it will be reactivated. +

    + + +
    + + + +
    + + +
    + +
    + +
    + + + + +
    + +
    + + + + +
    + +
    +
    + + + diff --git a/www/includes/easyparliament/templates/html/static/api_terms.php b/www/includes/easyparliament/templates/html/static/api_terms.php new file mode 100644 index 0000000000..04d2c78d0a --- /dev/null +++ b/www/includes/easyparliament/templates/html/static/api_terms.php @@ -0,0 +1,210 @@ +
    +
    +
    + +

    Terms and conditions

    +
    + +
    + +
    + +
    + +

    Introduction

    +

    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.

    +
    + +
    +

    Usage

    +

    We reserve the right to block, without warning, usage which violates any +of the following restrictions.

    +

    Call limits

    +

    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.

    +

    Charitable use

    +

    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.

    +
    + +
    +

    Accounts

    +

    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.

    +
    + +
    +

    Termination, cancellation and suspension

    +

    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:

    +
      +
    • You have failed to make any payment due; +
    • mySociety reasonably believes you to be in material breach of any term of +this agreement; +
    • mySociety reasonably believes that it is necessary to do so in order to +comply with any applicable law or the order of a court or other competent +authority. +
    +

    We also reserve the right to modify or terminate the API for any reason, +without notice at any time.

    +
    + +
    +

    Payment

    +

    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.

    +
    + + + +
    +

    Licensing

    +

    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.

    + +
    + +
    +

    Attribution

    +

    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.

    +
    + +
    +

    SLA and support

    +

    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.

    +
    + +
    +

    Small print

    +

    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.

    +
    + +
    +

    Privacy

    +

    You can find our Privacy Policy +here.

    +
    + +
    +
    +
    diff --git a/www/includes/easyparliament/templates/html/static/privacy.php b/www/includes/easyparliament/templates/html/static/privacy.php index fe81ecbb23..c570001d84 100644 --- a/www/includes/easyparliament/templates/html/static/privacy.php +++ b/www/includes/easyparliament/templates/html/static/privacy.php @@ -47,9 +47,15 @@

    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