-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update API to handle payment via Stripe.
Basically the same user handling as MapIt.
- Loading branch information
Showing
37 changed files
with
1,529 additions
and
212 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ matrix: | |
|
||
services: | ||
- memcached | ||
- redis-server | ||
|
||
notifications: | ||
email: false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace MySociety\TheyWorkForYou; | ||
|
||
class Redis extends \Predis\Client { | ||
public function __construct() { | ||
$redis_args = [ | ||
'host' => 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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace MySociety\TheyWorkForYou; | ||
|
||
class Stripe { | ||
static private $instance; | ||
|
||
public function __construct($stripeSecretKey) { | ||
if (self::$instance) { | ||
throw new \RuntimeException('Stripe could not be instantiate more than once. Check PHP implementation : https://github.com/stripe/stripe-php'); | ||
} | ||
self::$instance = $this; | ||
|
||
\Stripe\Stripe::setApiKey($stripeSecretKey); | ||
} | ||
|
||
public function getSubscription($args) { | ||
return \Stripe\Subscription::retrieve($args); | ||
} | ||
|
||
public function getUpcomingInvoice($args) { | ||
return \Stripe\Invoice::upcoming($args); | ||
} | ||
|
||
public function createCustomer($args) { | ||
return \Stripe\Customer::create($args); | ||
} | ||
|
||
public function createSubscription($args) { | ||
return \Stripe\Subscription::create($args); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
<?php | ||
|
||
namespace MySociety\TheyWorkForYou; | ||
|
||
function add_vat($pence) { | ||
return round($pence * 1.2 / 100, 2); | ||
} | ||
|
||
class Subscription { | ||
public $stripe; | ||
public $upcoming; | ||
public $has_payment_data = false; | ||
|
||
private static $plans = ['twfy-1k', 'twfy-5k', 'twfy-10k', 'twfy-0k']; | ||
|
||
public function __construct($arg) { | ||
# User ID | ||
if (is_int($arg)) { | ||
$user = new \USER; | ||
$user->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), | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<?php | ||
|
||
namespace MySociety\TheyWorkForYou; | ||
|
||
class TestStripe extends Stripe { | ||
public function getSubscription($args) { | ||
if ($args['id'] == 'sub_123') { | ||
return \Stripe\Util\Util::convertToStripeObject([ | ||
'id' => 'sub_123', | ||
'discount' => [ | ||
'coupon' => ['percent_off' => 100], | ||
'end' => null | ||
], | ||
'plan' => [ | ||
'amount' => '2000', | ||
'id' => 'twfy-1k', | ||
'nickname' => 'Some calls per month', | ||
'interval' => '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' => '[email protected]', | ||
], null); | ||
} | ||
|
||
public function createSubscription($args) { | ||
return \Stripe\Util\Util::convertToStripeObject([ | ||
'id' => 'sub_123', | ||
], null); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
namespace MySociety\TheyWorkForYou\Utility; | ||
|
||
class Session { | ||
public static function start() { | ||
if (session_status() !== PHP_SESSION_ACTIVE) { | ||
session_start(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.