Failed Conditions
Push — master ( 3150f9...0edb2e )
by Sébastien
02:24
created

ContredanseProductAccess   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 181
Duplicated Lines 0 %

Test Coverage

Coverage 60%

Importance

Changes 0
Metric Value
wmc 12
eloc 59
dl 0
loc 181
ccs 33
cts 55
cp 0.6
rs 10
c 0
b 0
f 0

3 Methods

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