Passed
Push — master ( a70ee3...b8386d )
by Sébastien
02:12
created

ContredanseProductAccess::ensureAccess()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 55
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 7.0018

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 55
ccs 29
cts 30
cp 0.9667
rs 8.5066
c 0
b 0
f 0
cc 7
nc 7
nop 2
crap 7.0018

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 7
    public function ensureAccess(string $productName, UserInterface $user): void
68
    {
69
70 7
		if (in_array('admin', (array) $user->getRoles(), true)) {
71
			return;
72
		}
73
74 7
		$email = $user->getDetail('email');
75
76 7
        $orders = $this->getProductOrders($productName, $email);
77
78 6
        if (count($orders) === 0) {
79
            // Cool, he never bought anything
80 1
            throw new NoProductAccessException(sprintf(
81 1
                sprintf('No access, product "%s" have not yet been ordered', $productName)
82
            ));
83
        }
84
85
        // Pick the most recent order
86
87 5
        $order = $orders[0];
88
89
        // Is there a payment issue ?
90
91 5
        if ((int) $order['pay_status'] !== self::VALID_PAY_STATUS) {
92 1
            throw new ProductPaymentIssueException(sprintf(
93 1
                sprintf(
94 1
                    'Look we have a payment issue, pay_status code in order detail %s is %s',
95 1
                    $order['detail_id'],
96 1
                    $order['pay_status']
97
                )
98
            ));
99
        }
100
101
        // Check expiration if any given
102 4
        if (trim($order['expires_at'] ?? '') !== '') {
103
            try {
104 4
                $expiresAt = Chronos::createFromFormat('Y-m-d H:i:s', $order['expires_at']);
105 1
            } catch (\Throwable $e) {
106 1
                throw new UnsupportedExpiryFormatException(
107 1
                    sprintf(
108 1
                        'Unexpected product expiry data (%s) for order detail %s. (%s)',
109 1
                        $order['expires_at'],
110 1
                        $order['detail_id'],
111 1
                        $e->getMessage()
112
                    )
113
                );
114
            }
115
116 3
            if ($expiresAt->isPast()) {
117 1
                throw new ProductAccessExpiredException(sprintf(
118 1
                    sprintf(
119 1
                        'Product access have expired on %s (see order detail_id %s)',
120 1
                        $expiresAt->format('Y-m-d'),
121 1
                        $order['detail_id']
122
                    )
123
                ));
124
            }
125
        }
126 2
    }
127
128
    /**
129
     * Get user orders relative to a certain product.
130
     *
131
     * @param string $productName see constants self::PAXTON_PRODUCT
132
     * @param string $email       identity of the user to check for product access
133
     *
134
     * @return array<int, mixed[]>
135
     *
136
     * @throws MissingProductConfigException
137
     * @throws UnsupportedProductException
138
     * @throws QueryErrorException
139
     */
140 1
    public function getProductOrders(string $productName, string $email): array
141
    {
142 1
        if (!in_array($productName, self::SUPPORTED_PRODUCTS, true)) {
143 1
            throw new UnsupportedProductException(sprintf(
144 1
                'Product name %s is not supported',
145 1
                $productName
146
            ));
147
        }
148
149
        if (!array_key_exists($productName, $this->productAccess)) {
150
            throw new MissingProductConfigException(sprintf(
151
                'Missing configuration: product %s does not have associated ids',
152
                $productName
153
            ));
154
        }
155
156
        $productIds = $this->productAccess[$productName];
157
158
        $holderValues = [];
159
        foreach ($productIds as $idx => $productId) {
160
            $holderValues[":product_id_$idx"] = (int) $productId;
161
        }
162
        $inParams = implode(',', array_keys($holderValues));
163
164
        $sql = "		
165
			SELECT 
166
				s.suj_id AS subject_id,
167
				l.Login AS email,
168
				o.order_id,
169
				o.total_value,
170
				o.total_pay,
171
				o.pay_status,
172
				DATE_FORMAT(FROM_UNIXTIME(o.cre_dt),
173
						'%Y-%m-%d %H:%i:%s') AS order_created_at,
174
			    d.detail_id,  
175
				d.product_id,
176
				d.support_id,
177
				d.quantity,
178
				DATE_FORMAT(FROM_UNIXTIME(d.cre_dt),
179
						'%Y-%m-%d %H:%i:%s') AS line_created_at,
180
				DATE_FORMAT(FROM_UNIXTIME(d.expiry_dt),
181
						'%Y-%m-%d %H:%i:%s') AS expires_at
182
			FROM
183
				shop_order o
184
					INNER JOIN
185
				shop_order_detail d ON d.order_id = o.order_id
186
					INNER JOIN
187
				sujet s ON s.suj_id = o.suj_id
188
					INNER JOIN
189
				usr_login l ON l.suj_id = s.suj_id
190
			WHERE 
191
			          l.Login = :email
192
				  AND d.product_id in (${inParams})	
193
			ORDER BY d.expiry_dt desc
194
		";
195
196
        $stmt = $this->adapter->prepare($sql);
197
        $stmt->execute(array_merge([
198
            ':email' => $email,
199
        ], $holderValues));
200
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
201
        if ($rows === false) {
202
            throw new QueryErrorException('Cannot get users');
203
        }
204
205
        return $rows;
206
    }
207
}
208