ContredanseProductAccess::ensureAccess()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 56
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 7.0016

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 31
c 2
b 0
f 0
nc 7
nop 2
dl 0
loc 56
ccs 30
cts 31
cp 0.9677
crap 7.0016
rs 8.4906

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Security;
6
7
use App\Security\Exception\MissingProductConfigException;
8
use App\Security\Exception\NoProductAccessException;
9
use App\Security\Exception\ProductAccessExpiredException;
10
use App\Security\Exception\ProductPaymentIssueException;
11
use App\Security\Exception\QueryErrorException;
12
use App\Security\Exception\UnsupportedExpiryFormatException;
13
use App\Security\Exception\UnsupportedProductException;
14
use Cake\Chronos\Chronos;
15
use Zend\Expressive\Authentication\UserInterface;
16
17
class ContredanseProductAccess
18
{
19
    /**
20
     * VALID PAY_STATUS CODE AT CONTREDANSE.
21
     */
22
    public const VALID_PAY_STATUS = 9;
23
24
    public const PAXTON_PRODUCT = 'product:paxton';
25
26
    public const SUPPORTED_PRODUCTS = [
27
        self::PAXTON_PRODUCT
28
    ];
29
30
    /**
31
     * @var \PDO
32
     */
33
    private $adapter;
34
35
    /**
36
     * @var array<string,string[]>
37
     */
38
    private $productAccess;
39
40
    /**
41
     * @param array<string,string[]> $productAccess
42
     */
43
    public function __construct(\PDO $adapter, array $productAccess)
44
    {
45
        $this->adapter       = $adapter;
46
        $this->productAccess = $productAccess;
47
    }
48
49
    /**
50
     * Ensure that a product (paxton) is available to the user.
51
     *
52
     * @param string $productName see constants self::PAXTON_PRODUCT
53
     *
54
     * Those exceptions can be considered as system/config errors
55
     *
56
     * @throws QueryErrorException
57
     * @throws MissingProductConfigException
58
     * @throws UnsupportedProductException
59
     * @throws UnsupportedExpiryFormatException
60
     *
61
     * Those exceptions implements ProductAccessExceptionInterface
62
     * and can be used to determine the exact cause of failure
63
     * @throws NoProductAccessException
64
     * @throws ProductPaymentIssueException
65
     * @throws ProductAccessExpiredException
66
     */
67 8
    public function ensureAccess(string $productName, UserInterface $user): void
68
    {
69 8
        if (in_array('admin', (array) $user->getRoles(), true)) {
70
            return;
71
        }
72
73 8
        $email = $user->getDetail('email');
74
75 8
        $orders = $this->getProductOrders($productName, $email);
76
77 7
        if (count($orders) === 0) {
78
            // Cool, he never bought anything
79 1
            throw new NoProductAccessException(sprintf(
80 1
                sprintf('No access, product "%s" have not yet been ordered', $productName)
81
            ));
82
        }
83
84
        // Pick the most recent order
85
86 6
        $order = $orders[0];
87
88
        // Is there a payment issue ?
89
90 6
        if ((int) $order['pay_status'] !== self::VALID_PAY_STATUS) {
91 1
            throw new ProductPaymentIssueException(sprintf(
92 1
                sprintf(
93 1
                    'Look we have a payment issue, pay_status code in order detail %s is %s',
94 1
                    $order['detail_id'],
95 1
                    $order['pay_status']
96
                )
97
            ));
98
        }
99
100
        // Check expiration if any given
101 5
        if (trim($order['expires_at'] ?? '') !== '') {
102
            try {
103 5
                $expiresAt = Chronos::createFromFormat('Y-m-d H:i:s', $order['expires_at']);
104 1
            } catch (\Throwable $e) {
105 1
                throw new UnsupportedExpiryFormatException(
106 1
                    sprintf(
107 1
                        'Unexpected product expiry data (%s) for order detail %s. (%s)',
108 1
                        $order['expires_at'],
109 1
                        $order['detail_id'],
110 1
                        $e->getMessage()
111
                    )
112
                );
113
            }
114
115 4
            if ($expiresAt->isPast()) {
116 2
                throw new ProductAccessExpiredException(sprintf(
117 2
                    sprintf(
118 2
                        'Product access have expired on %s (see order detail_id %s)',
119 2
                        $expiresAt->format('Y-m-d'),
120 2
                        $order['detail_id']
121
                    )
122 2
                ), $expiresAt);
123
            }
124
        }
125 2
    }
126
127
    /**
128
     * Get user orders relative to a certain product.
129
     *
130
     * @param string $productName see constants self::PAXTON_PRODUCT
131
     * @param string $email       identity of the user to check for product access
132
     *
133
     * @return array<int, mixed[]>
134
     *
135
     * @throws MissingProductConfigException
136
     * @throws UnsupportedProductException
137
     * @throws QueryErrorException
138
     */
139 1
    public function getProductOrders(string $productName, string $email): array
140
    {
141 1
        if (!in_array($productName, self::SUPPORTED_PRODUCTS, true)) {
142 1
            throw new UnsupportedProductException(sprintf(
143 1
                'Product name %s is not supported',
144 1
                $productName
145
            ));
146
        }
147
148
        if (!array_key_exists($productName, $this->productAccess)) {
149
            throw new MissingProductConfigException(sprintf(
150
                'Missing configuration: product %s does not have associated ids',
151
                $productName
152
            ));
153
        }
154
155
        $productIds = $this->productAccess[$productName];
156
157
        $holderValues = [];
158
        foreach ($productIds as $idx => $productId) {
159
            $holderValues[":product_id_$idx"] = (int) $productId;
160
        }
161
        $inParams = implode(',', array_keys($holderValues));
162
163
        $sql = "		
164
			SELECT 
165
				s.suj_id AS subject_id,
166
				l.Login AS email,
167
				o.order_id,
168
				o.total_value,
169
				o.total_pay,
170
				o.pay_status,
171
				DATE_FORMAT(FROM_UNIXTIME(o.cre_dt),
172
						'%Y-%m-%d %H:%i:%s') AS order_created_at,
173
			    d.detail_id,  
174
				d.product_id,
175
				d.support_id,
176
				d.quantity,
177
				DATE_FORMAT(FROM_UNIXTIME(d.cre_dt),
178
						'%Y-%m-%d %H:%i:%s') AS line_created_at,
179
				DATE_FORMAT(FROM_UNIXTIME(d.expiry_dt),
180
						'%Y-%m-%d %H:%i:%s') AS expires_at
181
			FROM
182
				shop_order o
183
					INNER JOIN
184
				shop_order_detail d ON d.order_id = o.order_id
185
					INNER JOIN
186
				sujet s ON s.suj_id = o.suj_id
187
					INNER JOIN
188
				usr_login l ON l.suj_id = s.suj_id
189
			WHERE 
190
			          LOWER(l.Login) = :email
191
				  AND d.support_id in (${inParams})	
192
			ORDER BY d.expiry_dt desc
193
		";
194
195
        $stmt = $this->adapter->prepare($sql);
196
        $stmt->execute(array_merge([
197
            ':email' => mb_strtolower(trim($email)),
198
        ], $holderValues));
199
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
200
        if ($rows === false) {
201
            throw new QueryErrorException('Cannot get users');
202
        }
203
204
        return $rows;
205
    }
206
}
207