Failed Conditions
Pull Request — master (#1800)
by Matthew
37:23
created

Subscription   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Test Coverage

Coverage 17.39%

Importance

Changes 0
Metric Value
eloc 213
dl 0
loc 345
ccs 32
cts 184
cp 0.1739
rs 2.56
c 0
b 0
f 0
wmc 73

16 Methods

Rating   Name   Duplication   Size   Complexity  
A delete_from_redis() 0 4 1
A update_email() 0 2 1
C __construct() 0 79 13
A update_payment_method() 0 6 1
F update_subscription() 0 73 20
A quota_status() 0 6 1
A createOrUpdateFromForm() 0 21 4
A checkValidPlan() 0 2 2
A invoices() 0 7 1
C checkForErrors() 0 36 12
B add_subscription() 0 40 6
A getFields() 0 5 2
A redis_reset_quota() 0 6 2
A checkPaymentGivenIfNeeded() 0 5 5
A update_customer() 0 2 1
A redis_update_max() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Subscription often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Subscription, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MySociety\TheyWorkForYou;
4
5
function add_vat($pence) {
6 1
    return round($pence * 1.2 / 100, 2);
7
}
8
9
class Subscription {
10
    public $stripe;
11
    public $upcoming;
12
    public $has_payment_data = false;
13
14
    private static $plans = ['twfy-1k', 'twfy-5k', 'twfy-10k', 'twfy-0k'];
15
    private static $prices = [2000, 5000, 10000, 30000];
16 1
17
    public function __construct($arg) {
18 1
        # User ID
19
        if (is_int($arg)) {
20
            $user = new \USER();
21
            $user->init($arg);
22
            $arg = $user;
23
        }
24 1
25 1
        $this->db = new \ParlDB();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
26 1
        $this->redis = new Redis();
0 ignored issues
show
Bug Best Practice introduced by
The property redis does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
27 1
        if (defined('TESTING')) {
28
            $this->api = new TestStripe("");
0 ignored issues
show
Bug Best Practice introduced by
The property api does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
29
        } else {
30
            $this->api = new Stripe(STRIPE_SECRET_KEY);
31
        }
32 1
33
        if (is_a($arg, 'User')) {
34 1
            # User object
35 1
            $this->user = $arg;
0 ignored issues
show
Bug Best Practice introduced by
The property user does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
36 1
            $this->redis_prefix = "user:{$this->user->user_id}:quota:" . REDIS_API_NAME;
0 ignored issues
show
Bug Best Practice introduced by
The property redis_prefix does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
37 1
            $q = $this->db->query('SELECT * FROM api_subscription WHERE user_id = :user_id', [
38 1
                ':user_id' => $this->user->user_id()])->first();
39 1
            if ($q) {
40
                $id = $q['stripe_id'];
41 1
            } else {
42
                return;
43
            }
44
        } else {
45
            # Assume Stripe ID string
46
            $id = $arg;
47
            $q = $this->db->query('SELECT * FROM api_subscription WHERE stripe_id = :stripe_id', [
48
                ':stripe_id' => $id])->first();
49
            if ($q) {
50
                $user = new \USER();
51
                $user->init($q['user_id']);
52
                $this->user = $user;
53
                $this->redis_prefix = "user:{$this->user->user_id}:quota:" . REDIS_API_NAME;
54
            } else {
55
                return;
56
            }
57
        }
58
59 1
        try {
60 1
            $this->stripe = $this->api->getSubscription([
61
                'id' => $id,
62
                'expand' => [
63
                    'customer.default_source',
64
                    'customer.invoice_settings.default_payment_method',
65
                    'latest_invoice.payment_intent',
66
                    'schedule.phases.items.plan',
67
                ],
68
            ]);
69
        } catch (\Stripe\Exception\InvalidRequestException $e) {
70
            $this->db->query('DELETE FROM api_subscription WHERE stripe_id = :stripe_id', [':stripe_id' => $id]);
71
            $this->delete_from_redis();
72
            return;
73 1
        }
74 1
75
        $this->has_payment_data = $this->stripe->customer->default_source || $this->stripe->customer->invoice_settings->default_payment_method;
76
        if ($this->stripe->customer->invoice_settings->default_payment_method) {
77 1
            $this->card_info = $this->stripe->customer->invoice_settings->default_payment_method->card;
0 ignored issues
show
Bug Best Practice introduced by
The property card_info does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
78
        } else {
79
            $this->card_info = $this->stripe->customer->default_source;
80 1
        }
81 1
82 1
        $data = $this->stripe;
83 1
        if ($data->discount && $data->discount->coupon && $data->discount->coupon->percent_off) {
84 1
            $this->actual_paid = add_vat(floor(
0 ignored issues
show
Bug Best Practice introduced by
The property actual_paid does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
85
                $data->plan->amount * (100 - $data->discount->coupon->percent_off) / 100
86
            ));
87
            $data->plan->amount = add_vat($data->plan->amount);
88
        } else {
89
            $data->plan->amount = add_vat($data->plan->amount);
90
            $this->actual_paid = $data->plan->amount;
91 1
        }
92
93
        try {
94 1
            $this->upcoming = $this->api->getUpcomingInvoice(["customer" => $this->stripe->customer->id]);
95
        } catch (\Stripe\Exception\ApiErrorException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
96
        }
97
    }
98
99
    private function update_subscription($form_data) {
100
        if ($form_data['payment_method']) {
101
            $this->update_payment_method($form_data['payment_method']);
102
        }
103
104
        foreach ($this::$plans as $i => $plan) {
105
            if ($plan == $form_data['plan']) {
106
                $new_price = $this::$prices[$i];
107
                if ($form_data['coupon'] == 'charitable100') {
108
                    $new_price = 0;
109
                } elseif ($form_data['coupon'] == 'charitable50') {
110
                    $new_price /= 2;
111
                }
112
            }
113
            if ($plan == $this->stripe->plan->id) {
114
                $old_price = $this::$prices[$i];
115
                if ($this->stripe->discount && ($coupon = $this->stripe->discount->coupon)) {
116
                    if ($coupon->percent_off == 100) {
117
                        $old_price = 0;
118
                    } elseif ($coupon->percent_off == 50) {
119
                        $old_price /= 2;
120
                    }
121
                }
122
            }
123
        }
124
125
       if ($old_price >= $new_price) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $old_price does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $new_price does not seem to be defined for all execution paths leading up to this point.
Loading history...
126
            if ($this->stripe->schedule) {
127
                \Stripe\SubscriptionSchedule::release($this->stripe->schedule);
0 ignored issues
show
Bug Best Practice introduced by
The method Stripe\SubscriptionSchedule::release() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

127
                \Stripe\SubscriptionSchedule::/** @scrutinizer ignore-call */ 
128
                                              release($this->stripe->schedule);
Loading history...
128
            }
129
            $schedule = \Stripe\SubscriptionSchedule::create(['from_subscription' => $this->stripe->id]);
130
            $phases = [
131 1
                [
132 1
                    'items' => [['price' => $schedule->phases[0]->items[0]->price]],
133 1
                    'start_date' => $schedule->phases[0]->start_date,
134
                    'end_date' => $schedule->phases[0]->end_date,
135 1
                    'proration_behavior' => 'none',
136 1
                    'default_tax_rates' => [STRIPE_TAX_RATE],
0 ignored issues
show
Bug introduced by
The constant MySociety\TheyWorkForYou\STRIPE_TAX_RATE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
137 1
                ],
138
                [
139
                    'items' => [['price' => $form_data['plan']]],
140
                    'iterations' => 1,
141
                    'metadata' => $form_data['metadata'],
142
                    'proration_behavior' => 'none',
143
                    'default_tax_rates' => [STRIPE_TAX_RATE],
144
                ],
145
            ];
146
            if ($schedule->phases[0]->discounts && $schedule->phases[0]->discounts[0]->coupon) {
147
                $phases[0]['discounts'] = [['coupon' => $schedule->phases[0]->discounts[0]->coupon]];
148
            }
149
            if ($form_data['coupon']) {
150
                $phases[1]['coupon'] = $form_data['coupon'];
151
            }
152
            \Stripe\SubscriptionSchedule::update($schedule->id, ['phases' => $phases]);
153
        }
154
155
        if ($old_price < $new_price) {
156
            $args = [
157
                'payment_behavior' => 'allow_incomplete',
158
                'plan' => $form_data['plan'],
159
                'metadata' => $form_data['metadata'],
160
                'cancel_at_period_end' => false, # Needed in Stripe 2018-02-28
161
                'proration_behavior' => 'always_invoice',
162
            ];
163
            if ($form_data['coupon']) {
164
                $args['coupon'] = $form_data['coupon'];
165
            } elseif ($this->stripe->discount) {
166
                $args['coupon'] = '';
167
            }
168
            if ($this->stripe->schedule) {
169
                \Stripe\SubscriptionSchedule::release($this->stripe->schedule);
170
            }
171
            \Stripe\Subscription::update($this->stripe->id, $args);
172
        }
173
    }
174
175
    private function update_customer($args) {
176
        $this->api->updateCustomer($this->stripe->customer->id, $args);
177
    }
178
179
    public function update_email($email) {
180
        $this->update_customer([ 'email' => $email ]);
181
    }
182
183
    public function update_payment_method($payment_method) {
184
        $payment_method = \Stripe\PaymentMethod::retrieve($payment_method);
185
        $payment_method->attach(['customer' => $this->stripe->customer->id]);
186
        $this->update_customer([
187
            'invoice_settings' => [
188
                'default_payment_method' => $payment_method,
189
            ],
190
        ]);
191
    }
192
193
    private function add_subscription($form_data) {
194
        # Create new Stripe customer and subscription
195
        $cust_params = ['email' => $this->user->email()];
196
        if ($form_data['stripeToken']) {
197
            $cust_params['source'] = $form_data['stripeToken'];
198
        }
199
200
        # At the point the customer is created, details such as postcode and
201
        # security code can be checked, and therefore fail
202
        try {
203
            $obj = $this->api->createCustomer($cust_params);
204
        } catch (\Stripe\Exception\CardException $e) {
205
            $body = $e->getJsonBody();
206
            $err  = $body['error'];
207
            $error = 'Sorry, we could not process your payment, please try again. ';
208
            $error .= 'Our payment processor returned: ' . $err['message'];
209
            unset($_POST['stripeToken']); # So card form is shown again
210
            return [ $error ];
211
        }
212
213
        $customer = $obj->id;
214
215
        if (!$form_data['stripeToken'] && !($form_data['plan'] == $this::$plans[0] && $form_data['coupon'] == 'charitable100')) {
216
            exit(1); # Should never reach here!
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
217
        }
218
219
        $obj = $this->api->createSubscription([
220
            'payment_behavior' => 'allow_incomplete',
221
            'expand' => ['latest_invoice.payment_intent'],
222
            'default_tax_rates' => [STRIPE_TAX_RATE],
0 ignored issues
show
Bug introduced by
The constant MySociety\TheyWorkForYou\STRIPE_TAX_RATE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
223
            'customer' => $customer,
224
            'plan' => $form_data['plan'],
225
            'coupon' => $form_data['coupon'],
226
            'metadata' => $form_data['metadata'],
227
        ]);
228
        $stripe_id = $obj->id;
229
230
        $this->db->query('INSERT INTO api_subscription (user_id, stripe_id) VALUES (:user_id, :stripe_id)', [
231
            ':user_id' => $this->user->user_id(),
232
            ':stripe_id' => $stripe_id,
233
        ]);
234
    }
235
236
    public function invoices() {
237
        $invoices = $this->api->getInvoices([
238
            'subscription' => $this->stripe->id,
239
            'limit' => 24,
240
        ]);
241
        $invoices = $invoices->data;
242
        return $invoices;
243
    }
244
245
    private function getFields() {
246
        $fields = ['plan', 'charitable_tick', 'charitable', 'charity_number', 'description', 'tandcs_tick', 'stripeToken', 'payment_method'];
247
        $this->form_data = [];
0 ignored issues
show
Bug Best Practice introduced by
The property form_data does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
248
        foreach ($fields as $field) {
249
            $this->form_data[$field] = get_http_var($field);
250
        }
251
    }
252
253
    private function checkValidPlan() {
254
        return ($this->form_data['plan'] && in_array($this->form_data['plan'], $this::$plans));
255
    }
256
257
    private function checkPaymentGivenIfNeeded() {
258
        $payment_data = $this->form_data['stripeToken'] || $this->form_data['payment_method'];
259
        return ($this->has_payment_data || $payment_data || (
260
            $this->form_data['plan'] == $this::$plans[0]
261
                && in_array($this->form_data['charitable'], ['c', 'i'])
262
        ));
263
    }
264
265
    public function checkForErrors() {
266
        $this->getFields();
267
        $form_data = &$this->form_data;
268
269
        $errors = [];
270
        if ($form_data['charitable'] && !in_array($form_data['charitable'], ['c', 'i', 'o'])) {
271
            $form_data['charitable'] = '';
272
        }
273
274
        if (!$this->checkValidPlan()) {
275
            $errors[] = 'Please pick a plan';
276
        }
277
278
        if (!$this->checkPaymentGivenIfNeeded()) {
279
            $errors[] = 'You need to submit payment';
280
        }
281
282
        if (!$this->stripe && !$form_data['tandcs_tick']) {
283
            $errors[] = 'Please agree to the terms and conditions';
284
        }
285
286
        if (!$form_data['charitable_tick']) {
287
            $form_data['charitable'] = '';
288
            $form_data['charity_number'] = '';
289
            $form_data['description'] = '';
290
            return $errors;
291
        }
292
293
        if ($form_data['charitable'] == 'c' && !$form_data['charity_number']) {
294
            $errors[] = 'Please provide your charity number';
295
        }
296
        if ($form_data['charitable'] == 'i' && !$form_data['description']) {
297
            $errors[] = 'Please provide details of your project';
298
        }
299
300
        return $errors;
301
    }
302
303
    public function createOrUpdateFromForm() {
304
        $form_data = $this->form_data;
305
306
        $form_data['coupon'] = null;
307
        if (in_array($form_data['charitable'], ['c', 'i'])) {
308
            $form_data['coupon'] = 'charitable50';
309
            if ($form_data['plan'] == $this::$plans[0]) {
310
                $form_data['coupon'] = 'charitable100';
311
            }
312
        }
313
314
        $form_data['metadata'] = [
315
            'charitable' => $form_data['charitable'],
316
            'charity_number' => $form_data['charity_number'],
317
            'description' => $form_data['description'],
318
        ];
319
320
        if ($this->stripe) {
321
            $this->update_subscription($form_data);
322
        } else {
323
            return $this->add_subscription($form_data);
324
        }
325
    }
326
327
    public function redis_update_max($plan) {
328
        preg_match('#^twfy-(\d+)k#', $plan, $m);
329
        $max = $m[1] * 1000;
330
        $this->redis->set("$this->redis_prefix:max", $max);
0 ignored issues
show
Bug introduced by
The method set() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

330
        $this->redis->/** @scrutinizer ignore-call */ 
331
                      set("$this->redis_prefix:max", $max);
Loading history...
331
        $this->redis->del("$this->redis_prefix:blocked");
0 ignored issues
show
Bug introduced by
The method del() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

331
        $this->redis->/** @scrutinizer ignore-call */ 
332
                      del("$this->redis_prefix:blocked");
Loading history...
332
    }
333
334
    public function redis_reset_quota() {
335
        $count = $this->redis->getset("$this->redis_prefix:count", 0);
0 ignored issues
show
Bug introduced by
The method getset() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

335
        /** @scrutinizer ignore-call */ 
336
        $count = $this->redis->getset("$this->redis_prefix:count", 0);
Loading history...
336
        if ($count !== null) {
337
            $this->redis->rpush("$this->redis_prefix:history", $count);
0 ignored issues
show
Bug introduced by
The method rpush() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

337
            $this->redis->/** @scrutinizer ignore-call */ 
338
                          rpush("$this->redis_prefix:history", $count);
Loading history...
338
        }
339
        $this->redis->del("$this->redis_prefix:blocked");
340
    }
341
342
    public function delete_from_redis() {
343
        $this->redis->del("$this->redis_prefix:max");
344
        $this->redis->del("$this->redis_prefix:count");
345
        $this->redis->del("$this->redis_prefix:blocked");
346
    }
347
348
    public function quota_status() {
349
        return [
350
            'count' => floor($this->redis->get("$this->redis_prefix:count")),
0 ignored issues
show
Bug introduced by
The method get() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

350
            'count' => floor($this->redis->/** @scrutinizer ignore-call */ get("$this->redis_prefix:count")),
Loading history...
351
            'blocked' => floor($this->redis->get("$this->redis_prefix:blocked")),
352
            'quota' => floor($this->redis->get("$this->redis_prefix:max")),
353
            'history' => $this->redis->lrange("$this->redis_prefix:history", 0, -1),
0 ignored issues
show
Bug introduced by
The method lrange() does not exist on MySociety\TheyWorkForYou\Redis. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

353
            'history' => $this->redis->/** @scrutinizer ignore-call */ lrange("$this->redis_prefix:history", 0, -1),
Loading history...
354
        ];
355
    }
356
}
357