Skip to content

Commit 227f88e

Browse files
TsankoBgapeschar
andauthored
Consent flow (#5)
* WIP * 'system_name' -> $system_name * Add Module::getSystemKey * WIP * Test on latest version * WIP * Add getCurl * Use `Module::request` * Fix getting config values * trustprofile.io -> trustprofile.com * orderConfirmation -> displayOrderConfirmation * Add hasConsent * Fix consent URL * Fix erros * Add timeout * Small improvements * Remove phone_numbers * Use http_build_query for CONSENT_URL * http_build_query invite post data * Fix empty product commend id * Fix delete product comment * Handle deleted product comment * Move check only when ID is provided * Add empty line at the end of files * Remove unnecessary `return` * Update common/src/Module.php Co-authored-by: Albert Peschar <albert@peschar.net> * Update common/src/Module.php Co-authored-by: Albert Peschar <albert@peschar.net> * Update common/src/Module.php Co-authored-by: Albert Peschar <albert@peschar.net> * Update common/src/Sync.php Co-authored-by: Albert Peschar <albert@peschar.net> * Update common/src/Module.php Co-authored-by: Albert Peschar <albert@peschar.net> * Move curl_init failed to getCurl --------- Co-authored-by: Albert Peschar <albert@peschar.net>
1 parent ceed5b1 commit 227f88e

File tree

8 files changed

+151
-80
lines changed

8 files changed

+151
-80
lines changed

Dockerfile

Whitespace-only changes.

common/src/Module.php

Lines changed: 112 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@ abstract protected function getDisplayName();
2121

2222
/** @return string */
2323
abstract protected function getDashboardDomain();
24+
25+
/** @return string */
26+
abstract protected function getSystemKey();
27+
28+
private $curl;
29+
2430
const SYNC_URL = 'https://%s/webshops/sync_url';
2531

32+
const CONSENT_URL = 'https://%s/api/2.0/order_permissions.json?%s';
33+
2634
public function __construct() {
2735
$this->name = $this->getName();
2836
$this->tab = 'pricing_promotion';
@@ -96,38 +104,16 @@ private function sendSyncUrl(): void {
96104
'api_key' => Configuration::get($this->getConfigName('API_KEY')),
97105
'url' => Context::getContext()->link->getModuleLink($this->getName(), 'sync'),
98106
];
99-
try {
100-
$this->doSendSyncUrl($url, $data);
101-
} catch (\Exception $e) {
102-
PrestaShopLogger::addLog(sprintf('Sending sync URL to Dashboard failed with error %s', $e->getMessage()));
103-
}
104-
}
105-
106-
/**
107-
* @throws \Exception
108-
*/
109-
private function doSendSyncUrl(string $url, array $data): void {
110-
$curl = curl_init();
111107
$options = [
112-
CURLOPT_RETURNTRANSFER => true,
113-
CURLOPT_FAILONERROR => true,
114-
CURLOPT_FOLLOWLOCATION => true,
115-
CURLOPT_POST => true,
116108
CURLOPT_POSTFIELDS => json_encode($data),
117109
CURLOPT_HTTPHEADER => ['Content-Type:application/json'],
118-
CURLOPT_URL => $url,
119110
CURLOPT_TIMEOUT => 10,
120111
];
121-
if (!curl_setopt_array($curl, $options)) {
122-
throw new \Exception('Could not set cURL options');
123-
}
124-
125-
$response = curl_exec($curl);
126-
if ($response === false) {
127-
throw new \Exception(sprintf('(%s) %s', curl_errno($curl), curl_error($curl)));
112+
try {
113+
$this->request($url, 'POST', $options);
114+
} catch (\Exception $e) {
115+
PrestaShopLogger::addLog(sprintf('Sending sync URL to Dashboard failed with error %s', $e->getMessage()));
128116
}
129-
130-
curl_close($curl);
131117
}
132118

133119
private function execSQL($query) {
@@ -156,7 +142,7 @@ public function uninstall() {
156142
return parent::uninstall();
157143
}
158144

159-
public function hookHeader($params) {
145+
public function hookDisplayHeader($params) {
160146
if (!Configuration::get($this->getConfigName('JAVASCRIPT'))) {
161147
return "<!-- {$this->getDisplayName()}: JS disabled -->\n";
162148
}
@@ -175,7 +161,27 @@ public function hookHeader($params) {
175161
]);
176162
}
177163

178-
public function hookFooter($params) {
164+
165+
public function hookDisplayOrderConfirmation($params) {
166+
$order = $params['order'];
167+
$customer = new \Customer((int) $order->id_customer);
168+
$ps_shop_id = $order->id_shop;
169+
$webshop_id = Configuration::get($this->getConfigName('SHOP_ID'), null, null, $ps_shop_id);
170+
171+
return $this->render('consent_data', [
172+
'system_key' => $this->getSystemKey(),
173+
'consent_flow_enabled' => (int) Configuration::get($this->getConfigName('INVITE'), null, null, $ps_shop_id) == 3,
174+
'consent_data' => json_encode([
175+
'webshopId' => $webshop_id,
176+
'orderNumber' => $order->id,
177+
'email' => $customer->email,
178+
'firstName' => $customer->firstname,
179+
'inviteDelay' => (int) Configuration::get($this->getConfigName('INVITE_DELAY'), null, null, $ps_shop_id),
180+
]),
181+
]);
182+
}
183+
184+
public function hookDisplayFooter($params) {
179185
if (!Configuration::get($this->getConfigName('RICH_SNIPPET'))
180186
|| !($shop_id = Configuration::get($this->getConfigName('SHOP_ID')))
181187
|| !ctype_digit($shop_id)
@@ -215,7 +221,7 @@ private function getRichSnippet($shop_id) {
215221
$data = json_decode($json, true);
216222
} else {
217223
try {
218-
$json = $this->request($url, [
224+
$json = $this->request($url, 'GET', [
219225
CURLOPT_CONNECTTIMEOUT => 2,
220226
CURLOPT_TIMEOUT => 4,
221227
]);
@@ -354,22 +360,26 @@ private function sendInvites($ps_shop_id) {
354360
}
355361

356362
foreach ($orders as $order) {
363+
if (
364+
Configuration::get($this->getConfigName('INVITE'), null, null, $ps_shop_id) == 3
365+
&& !$this->hasConsent($order['id_order'], $ps_shop_id)
366+
) {
367+
$this->markInviteAsSent($order['id_order']);
368+
PrestaShopLogger::addLog(
369+
sprintf('Invitation was not created for order (%s) as customer did not consent', $order['id_order']),
370+
);
371+
return;
372+
}
373+
357374
$invoice_address = $this->getOrderAddress($db, $order['id_address_invoice'])[0];
358375
$delivery_address = $this->getOrderAddress($db, $order['id_address_delivery'])[0];
359-
$phones = array_unique(array_filter([
360-
$invoice_address['phone'],
361-
$invoice_address['phone_mobile'],
362-
$delivery_address['phone'],
363-
$delivery_address['phone_mobile'],
364-
]));
365376

366377
$post = [
367378
'email' => $order['email'],
368379
'order' => $order['id_order'],
369380
'delay' => $invite_delay,
370381
'language' => str_replace('-', '_', $order['language_code']),
371382
'customer_name' => $order['firstname'] . ' ' . $order['lastname'],
372-
'phone_numbers' => $phones,
373383
'order_total' => $order['total_paid'],
374384
'client' => 'prestashop',
375385
'platform_version' => _PS_VERSION_,
@@ -412,8 +422,9 @@ private function sendInvites($ps_shop_id) {
412422
'code' => $api_key,
413423
]);
414424

415-
$response = $this->request($url, [
416-
CURLOPT_POSTFIELDS => $post,
425+
$response = $this->request($url, 'POST', [
426+
CURLOPT_POSTFIELDS => http_build_query($post),
427+
CURLOPT_TIMEOUT => 10,
417428
]);
418429

419430
$data = json_decode($response, true);
@@ -426,7 +437,7 @@ private function sendInvites($ps_shop_id) {
426437
throw new RuntimeException($data['message']);
427438
}
428439

429-
$db->execute("UPDATE `{$this->getTableName('orders')}` SET {$this->getPluginColumnName('invite_sent')} = 1 WHERE id_order = " . (int) $order['id_order']);
440+
$this->markInviteAsSent($order['id_order']);
430441

431442
PrestaShopLogger::addLog(sprintf(
432443
'%s: Requested invitation for order %s',
@@ -469,7 +480,7 @@ private function getProductImage($product): string {
469480
return str_replace('http://', Tools::getShopProtocol(), $context->link->getImageLink($product->link_rewrite, $img['id_image'], 'home_default'));
470481
}
471482

472-
public function hookBackofficeTop() {
483+
public function hookDisplayBackOfficeTop() {
473484
foreach (Shop::getCompleteListOfShopsID() as $shop) {
474485
$this->sendInvites($shop);
475486
}
@@ -497,9 +508,7 @@ public function getContent() {
497508
Configuration::updateValue($this->getConfigName('SHOP_ID'), trim($shop_id));
498509
Configuration::updateValue($this->getConfigName('API_KEY'), trim($api_key));
499510
Configuration::updateValue($this->getConfigName('SYNC_PROD_REVIEWS'), (bool) Tools::getValue('sync_prod_reviews'));
500-
if (Configuration::get($this->getConfigName('SYNC_PROD_REVIEWS'))) {
501-
$this->sendSyncUrl();
502-
}
511+
$this->sendSyncUrl();
503512

504513
Configuration::updateValue(
505514
$this->getConfigName('INVITE'),
@@ -533,9 +542,10 @@ public function getContent() {
533542
!!Tools::getValue('limit_order_data')
534543
);
535544

536-
$this->registerHook('header');
537-
$this->registerHook('footer');
538-
$this->registerHook('backOfficeTop');
545+
$this->registerHook('displayHeader');
546+
$this->registerHook('displayFooter');
547+
$this->registerHook('displayOrderConfirmation');
548+
$this->registerHook('displayBackOfficeTop');
539549

540550
if (sizeof($errors) == 0) {
541551
$success = true;
@@ -612,30 +622,22 @@ private function getPluginTableName($name) {
612622
return $this->getTableName($this->getName() . '_' . $name);
613623
}
614624

615-
/**
616-
* @param string $url
617-
* @param array $options
618-
* @return string
619-
*/
620-
private function request($url, $options = []) {
621-
$ch = curl_init($url);
622-
if (!$ch) {
623-
throw new RuntimeException('curl_init failed');
624-
}
625-
$options += [
625+
private function request(string $url, string $method, array $options = []): string {
626+
$default_options = [
627+
CURLOPT_URL => $url,
626628
CURLOPT_FAILONERROR => true,
627629
CURLOPT_RETURNTRANSFER => true,
628630
CURLOPT_FOLLOWLOCATION => true,
631+
CURLOPT_CUSTOMREQUEST => $method,
629632
];
630-
if (!curl_setopt_array($ch, $options)) {
631-
throw new RuntimeException('curl_setopt_array failed');
632-
}
633+
$ch = $this->getCurl($default_options + $options);
634+
633635
$response = curl_exec($ch);
634636
if ($response === false) {
635637
throw new RuntimeException(sprintf(
636638
'curl: (%s) %s',
637639
curl_errno($ch),
638-
curl_error($ch)
640+
curl_error($ch),
639641
));
640642
}
641643
return $response;
@@ -649,4 +651,53 @@ private function render($__template, array $__scope) {
649651
return ob_get_clean();
650652
})();
651653
}
654+
655+
private function getCurl(array $options) {
656+
if (!$this->curl) {
657+
$this->curl = curl_init();
658+
} else {
659+
curl_reset($this->curl);
660+
}
661+
662+
if (!curl_setopt_array($this->curl, $options)) {
663+
throw new RuntimeException('curl_setopt_array failed');
664+
}
665+
666+
if (!$this->curl) {
667+
throw new RuntimeException('curl_init failed');
668+
}
669+
670+
return $this->curl;
671+
}
672+
673+
private function hasConsent(int $order_id, int $ps_shop_id): bool {
674+
$url = sprintf(
675+
self::CONSENT_URL,
676+
$this->getDashboardDomain(),
677+
http_build_query([
678+
'id' => Configuration::get($this->getConfigName('SHOP_ID'), null, null, $ps_shop_id),
679+
'code' => Configuration::get($this->getConfigName('API_KEY'), null, null, $ps_shop_id),
680+
'orderNumber' => $order_id,
681+
]),
682+
);
683+
684+
try {
685+
$response_data = json_decode($this->request($url, 'GET'), true);
686+
} catch (\Exception $e) {
687+
$message = sprintf(
688+
'Checking consent for order %s failed: %s',
689+
$order_id,
690+
$e->getMessage(),
691+
);
692+
PrestaShopLogger::addLog($message);
693+
return false;
694+
}
695+
696+
return $response_data['has_consent'] ?? false;
697+
}
698+
699+
private function markInviteAsSent(int $order_id): void {
700+
$db = Db::getInstance();
701+
$db->execute("UPDATE `{$this->getTableName('orders')}` SET {$this->getPluginColumnName('invite_sent')} = 1 WHERE id_order = " . $order_id);
702+
}
652703
}

common/src/Sync.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,16 @@ private function syncProductReview(array $product_review): void {
5353
$entity_manager = $this->container->get('doctrine.orm.entity_manager');
5454
$date_add = DateTime::createFromFormat('Y-m-d H:i:s', $product_review['created']);
5555

56-
$product_comment_repository = $entity_manager->getRepository(ProductComment::class);
57-
$product_comment = $product_review['id'] ? $product_comment_repository->find($product_review['id']) : new ProductComment();
56+
if ($product_review['id']) {
57+
$product_comment_repository = $entity_manager->getRepository(ProductComment::class);
58+
$product_comment = $product_comment_repository->find($product_review['id']);
59+
if (!$product_comment) {
60+
$this->returnResponseCode(404, sprintf('Could not find product review with ID %d', $product_review['id']));
61+
}
62+
} else {
63+
$product_comment = new ProductComment();
64+
}
65+
5866
$product_comment->setProductId($product_review['product_id'])
5967
->setCustomerId($this->getCustomerIdByEmail($product_review['reviewer']['email']) ?? 0)
6068
->setGuestId(0)
@@ -63,14 +71,15 @@ private function syncProductReview(array $product_review): void {
6371
->setCustomerName($product_review['reviewer']['name'])
6472
->setGrade($product_review['rating'])
6573
->setValidate(1)
66-
->setDateAdd($date_add);
74+
->setDateAdd($date_add)
75+
->setDeleted($product_review['deleted']);
6776

6877
if (!$product_review['id']) {
6978
$entity_manager->persist($product_comment);
7079
}
7180
$entity_manager->flush();
7281
$this->logReviewSync($product_review['id'] ?? $product_comment->getId(), $product_review['deleted']);
73-
$this->ajaxRender(json_encode(['review_id' => $product_review['id']] ?? $product_comment->getId(), JSON_PARTIAL_OUTPUT_ON_ERROR));
82+
$this->ajaxRender(json_encode(['review_id' => $product_comment->getId()]));
7483
}
7584

7685
private function getCustomerIdByEmail(string $email): ?int {

common/templates/config_form.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@
7878
<input type="radio" name="invite" id="webwinkelkeur-invite-on" value="1" <?= $module->getConfigValue('INVITE') == 1 ? 'checked' : ''; ?>>
7979
<label class="t" for="webwinkelkeur-invite-on"><?= $module->l('Yes, for every order', 'config_form'); ?></label><br>
8080

81+
<label class="t" for="webwinkelkeur-consent-flow">
82+
<img src="../img/admin/enabled.gif" alt="">
83+
</label>
84+
<input type="radio" name="invite" id="webwinkelkeur-consent-flow" value="3" <?= $module->getConfigValue('INVITE') == 3 ? 'checked' : ''; ?>>
85+
<label class="t" for="webwinkelkeur-consent-flow"><?= $module->l('Yes, pop-up after purchase at "thank you" page.', 'config_form'); ?></label><br>
86+
8187
<label class="t" for="webwinkelkeur-invite-first">
8288
<img src="../img/admin/enabled.gif" alt="">
8389
</label>

common/templates/consent_data.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php if (!empty($consent_flow_enabled)): ?>
2+
<script id="<?= $system_key; ?>_order_completed">
3+
<?= $consent_data; ?>
4+
</script>
5+
<?php endif; ?>
6+

docker-compose.yml

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ services:
88
- MARIADB_USER=bn_prestashop
99
- MARIADB_DATABASE=bitnami_prestashop
1010
volumes:
11-
- './db:/bitnami/mariadb'
12-
ports:
13-
- '19966:3306'
11+
- 'mariadb_data:/bitnami/mariadb'
1412
prestashop:
15-
image: docker.io/bitnami/prestashop:1.7
13+
image: docker.io/bitnami/prestashop:8
1614
ports:
17-
- '80:8080'
18-
- '443:8443'
15+
- '19966:8080'
16+
- '19967:8443'
1917
environment:
2018
- PRESTASHOP_HOST=localhost
2119
- PRESTASHOP_DATABASE_HOST=mariadb
@@ -24,16 +22,9 @@ services:
2422
- PRESTASHOP_DATABASE_NAME=bitnami_prestashop
2523
# ALLOW_EMPTY_PASSWORD is recommended only for development.
2624
- ALLOW_EMPTY_PASSWORD=yes
27-
volumes:
28-
- 'prestashop_data:/bitnami/prestashop'
29-
- '/Volumes/Work/prestashop/html:/bitnami/prestashop'
30-
- './common:/bitnami/prestashop/modules/common'
31-
- './trustprofile:/bitnami/prestashop/modules/trustprofile'
32-
- './webwinkelkeur:/bitnami/prestashop/modules/webwinkelkeur'
3325
depends_on:
3426
- mariadb
3527
volumes:
3628
mariadb_data:
3729
driver: local
38-
prestashop_data:
39-
driver: local
30+

trustprofile/trustprofile.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ protected function getDisplayName() {
1212
}
1313

1414
protected function getDashboardDomain() {
15-
return 'dashboard.trustprofile.io';
15+
return 'dashboard.trustprofile.com';
16+
}
17+
18+
protected function getSystemKey() {
19+
return 'trustprofile';
1620
}
1721
}

0 commit comments

Comments
 (0)