ContredanseProductAccess::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
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