From 9ac9680943113ff76e16a4b8976f3dc09ef4ea25 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 21 Feb 2023 14:19:16 +0200 Subject: [PATCH 01/81] Removed previous Shopify logic --- README.md | 6 +- .../v1/models/mappers/ShopifyMapper.php | 78 - common/config/bootstrap.php | 1 - common/config/params.php | 6 - common/models/shopify/Shopify.php | 63 - common/models/shopify/Webhook.php | 50 - common/services/ecommerce/ShopifyService.php | 138 -- composer.json | 1 - composer.lock | 1451 +++++++---------- ..._115138_remove_previous_shopify_tables.php | 49 + docker-compose.yml | 10 - environments/index.php | 2 - .../models/forms/untested/ShopifyForm.php | 34 - shopify/config/bootstrap.php | 1 - tests/unit/ShopifyAdapterTester.php | 55 - 15 files changed, 606 insertions(+), 1339 deletions(-) delete mode 100644 api/modules/v1/models/mappers/ShopifyMapper.php delete mode 100644 common/models/shopify/Shopify.php delete mode 100644 common/models/shopify/Webhook.php delete mode 100644 common/services/ecommerce/ShopifyService.php create mode 100644 console/migrations/m230221_115138_remove_previous_shopify_tables.php delete mode 100644 frontend/models/forms/untested/ShopifyForm.php delete mode 100755 shopify/config/bootstrap.php delete mode 100644 tests/unit/ShopifyAdapterTester.php diff --git a/README.md b/README.md index b12d6a1d..131f28a6 100755 --- a/README.md +++ b/README.md @@ -144,8 +144,10 @@ Example of importing a gzipped mysql dump: 1. Install composer and run `composer install` on the root directory 1. Run `php init` for development -Frontend: localhost:30000 API: localhost:30001 MySQL: localhost:30002 PhpMyAdmin: localhost:30003 Shopify: localhost: -30004 +Frontend: localhost:30000 +API: localhost:30001 +MySQL: localhost:30002 +PhpMyAdmin: localhost:30003 ### Running queue jobs locally diff --git a/api/modules/v1/models/mappers/ShopifyMapper.php b/api/modules/v1/models/mappers/ShopifyMapper.php deleted file mode 100644 index 9430c6d8..00000000 --- a/api/modules/v1/models/mappers/ShopifyMapper.php +++ /dev/null @@ -1,78 +0,0 @@ - (string)$item['id'], - 'name' => $item['title'], - 'sku' => $item['sku'], - 'quantity' => $item['quantity'], - ]; - } - if (is_array($config['refunds'])) { - foreach ($config['refunds'] as $item) { - foreach ($item['refund_line_items'] as $kill) { - $uuidIsSet = $kill['line_item_id']; - if (array_key_exists($uuidIsSet, $items)) - unset($items[$uuidIsSet]); - } - } - } - - /** - * 1. foreach over refunds - * 2. foreach over refund_line-items - * 3. remove line items if it matches refund line item UUID - * - * - foreach, if - * - array_key_exists - * - unset() - * - if (is_array() { - */ - - /** @var Country $country */ - $country = Country::find()->where(['name' => $config['shipping_address']['country']])->one(); - - $shipAddress = [ - 'name' => $config['shipping_address']['name'], - 'company' => $config['shipping_address']['company'], - //'email' => $config[''][''], - 'address1' => $config['shipping_address']['address1'], - 'address2' => $config['shipping_address']['address2'], - 'city' => $config['shipping_address']['city'], - 'state' => $config['shipping_address']['province'], - 'zip' => $config['shipping_address']['zip'], - 'phone' => $config['shipping_address']['phone'], - 'country' => $country->abbreviation, - ]; - - - // Assign config var to Shopify request params - $config = [ - 'uuid' => (string)$config['id'], - 'notes' => $config['note'], - 'origin' => 'Shopify', - 'customerReference' => (string)$config['order_number'], - 'shipTo' => $shipAddress, - 'status' => StatusEx::OPEN, - 'items' => $items, - - ]; - - return $config; - } -} \ No newline at end of file diff --git a/common/config/bootstrap.php b/common/config/bootstrap.php index c247a077..25998c23 100755 --- a/common/config/bootstrap.php +++ b/common/config/bootstrap.php @@ -2,5 +2,4 @@ Yii::setAlias('@common', dirname(__DIR__)); Yii::setAlias('@api', dirname(dirname(__DIR__)) . '/api'); Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend'); -Yii::setAlias('@shopify', dirname(dirname(__DIR__)) . '/shopify'); Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console'); diff --git a/common/config/params.php b/common/config/params.php index ad498397..496ad838 100755 --- a/common/config/params.php +++ b/common/config/params.php @@ -7,8 +7,6 @@ 'user.passwordResetTokenExpire' => 3600, 'stripePublicKey' => '1234', 'stripePrivateKey' => '1234', - 'shopifyPublicKey' => '5678', - 'shopifyPrivateKey' => '5678', 'encryptionKey' => 'secret', 'digitalOceanKey' => 'key', 'digitalOceanSecret' => 'secret', @@ -18,10 +16,6 @@ 'clientId' => 'lolno', 'secret' => 'lulz', ], - /** - * Shopify App Parameters and App Credentials - */ - /** * Put here default values for when customers meta value does not exist diff --git a/common/models/shopify/Shopify.php b/common/models/shopify/Shopify.php deleted file mode 100644 index 630e631d..00000000 --- a/common/models/shopify/Shopify.php +++ /dev/null @@ -1,63 +0,0 @@ - 128], - [['shop'], 'unique'], - ]; - } - - /** - * {@inheritdoc} - */ - public function attributeLabels() - { - return [ - 'id' => 'ID', - 'customer_id' => 'Customer ID', - 'shop' => 'Shop', - 'scopes' => 'Scopes', - 'access_token' => 'Access Token', - 'created_date' => 'Created Date', - ]; - } - - /** - * @return \yii\db\ActiveQuery - */ - public function getCustomer() - { - return $this->hasOne('common\models\Customer', ['id' => 'customer_id']); - } -} diff --git a/common/models/shopify/Webhook.php b/common/models/shopify/Webhook.php deleted file mode 100644 index 3d095eb3..00000000 --- a/common/models/shopify/Webhook.php +++ /dev/null @@ -1,50 +0,0 @@ - 64], - ]; - } - - /** - * {@inheritdoc} - */ - public function attributeLabels() - { - return [ - 'id' => 'ID', - 'customer_id' => 'Customer ID', - 'shopify_webhook_id' => 'Shopify Webhook ID', - 'created_date' => 'Created Date', - ]; - } -} diff --git a/common/services/ecommerce/ShopifyService.php b/common/services/ecommerce/ShopifyService.php deleted file mode 100644 index 50644f1a..00000000 --- a/common/services/ecommerce/ShopifyService.php +++ /dev/null @@ -1,138 +0,0 @@ -key) - { - case self::META_URL: - $this->client = new Client(['baseUrl' => $meta->decryptedValue()]); - break; - case self::META_API_KEY: - $auth[0] = $meta->decryptedValue(); - break; - case self::META_API_SECRET: - $auth[1] = $meta->decryptedValue(); - break; - } - } - - // add semicolon to end for BASIC auth - $this->auth = base64_encode(implode(array: $auth, separator: ':')); - } - - public function getOrders(): array - { - $startDate = new \DateTime('-12 minutes', new \DateTimeZone('America/Chicago')); - - $orderarray = []; - - $page = $this->client->createRequest() - ->setMethod(method: 'GET') - ->setUrl(url: self::BASE_SHOPIFY_URL . 'orders.json') - ->setData([ - 'created_at_min' => $startDate->format(format: DATE_ISO8601), - 'status' => 'open', - 'fulfillment_status' => null, - 'limit' => 250, - ])->setHeaders(['Authorization' => "Basic {$this->auth}"]) - ->send(); - - $pages = 1; - - do { - $pagesLeft = false; - - // Add page to $orderarray - $orders = $page->getContent(); - $headers = $page->getHeaders(); - - try { - $orders = Json::decode($orders, asArray: true); - } catch (\yii\base\InvalidArgumentException $e) { - $orders = ['error' => "Error on decode: $e"]; - } - - $orders = end(array:$orders); - - if(!is_array($orders)){ - throw new Exception(message: '$orders is not an array. Orders reads: ' . $orders); - } - - foreach($orders as $order) - { - $order = Json::encode($order); - $orderarray[] = $order; - } - - if (isset($headers['link'])) { - $pageLinks = explode(string: $headers['link'], separator: ', '); - $nextPageLink = preg_grep(pattern:"/rel=\"next\"$/", array: $pageLinks); - $nextPageLink = end(array: $nextPageLink); - $nextPageLink = substr( - string: substr( - string: $nextPageLink, offset: 1, length: strlen($nextPageLink) - ), offset: 0, length: strpos($nextPageLink, '>;') - 1 - ); - - $pagesLeft = true; - $pages++; - - $page = $this->client->createRequest() - ->setMethod(method: 'GET') - ->setUrl($nextPageLink) - ->setHeaders(['Authorization' => "Basic {$this->auth}"]) - ->send(); - } - } while ($pagesLeft); - - /** - * 1. Get all unfulfilled Shopify orders from the last 12 minutes (just to be safe) - * 2. Extract all individual order object-arrays from array - */ - - echo "\t$pages page(s) of orders. " . (count($orderarray)) . " order(s) found" . PHP_EOL; - - return $orderarray; - } -} \ No newline at end of file diff --git a/composer.json b/composer.json index d42fbf29..99e72293 100755 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "setasign/fpdf": "^1.8", "cgsmith/yii2-stripe": "dev-master", "serzh/amazon-mws-merchant-fulfillment": "^1.0", - "osiset/basic-shopify-api": "^9.1", "yii2tech/csv-grid": "^1.0", "bp-sys/yii2-aws-s3": "~2.0", "bilberrry/yii2-digitalocean-spaces": "^0.1.2", diff --git a/composer.lock b/composer.lock index 69696d99..58928c02 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "99b5d03b314c1a40e5775d61eb68761e", + "content-hash": "1367a46f8214849610c956ec18d57da1", "packages": [ { "name": "2amigos/2fa-library", @@ -213,45 +213,51 @@ }, { "name": "2amigos/yii2-usuario", - "version": "1.5.1", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/2amigos/yii2-usuario.git", - "reference": "e2b041a32a9cd248c7849ff6cfb12a2f60822b57" + "reference": "9c84f12c11878790e8f2a4271f02d4e60d13dbbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/2amigos/yii2-usuario/zipball/e2b041a32a9cd248c7849ff6cfb12a2f60822b57", - "reference": "e2b041a32a9cd248c7849ff6cfb12a2f60822b57", + "url": "https://api.github.com/repos/2amigos/yii2-usuario/zipball/9c84f12c11878790e8f2a4271f02d4e60d13dbbc", + "reference": "9c84f12c11878790e8f2a4271f02d4e60d13dbbc", "shasum": "" }, "require": { "2amigos/yii2-selectize-widget": "^1.1", "php": ">=5.5", + "yetopen/yii2-sms-sender-interface": "^0.1.1", "yiisoft/yii2-authclient": "^2.1", "yiisoft/yii2-bootstrap": "^2.0", - "yiisoft/yii2-httpclient": "^2.0", - "yiisoft/yii2-swiftmailer": "^2.0" + "yiisoft/yii2-httpclient": "^2.0" }, "conflict": { "dektrium/yii2-rbac": "*", "dektrium/yii2-user": "*" }, "require-dev": { + "2amigos/2fa-library": "^2.0", + "2amigos/qrcode-library": "^2.0", "codeception/codeception": "*", "codeception/module-asserts": "^1.1", "codeception/module-db": "^1.0", "codeception/module-filesystem": "^1.0", "codeception/module-yii2": "^1.1", "codeception/verify": "^0.3.3", - "friendsofphp/php-cs-fixer": "^2.3", + "friendsofphp/php-cs-fixer": "^3", + "php": ">=7.4", "phpmd/phpmd": "@stable", + "phpstan/phpstan": "^1.8", "roave/security-advisories": "dev-master", - "squizlabs/php_codesniffer": "*" + "squizlabs/php_codesniffer": "*", + "yiisoft/yii2-symfonymailer": "~2.0.0" }, "suggest": { "2amigos/2fa-library": "Needed if you want to enable 2 Factor Authentication. Require version ^1.0", - "2amigos/qrcode-library": "Needed if you want to enable 2FA with QR Code generation. Require version ^1.1" + "2amigos/qrcode-library": "Needed if you want to enable 2FA with QR Code generation. Require version ^1.1", + "yiisoft/yii2-symfonymailer": "A mailer driver is needed to send e-mails. Older versions use abandoned Swiftmailer which can be replaced with symfonymailer" }, "type": "yii2-extension", "extra": { @@ -301,27 +307,27 @@ "issues": "https://github.com/2amigos/yii2-usuario/issues?state=open", "source": "https://github.com/2amigos/yii2-usuario" }, - "time": "2020-04-05T11:59:36+00:00" + "time": "2023-01-09T08:51:26+00:00" }, { "name": "aws/aws-crt-php", - "version": "v1.0.2", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "3942776a8c99209908ee0b287746263725685732" + "reference": "f5c64ee7c5fce196e2519b3d9b7138649efe032d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/3942776a8c99209908ee0b287746263725685732", - "reference": "3942776a8c99209908ee0b287746263725685732", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/f5c64ee7c5fce196e2519b3d9b7138649efe032d", + "reference": "f5c64ee7c5fce196e2519b3d9b7138649efe032d", "shasum": "" }, "require": { "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.8.35|^5.4.3" + "phpunit/phpunit": "^4.8.35|^5.6.3" }, "type": "library", "autoload": { @@ -349,26 +355,26 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.2" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.4" }, - "time": "2021-09-03T22:57:30+00:00" + "time": "2023-01-31T23:08:25+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.228.5", + "version": "3.259.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8b6e626e02c42310c3ce8615acc7b2c992e48c97" + "reference": "b200fc0a04e904bfc44922767388b279bf90ffe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8b6e626e02c42310c3ce8615acc7b2c992e48c97", - "reference": "8b6e626e02c42310c3ce8615acc7b2c992e48c97", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b200fc0a04e904bfc44922767388b279bf90ffe4", + "reference": "b200fc0a04e904bfc44922767388b279bf90ffe4", "shasum": "" }, "require": { - "aws/aws-crt-php": "^1.0.2", + "aws/aws-crt-php": "^1.0.4", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", @@ -382,6 +388,8 @@ "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", @@ -389,10 +397,11 @@ "ext-sockets": "*", "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35 || ^5.6.3", + "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -440,9 +449,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.228.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.259.1" }, - "time": "2022-06-28T18:13:37+00:00" + "time": "2023-02-20T19:21:16+00:00" }, { "name": "bacon/bacon-qr-code", @@ -582,16 +591,16 @@ }, { "name": "bower-asset/jquery", - "version": "3.6.0", + "version": "3.6.3", "source": { "type": "git", "url": "https://github.com/jquery/jquery-dist.git", - "reference": "e786e3d9707ffd9b0dd330ca135b66344dcef85a" + "reference": "da0f228131a578aea168b799fe4d7fe01764c98b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/e786e3d9707ffd9b0dd330ca135b66344dcef85a", - "reference": "e786e3d9707ffd9b0dd330ca135b66344dcef85a" + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/da0f228131a578aea168b799fe4d7fe01764c98b", + "reference": "da0f228131a578aea168b799fe4d7fe01764c98b" }, "type": "bower-asset", "license": [ @@ -621,12 +630,12 @@ "version": "v1.3.2", "source": { "type": "git", - "url": "https://github.com/mathiasbynens/punycode.js.git", + "url": "git@github.com:bestiejs/punycode.js.git", "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mathiasbynens/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", + "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" }, "type": "bower-asset" @@ -689,16 +698,16 @@ }, { "name": "bower-asset/yii2-pjax", - "version": "2.0.7.1", + "version": "2.0.8", "source": { "type": "git", - "url": "https://github.com/yiisoft/jquery-pjax.git", - "reference": "aef7b953107264f00234902a3880eb50dafc48be" + "url": "git@github.com:yiisoft/jquery-pjax.git", + "reference": "a9298d57da63d14a950f1b94366a864bc62264fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/aef7b953107264f00234902a3880eb50dafc48be", - "reference": "aef7b953107264f00234902a3880eb50dafc48be" + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/a9298d57da63d14a950f1b94366a864bc62264fb", + "reference": "a9298d57da63d14a950f1b94366a864bc62264fb" }, "require": { "bower-asset/jquery": ">=1.8" @@ -710,16 +719,16 @@ }, { "name": "bp-sys/yii2-aws-s3", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/bp-sys/yii2-aws-s3.git", - "reference": "b64592da78357934ed47817cbb2dcc197faee51a" + "reference": "05b7028c7d9ac529c78d95a296e13c934ca3c8d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bp-sys/yii2-aws-s3/zipball/b64592da78357934ed47817cbb2dcc197faee51a", - "reference": "b64592da78357934ed47817cbb2dcc197faee51a", + "url": "https://api.github.com/repos/bp-sys/yii2-aws-s3/zipball/05b7028c7d9ac529c78d95a296e13c934ca3c8d3", + "reference": "05b7028c7d9ac529c78d95a296e13c934ca3c8d3", "shasum": "" }, "require": { @@ -760,75 +769,9 @@ ], "support": { "issues": "https://github.com/bp-sys/yii2-aws-s3/issues?state=open", - "source": "https://github.com/bp-sys/yii2-aws-s3/tree/2.3.0" - }, - "time": "2020-12-07T20:49:21+00:00" - }, - { - "name": "caseyamcl/guzzle_retry_middleware", - "version": "v2.7", - "source": { - "type": "git", - "url": "https://github.com/caseyamcl/guzzle_retry_middleware.git", - "reference": "e6717d8460e5ef40db6d2e7218069a2826f69138" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/caseyamcl/guzzle_retry_middleware/zipball/e6717d8460e5ef40db6d2e7218069a2826f69138", - "reference": "e6717d8460e5ef40db6d2e7218069a2826f69138", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^6.3|^7.0", - "php": "^7.1|^8.0" - }, - "require-dev": { - "jaschilz/php-coverage-badger": "^2.0", - "nesbot/carbon": "^2.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.5|^8.0|^9.0", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleRetry\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Casey McLaughlin", - "email": "caseyamcl@gmail.com", - "homepage": "https://caseymclaughlin.com", - "role": "Developer" - } - ], - "description": "Guzzle v6+ retry middleware that handles 429/503 status codes and connection timeouts", - "homepage": "https://github.com/caseyamcl/guzzle_retry_middleware", - "keywords": [ - "Guzzle", - "back-off", - "caseyamcl", - "guzzle_retry_middleware", - "middleware", - "retry", - "retry-after" - ], - "support": { - "issues": "https://github.com/caseyamcl/guzzle_retry_middleware/issues", - "source": "https://github.com/caseyamcl/guzzle_retry_middleware/tree/v2.7" + "source": "https://github.com/bp-sys/yii2-aws-s3/tree/2.4.0" }, - "time": "2021-12-04T02:49:15+00:00" + "time": "2022-08-21T22:57:18+00:00" }, { "name": "cebe/markdown", @@ -1058,30 +1001,34 @@ }, { "name": "doctrine/annotations", - "version": "1.13.2", + "version": "1.14.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", + "doctrine/lexer": "^1 || ^2", "ext-tokenizer": "*", "php": "^7.1 || ^8.0", "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "~1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" }, "type": "library", "autoload": { @@ -1124,37 +1071,82 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" + "source": "https://github.com/doctrine/annotations/tree/1.14.3" + }, + "time": "2023-02-01T09:20:38+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" }, - "time": "2021-08-05T19:00:23+00:00" + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.3", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.0", "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9.0", + "doctrine/coding-standard": "^9 || ^10", "phpstan/phpstan": "^1.3", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^5.0" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Doctrine\\Common\\Lexer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1186,7 +1178,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" + "source": "https://github.com/doctrine/lexer/tree/2.1.0" }, "funding": [ { @@ -1202,29 +1194,28 @@ "type": "tidelift" } ], - "time": "2022-02-28T11:07:21+00:00" + "time": "2022-12-14T08:49:07+00:00" }, { "name": "egulias/email-validator", - "version": "3.2.1", + "version": "3.2.5", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715" + "reference": "b531a2311709443320c786feb4519cfaf94af796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f88dcf4b14af14a98ad96b14b2b317969eab6715", - "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796", + "reference": "b531a2311709443320c786feb4519cfaf94af796", "shasum": "" }, "require": { - "doctrine/lexer": "^1.2", + "doctrine/lexer": "^1.2|^2", "php": ">=7.2", "symfony/polyfill-intl-idn": "^1.15" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^8.5.8|^9.3.3", "vimeo/psalm": "^4" }, @@ -1262,7 +1253,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/3.2.1" + "source": "https://github.com/egulias/EmailValidator/tree/3.2.5" }, "funding": [ { @@ -1270,24 +1261,34 @@ "type": "github" } ], - "time": "2022-06-18T20:57:19+00:00" + "time": "2023-01-02T17:26:14+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.14.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { @@ -1319,9 +1320,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" }, - "time": "2021-12-25T01:21:49+00:00" + "time": "2022-09-18T07:06:19+00:00" }, { "name": "frostealth/yii2-aws-s3", @@ -1385,37 +1386,49 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.8", + "version": "7.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", - "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", + "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.9", - "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.17" + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.9 || ^2.4", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.1" + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "6.5-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -1468,19 +1481,20 @@ } ], "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", "keywords": [ "client", "curl", "framework", "http", "http client", + "psr-18", + "psr-7", "rest", "web service" ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + "source": "https://github.com/guzzle/guzzle/tree/7.5.0" }, "funding": [ { @@ -1496,20 +1510,20 @@ "type": "tidelift" } ], - "time": "2022-06-20T22:16:07+00:00" + "time": "2022-08-28T15:39:27+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "b94b2807d85443f9719887892882d0329d1e2598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", + "reference": "b94b2807d85443f9719887892882d0329d1e2598", "shasum": "" }, "require": { @@ -1564,7 +1578,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/1.5.2" }, "funding": [ { @@ -1580,47 +1594,51 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2022-08-28T14:55:35+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.9.0", + "version": "2.4.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" + "reference": "67c26b443f348a51926030c83481b85718457d3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", - "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", + "reference": "67c26b443f348a51926030c83481b85718457d3d", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "2.4-dev" } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Psr7\\": "src/" } @@ -1659,6 +1677,11 @@ "name": "Tobias Schultze", "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "description": "PSR-7 message implementation that also provides common utility methods", @@ -1674,7 +1697,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.4.3" }, "funding": [ { @@ -1690,7 +1713,7 @@ "type": "tidelift" } ], - "time": "2022-06-20T21:43:03+00:00" + "time": "2022-10-26T14:07:24+00:00" }, { "name": "http-interop/http-factory-guzzle", @@ -2116,59 +2139,6 @@ }, "time": "2021-01-12T15:17:45+00:00" }, - { - "name": "osiset/basic-shopify-api", - "version": "v9.1.4", - "source": { - "type": "git", - "url": "https://github.com/osiset/Basic-Shopify-API.git", - "reference": "43b1165a9b4a2f432f1943a052d21cb33c504197" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/osiset/Basic-Shopify-API/zipball/43b1165a9b4a2f432f1943a052d21cb33c504197", - "reference": "43b1165a9b4a2f432f1943a052d21cb33c504197", - "shasum": "" - }, - "require": { - "caseyamcl/guzzle_retry_middleware": "^2.3", - "guzzlehttp/guzzle": "^6.5", - "php": ">=7.2.0" - }, - "require-dev": { - "phpdocumentor/phpdocumentor": "2.*", - "phpunit/phpunit": "^6.2", - "squizlabs/php_codesniffer": "^3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Osiset\\BasicShopifyAPI\\": "src/Osiset/BasicShopifyAPI" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tyler King", - "email": "tyler@osiset.com" - } - ], - "description": "A basic Shopify API wrapper with REST and GraphQL support, powered by Guzzle.", - "keywords": [ - "api", - "graphql", - "rest", - "shopify" - ], - "support": { - "issues": "https://github.com/osiset/Basic-Shopify-API/issues", - "source": "https://github.com/osiset/Basic-Shopify-API/tree/v9.1.4" - }, - "time": "2020-08-25T13:25:33+00:00" - }, { "name": "paragonie/constant_time_encoding", "version": "v2.6.3", @@ -2292,16 +2262,16 @@ }, { "name": "php-http/client-common", - "version": "2.5.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/php-http/client-common.git", - "reference": "d135751167d57e27c74de674d6a30cef2dc8e054" + "reference": "45db684cd4e186dcdc2b9c06b22970fe123796c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/d135751167d57e27c74de674d6a30cef2dc8e054", - "reference": "d135751167d57e27c74de674d6a30cef2dc8e054", + "url": "https://api.github.com/repos/php-http/client-common/zipball/45db684cd4e186dcdc2b9c06b22970fe123796c0", + "reference": "45db684cd4e186dcdc2b9c06b22970fe123796c0", "shasum": "" }, "require": { @@ -2361,22 +2331,22 @@ ], "support": { "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.5.0" + "source": "https://github.com/php-http/client-common/tree/2.6.0" }, - "time": "2021-11-26T15:01:24+00:00" + "time": "2022-09-29T09:59:43+00:00" }, { "name": "php-http/discovery", - "version": "1.14.2", + "version": "1.14.3", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "c8d48852fbc052454af42f6de27635ddd916b959" + "reference": "31d8ee46d0215108df16a8527c7438e96a4d7735" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/c8d48852fbc052454af42f6de27635ddd916b959", - "reference": "c8d48852fbc052454af42f6de27635ddd916b959", + "url": "https://api.github.com/repos/php-http/discovery/zipball/31d8ee46d0215108df16a8527c7438e96a4d7735", + "reference": "31d8ee46d0215108df16a8527c7438e96a4d7735", "shasum": "" }, "require": { @@ -2428,9 +2398,9 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.14.2" + "source": "https://github.com/php-http/discovery/tree/1.14.3" }, - "time": "2022-05-25T07:26:05+00:00" + "time": "2022-07-11T14:04:40+00:00" }, { "name": "php-http/httplug", @@ -3037,21 +3007,21 @@ }, { "name": "sentry/sdk", - "version": "3.2.0", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php-sdk.git", - "reference": "6d78bd83b43efbb52f81d6824f4af344fa9ba292" + "reference": "d0678fc7274dbb03046ed05cb24eb92945bedf8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/6d78bd83b43efbb52f81d6824f4af344fa9ba292", - "reference": "6d78bd83b43efbb52f81d6824f4af344fa9ba292", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/d0678fc7274dbb03046ed05cb24eb92945bedf8e", + "reference": "d0678fc7274dbb03046ed05cb24eb92945bedf8e", "shasum": "" }, "require": { "http-interop/http-factory-guzzle": "^1.0", - "sentry/sentry": "^3.5", + "sentry/sentry": "^3.9", "symfony/http-client": "^4.3|^5.0|^6.0" }, "type": "metapackage", @@ -3078,7 +3048,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php-sdk/issues", - "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.2.0" + "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.3.0" }, "funding": [ { @@ -3090,20 +3060,20 @@ "type": "custom" } ], - "time": "2022-05-21T11:10:11+00:00" + "time": "2022-10-11T09:05:00+00:00" }, { "name": "sentry/sentry", - "version": "3.6.1", + "version": "3.13.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "5b8f2934b0b20bb01da11c76985ceb5bd6c6af91" + "reference": "71c86fe4699a7f1a40c7d985f3dc7667045152f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/5b8f2934b0b20bb01da11c76985ceb5bd6c6af91", - "reference": "5b8f2934b0b20bb01da11c76985ceb5bd6c6af91", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/71c86fe4699a7f1a40c7d985f3dc7667045152f0", + "reference": "71c86fe4699a7f1a40c7d985f3dc7667045152f0", "shasum": "" }, "require": { @@ -3115,15 +3085,14 @@ "php": "^7.2|^8.0", "php-http/async-client-implementation": "^1.0", "php-http/client-common": "^1.5|^2.0", - "php-http/discovery": "^1.11", + "php-http/discovery": "^1.11, <1.15", "php-http/httplug": "^1.1|^2.0", "php-http/message": "^1.5", "psr/http-factory": "^1.0", "psr/http-message-implementation": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0", - "symfony/polyfill-php80": "^1.17", - "symfony/polyfill-uuid": "^1.13.1" + "symfony/polyfill-php80": "^1.17" }, "conflict": { "php-http/client-common": "1.8.0", @@ -3149,7 +3118,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.6.x-dev" + "dev-master": "3.13.x-dev" } }, "autoload": { @@ -3183,7 +3152,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/3.6.1" + "source": "https://github.com/getsentry/sentry-php/tree/3.13.1" }, "funding": [ { @@ -3195,7 +3164,7 @@ "type": "custom" } ], - "time": "2022-06-27T07:58:00+00:00" + "time": "2023-02-10T10:17:57+00:00" }, { "name": "serzh/amazon-mws-merchant-fulfillment", @@ -3239,16 +3208,16 @@ }, { "name": "setasign/fpdf", - "version": "1.8.4", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/Setasign/FPDF.git", - "reference": "b0ddd9c5b98ced8230ef38534f6f3c17308a7974" + "reference": "f4104a04c9a3f95c4c26a0a0531abebcc980987a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDF/zipball/b0ddd9c5b98ced8230ef38534f6f3c17308a7974", - "reference": "b0ddd9c5b98ced8230ef38534f6f3c17308a7974", + "url": "https://api.github.com/repos/Setasign/FPDF/zipball/f4104a04c9a3f95c4c26a0a0531abebcc980987a", + "reference": "f4104a04c9a3f95c4c26a0a0531abebcc980987a", "shasum": "" }, "require": { @@ -3279,22 +3248,22 @@ "pdf" ], "support": { - "source": "https://github.com/Setasign/FPDF/tree/1.8.4" + "source": "https://github.com/Setasign/FPDF/tree/1.8.5" }, - "time": "2021-08-30T07:50:06+00:00" + "time": "2022-11-18T07:02:00+00:00" }, { "name": "stripe/stripe-php", - "version": "v8.8.0", + "version": "v10.6.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "06bfb65639cfecae7c2b57d340f740599c548753" + "reference": "5fc46f43c743c715cb5edeb7be3383efb7b4bb2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/06bfb65639cfecae7c2b57d340f740599c548753", - "reference": "06bfb65639cfecae7c2b57d340f740599c548753", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/5fc46f43c743c715cb5edeb7be3383efb7b4bb2e", + "reference": "5fc46f43c743c715cb5edeb7be3383efb7b4bb2e", "shasum": "" }, "require": { @@ -3305,6 +3274,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.5.0", + "php-coveralls/php-coveralls": "^2.5", "phpstan/phpstan": "^1.2", "phpunit/phpunit": "^5.7 || ^9.0", "squizlabs/php_codesniffer": "^3.3" @@ -3339,9 +3309,9 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v8.8.0" + "source": "https://github.com/stripe/stripe-php/tree/v10.6.0" }, - "time": "2022-06-23T16:42:38+00:00" + "time": "2023-02-16T23:01:54+00:00" }, { "name": "swiftmailer/swiftmailer", @@ -3421,25 +3391,25 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.0.2", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -3468,7 +3438,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.0" }, "funding": [ { @@ -3484,20 +3454,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-11-25T10:21:52+00:00" }, { "name": "symfony/finder", - "version": "v5.4.8", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" + "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", + "url": "https://api.github.com/repos/symfony/finder/zipball/6071aebf810ad13fe8200c224f36103abb37cf1f", + "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f", "shasum": "" }, "require": { @@ -3531,7 +3501,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.8" + "source": "https://github.com/symfony/finder/tree/v5.4.19" }, "funding": [ { @@ -3547,25 +3517,26 @@ "type": "tidelift" } ], - "time": "2022-04-15T08:07:45+00:00" + "time": "2023-01-14T19:14:44+00:00" }, { "name": "symfony/http-client", - "version": "v6.0.9", + "version": "v6.2.6", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3c6fc53a3deed2d3c1825d41ad8b3f23a6b038b5" + "reference": "6efa9a7521ab7d031a82cf0a759484d1b02a6ad9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3c6fc53a3deed2d3c1825d41ad8b3f23a6b038b5", - "reference": "3c6fc53a3deed2d3c1825d41ad8b3f23a6b038b5", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6efa9a7521ab7d031a82cf0a759484d1b02a6ad9", + "reference": "6efa9a7521ab7d031a82cf0a759484d1b02a6ad9", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/http-client-contracts": "^3", "symfony/service-contracts": "^1.0|^2|^3" }, @@ -3615,7 +3586,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v6.0.9" + "source": "https://github.com/symfony/http-client/tree/v6.2.6" }, "funding": [ { @@ -3631,24 +3602,24 @@ "type": "tidelift" } ], - "time": "2022-05-21T13:33:31+00:00" + "time": "2023-01-30T15:46:28+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.0.2", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129" + "reference": "c5f587eb445224ddfeb05b5ee703476742d730bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4184b9b63af1edaf35b6a7974c6f1f9f33294129", - "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/c5f587eb445224ddfeb05b5ee703476742d730bf", + "reference": "c5f587eb445224ddfeb05b5ee703476742d730bf", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "suggest": { "symfony/http-client-implementation": "" @@ -3656,7 +3627,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -3666,7 +3637,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\HttpClient\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3693,7 +3667,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.2.0" }, "funding": [ { @@ -3709,24 +3683,24 @@ "type": "tidelift" } ], - "time": "2022-04-12T16:11:42+00:00" + "time": "2022-11-25T10:21:52+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.0.3", + "version": "v6.2.5", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "51f7006670febe4cbcbae177cbffe93ff833250d" + "reference": "e8324d44f5af99ec2ccec849934a242f64458f86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/51f7006670febe4cbcbae177cbffe93ff833250d", - "reference": "51f7006670febe4cbcbae177cbffe93ff833250d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/e8324d44f5af99ec2ccec849934a242f64458f86", + "reference": "e8324d44f5af99ec2ccec849934a242f64458f86", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.1|^3" }, "type": "library", @@ -3760,7 +3734,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.0.3" + "source": "https://github.com/symfony/options-resolver/tree/v6.2.5" }, "funding": [ { @@ -3776,20 +3750,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2023-01-01T08:38:09+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "143f1881e655bebca1312722af8068de235ae5dc" + "reference": "927013f3aac555983a5059aada98e1907d842695" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/143f1881e655bebca1312722af8068de235ae5dc", - "reference": "143f1881e655bebca1312722af8068de235ae5dc", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/927013f3aac555983a5059aada98e1907d842695", + "reference": "927013f3aac555983a5059aada98e1907d842695", "shasum": "" }, "require": { @@ -3804,7 +3778,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3843,7 +3817,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.27.0" }, "funding": [ { @@ -3859,20 +3833,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + "reference": "639084e360537a19f9ee352433b84ce831f3d2da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", - "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da", "shasum": "" }, "require": { @@ -3886,7 +3860,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3930,7 +3904,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" }, "funding": [ { @@ -3946,20 +3920,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "shasum": "" }, "require": { @@ -3971,7 +3945,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4014,7 +3988,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" }, "funding": [ { @@ -4030,20 +4004,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { @@ -4058,7 +4032,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4097,7 +4071,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, "funding": [ { @@ -4113,20 +4087,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", - "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", "shasum": "" }, "require": { @@ -4135,7 +4109,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4173,7 +4147,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" }, "funding": [ { @@ -4189,20 +4163,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "shasum": "" }, "require": { @@ -4211,7 +4185,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4256,89 +4230,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-10T07:21:04+00:00" - }, - { - "name": "symfony/polyfill-uuid", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "a41886c1c81dc075a09c71fe6db5b9d68c79de23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/a41886c1c81dc075a09c71fe6db5b9d68c79de23", - "reference": "a41886c1c81dc075a09c71fe6db5b9d68c79de23", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-uuid": "*" - }, - "suggest": { - "ext-uuid": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Uuid\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for uuid functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "uuid" - ], - "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" }, "funding": [ { @@ -4354,25 +4246,24 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/process", - "version": "v5.4.8", + "version": "v6.2.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" + "reference": "9ead139f63dfa38c4e4a9049cc64a8b2748c83b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "url": "https://api.github.com/repos/symfony/process/zipball/9ead139f63dfa38c4e4a9049cc64a8b2748c83b7", + "reference": "9ead139f63dfa38c4e4a9049cc64a8b2748c83b7", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -4400,7 +4291,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.8" + "source": "https://github.com/symfony/process/tree/v6.2.5" }, "funding": [ { @@ -4416,24 +4307,24 @@ "type": "tidelift" } ], - "time": "2022-04-08T05:07:18+00:00" + "time": "2023-01-01T08:38:09+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.0.2", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75", + "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/container": "^2.0" }, "conflict": { @@ -4445,7 +4336,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -4455,7 +4346,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4482,7 +4376,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.2.0" }, "funding": [ { @@ -4498,7 +4392,52 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:58+00:00" + "time": "2022-11-25T10:21:52+00:00" + }, + { + "name": "yetopen/yii2-sms-sender-interface", + "version": "v0.1.2", + "source": { + "type": "git", + "url": "https://github.com/YetOpen/yii2-sms-sender-interface.git", + "reference": "f3d1fc37d271614195173ceee14b7b83b94dc346" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/YetOpen/yii2-sms-sender-interface/zipball/f3d1fc37d271614195173ceee14b7b83b94dc346", + "reference": "f3d1fc37d271614195173ceee14b7b83b94dc346", + "shasum": "" + }, + "require": { + "yiisoft/yii2": "~2.0.0" + }, + "type": "yii2-extension", + "autoload": { + "psr-4": { + "yetopen\\smssender\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Yetopen", + "email": "marco.piazza@yetopen.com" + } + ], + "description": "It implements SmsSenderInterface that should be implemented by text message sender classes", + "keywords": [ + "extension", + "interface", + "yii2" + ], + "support": { + "issues": "https://github.com/YetOpen/yii2-sms-sender-interface/issues", + "source": "https://github.com/YetOpen/yii2-sms-sender-interface/tree/v0.1.2" + }, + "time": "2022-07-08T08:33:16+00:00" }, { "name": "yii2tech/csv-grid", @@ -4569,16 +4508,16 @@ }, { "name": "yiisoft/yii2", - "version": "2.0.45", + "version": "2.0.47", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "e2223d4085e5612aa616635f8fcaf478607f62e8" + "reference": "8ecf57895d9c4b29cf9658ffe57af5f3d0e25254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/e2223d4085e5612aa616635f8fcaf478607f62e8", - "reference": "e2223d4085e5612aa616635f8fcaf478607f62e8", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/8ecf57895d9c4b29cf9658ffe57af5f3d0e25254", + "reference": "8ecf57895d9c4b29cf9658ffe57af5f3d0e25254", "shasum": "" }, "require": { @@ -4687,20 +4626,20 @@ "type": "tidelift" } ], - "time": "2022-02-11T13:12:40+00:00" + "time": "2022-11-18T16:21:58+00:00" }, { "name": "yiisoft/yii2-authclient", - "version": "2.2.12", + "version": "2.2.14", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-authclient.git", - "reference": "84547828d3381c76a1e926d87c570d08ebbce7bd" + "reference": "6bd4b4efc60db2b31bb957a473442900c704857e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-authclient/zipball/84547828d3381c76a1e926d87c570d08ebbce7bd", - "reference": "84547828d3381c76a1e926d87c570d08ebbce7bd", + "url": "https://api.github.com/repos/yiisoft/yii2-authclient/zipball/6bd4b4efc60db2b31bb957a473442900c704857e", + "reference": "6bd4b4efc60db2b31bb957a473442900c704857e", "shasum": "" }, "require": { @@ -4780,7 +4719,7 @@ "type": "tidelift" } ], - "time": "2021-12-03T11:37:55+00:00" + "time": "2022-11-18T17:23:56+00:00" }, { "name": "yiisoft/yii2-bootstrap", @@ -5037,28 +4976,28 @@ }, { "name": "yiisoft/yii2-queue", - "version": "2.3.4", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-queue.git", - "reference": "ed30b5f46ddadd62587a4963dec35f9b756c408b" + "reference": "c1bf0ef5dbe107dc1cf692c1349b9ddd2485a399" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/ed30b5f46ddadd62587a4963dec35f9b756c408b", - "reference": "ed30b5f46ddadd62587a4963dec35f9b756c408b", + "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/c1bf0ef5dbe107dc1cf692c1349b9ddd2485a399", + "reference": "c1bf0ef5dbe107dc1cf692c1349b9ddd2485a399", "shasum": "" }, "require": { "php": ">=5.5.0", - "symfony/process": "^3.3||^4.0||^5.0", + "symfony/process": "^3.3||^4.0||^5.0||^6.0", "yiisoft/yii2": "~2.0.14" }, "require-dev": { "aws/aws-sdk-php": ">=2.4", "enqueue/amqp-lib": "^0.8||^0.9.10", "enqueue/stomp": "^0.8.39", - "jeremeamia/superclosure": "*", + "opis/closure": "*", "pda/pheanstalk": "v3.*", "php-amqplib/php-amqplib": "*", "phpunit/phpunit": "~4.4", @@ -5139,7 +5078,7 @@ "type": "tidelift" } ], - "time": "2022-03-31T07:41:51+00:00" + "time": "2022-11-18T17:16:47+00:00" }, { "name": "yiisoft/yii2-swiftmailer", @@ -5438,16 +5377,16 @@ }, { "name": "codeception/codeception", - "version": "4.2.1", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "77b3e2003fd4446b35826cb9dc397129c521c888" + "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/77b3e2003fd4446b35826cb9dc397129c521c888", - "reference": "77b3e2003fd4446b35826cb9dc397129c521c888", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b88014f3348c93f3df99dc6d0967b0dbfa804474", + "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474", "shasum": "" }, "require": { @@ -5524,7 +5463,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.2.1" + "source": "https://github.com/Codeception/Codeception/tree/4.2.2" }, "funding": [ { @@ -5532,7 +5471,7 @@ "type": "open_collective" } ], - "time": "2022-06-22T06:18:59+00:00" + "time": "2022-08-13T13:28:25+00:00" }, { "name": "codeception/lib-asserts", @@ -5954,30 +5893,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -6004,7 +5943,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -6020,24 +5959,24 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "fakerphp/faker", - "version": "v1.19.0", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75" + "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/d7f08a622b3346766325488aa32ddc93ccdecc75", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d", + "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" }, @@ -6048,7 +5987,8 @@ "bamarni/composer-bin-plugin": "^1.4.1", "doctrine/persistence": "^1.3 || ^2.0", "ext-intl": "*", - "symfony/phpunit-bridge": "^4.4 || ^5.2" + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" }, "suggest": { "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", @@ -6060,7 +6000,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.19-dev" + "dev-main": "v1.21-dev" } }, "autoload": { @@ -6085,9 +6025,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.19.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.21.0" }, - "time": "2022-02-02T17:38:57+00:00" + "time": "2022-12-13T13:54:32+00:00" }, { "name": "myclabs/deep-copy", @@ -6150,16 +6090,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.3", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", "shasum": "" }, "require": { @@ -6200,9 +6140,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2023-01-16T22:05:37+00:00" }, { "name": "phar-io/manifest", @@ -6315,166 +6255,6 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, - "time": "2022-03-15T21:29:03+00:00" - }, { "name": "phpspec/php-diff", "version": "v1.1.3", @@ -6516,92 +6296,25 @@ }, "time": "2020-09-18T13:47:07+00:00" }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed", + "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -6650,7 +6363,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24" }, "funding": [ { @@ -6658,7 +6371,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2023-01-26T08:26:55+00:00" }, { "name": "phpunit/php-file-iterator", @@ -6903,20 +6616,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.21", + "version": "9.6.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" + "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7b1615e3e887d6c719121c6d4a44b0ab9645555", + "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -6927,7 +6640,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -6935,19 +6647,16 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -6958,7 +6667,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -6989,7 +6698,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.3" }, "funding": [ { @@ -6999,9 +6708,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-06-19T12:14:25+00:00" + "time": "2023-02-04T13:37:15+00:00" }, { "name": "psr/event-dispatcher", @@ -7222,16 +6935,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -7284,7 +6997,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -7292,7 +7005,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -7419,16 +7132,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -7470,7 +7183,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -7478,20 +7191,20 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -7547,7 +7260,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -7555,7 +7268,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -7792,16 +7505,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -7840,10 +7553,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -7851,7 +7564,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -7910,16 +7623,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -7931,7 +7644,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -7954,7 +7667,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -7962,7 +7675,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -8019,16 +7732,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.4.3", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad" + "reference": "572b9e03741051b97c316f65f8c361eed08fdb14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/18e73179c6a33d520de1b644941eba108dd811ad", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/572b9e03741051b97c316f65f8c361eed08fdb14", + "reference": "572b9e03741051b97c316f65f8c361eed08fdb14", "shasum": "" }, "require": { @@ -8071,7 +7784,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.3" + "source": "https://github.com/symfony/browser-kit/tree/v5.4.19" }, "funding": [ { @@ -8087,7 +7800,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-01-01T08:32:19+00:00" }, { "name": "symfony/console", @@ -8175,16 +7888,16 @@ }, { "name": "symfony/css-selector", - "version": "v5.4.3", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e" + "reference": "f4a7d150f5b9e8f974f6f127d8167e420d11fc62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b0a190285cd95cb019237851205b8140ef6e368e", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f4a7d150f5b9e8f974f6f127d8167e420d11fc62", + "reference": "f4a7d150f5b9e8f974f6f127d8167e420d11fc62", "shasum": "" }, "require": { @@ -8221,7 +7934,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.3" + "source": "https://github.com/symfony/css-selector/tree/v5.4.19" }, "funding": [ { @@ -8237,20 +7950,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-01-01T08:32:19+00:00" }, { "name": "symfony/debug", - "version": "v4.4.41", + "version": "v4.4.44", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "6637e62480b60817b9a6984154a533e8e64c6bd5" + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/6637e62480b60817b9a6984154a533e8e64c6bd5", - "reference": "6637e62480b60817b9a6984154a533e8e64c6bd5", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", "shasum": "" }, "require": { @@ -8289,7 +8002,7 @@ "description": "Provides tools to ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug/tree/v4.4.41" + "source": "https://github.com/symfony/debug/tree/v4.4.44" }, "funding": [ { @@ -8306,20 +8019,20 @@ } ], "abandoned": "symfony/error-handler", - "time": "2022-04-12T15:19:55+00:00" + "time": "2022-07-28T16:29:46+00:00" }, { "name": "symfony/dom-crawler", - "version": "v5.4.9", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "a213cbc80382320b0efdccdcdce232f191fafe3a" + "reference": "224a1820e7669babdd85970230ed72bd6e342ad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/a213cbc80382320b0efdccdcdce232f191fafe3a", - "reference": "a213cbc80382320b0efdccdcdce232f191fafe3a", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/224a1820e7669babdd85970230ed72bd6e342ad4", + "reference": "224a1820e7669babdd85970230ed72bd6e342ad4", "shasum": "" }, "require": { @@ -8365,7 +8078,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.9" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.19" }, "funding": [ { @@ -8381,20 +8094,20 @@ "type": "tidelift" } ], - "time": "2022-05-04T14:46:32+00:00" + "time": "2023-01-14T19:14:44+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.9", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" + "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", - "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abf49cc084c087d94b4cb939c3f3672971784e0c", + "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c", "shasum": "" }, "require": { @@ -8450,7 +8163,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.19" }, "funding": [ { @@ -8466,24 +8179,24 @@ "type": "tidelift" } ], - "time": "2022-05-05T16:45:39+00:00" + "time": "2023-01-01T08:32:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.0.2", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051" + "reference": "0782b0b52a737a05b4383d0df35a474303cabdae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7bc61cc2db649b4637d331240c5346dcc7708051", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0782b0b52a737a05b4383d0df35a474303cabdae", + "reference": "0782b0b52a737a05b4383d0df35a474303cabdae", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, "suggest": { @@ -8492,7 +8205,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.3-dev" }, "thanks": { "name": "symfony/contracts", @@ -8529,7 +8242,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.0" }, "funding": [ { @@ -8545,20 +8258,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-11-25T10:21:52+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "shasum": "" }, "require": { @@ -8573,7 +8286,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8611,7 +8324,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" }, "funding": [ { @@ -8627,20 +8340,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.43", + "version": "v4.4.45", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "07e392f0ef78376d080d5353c081a5e5704835bd" + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/07e392f0ef78376d080d5353c081a5e5704835bd", - "reference": "07e392f0ef78376d080d5353c081a5e5704835bd", + "url": "https://api.github.com/repos/symfony/yaml/zipball/aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", "shasum": "" }, "require": { @@ -8682,7 +8395,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.43" + "source": "https://github.com/symfony/yaml/tree/v4.4.45" }, "funding": [ { @@ -8698,7 +8411,7 @@ "type": "tidelift" } ], - "time": "2022-06-20T08:31:17+00:00" + "time": "2022-08-02T15:47:23+00:00" }, { "name": "theseer/tokenizer", @@ -8750,76 +8463,18 @@ ], "time": "2021-07-28T10:34:58+00:00" }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" - }, { "name": "yiisoft/yii2-debug", - "version": "2.1.19", + "version": "2.1.22", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-debug.git", - "reference": "84d20d738b0698298f851fcb6fc25e748d759223" + "reference": "c0fa388c56b64edfb92987fdcc37d7a0243170d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/84d20d738b0698298f851fcb6fc25e748d759223", - "reference": "84d20d738b0698298f851fcb6fc25e748d759223", + "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/c0fa388c56b64edfb92987fdcc37d7a0243170d7", + "reference": "c0fa388c56b64edfb92987fdcc37d7a0243170d7", "shasum": "" }, "require": { @@ -8896,7 +8551,7 @@ "type": "tidelift" } ], - "time": "2022-04-05T20:35:14+00:00" + "time": "2022-11-18T17:29:27+00:00" }, { "name": "yiisoft/yii2-faker", diff --git a/console/migrations/m230221_115138_remove_previous_shopify_tables.php b/console/migrations/m230221_115138_remove_previous_shopify_tables.php new file mode 100644 index 00000000..a4a14224 --- /dev/null +++ b/console/migrations/m230221_115138_remove_previous_shopify_tables.php @@ -0,0 +1,49 @@ +dropTable("{{%shopify_app}}"); + $this->dropTable("{{%shopify_webhook}}"); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->execute(" + CREATE TABLE `shopify_app` ( + `id` int NOT NULL AUTO_INCREMENT, + `customer_id` int NOT NULL, + `shop` varchar(128) NOT NULL, + `scopes` varchar(128) NOT NULL, + `access_token` varchar(128) DEFAULT NULL, + `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `shop` (`shop`), + KEY `customer_id` (`customer_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + + $this->execute(" + CREATE TABLE `shopify_webhook` ( + `id` int NOT NULL AUTO_INCREMENT, + `customer_id` int NOT NULL, + `shopify_webhook_id` varchar(64) NOT NULL, + `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `customer_id` (`customer_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 9a0d5766..c1623608 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,16 +22,6 @@ services: # Mount source-code for development - ./:/app - shopify: - build: shopify - ports: - - 30004:80 - volumes: - # Re-use local composer cache via host-volume - - ~/.composer-docker/cache:/root/.composer/cache:delegated - # Mount source-code for development - - ./:/app - mysql: image: mysql:8 environment: diff --git a/environments/index.php b/environments/index.php index 28def5ad..456c6a8f 100755 --- a/environments/index.php +++ b/environments/index.php @@ -37,8 +37,6 @@ 'console/runtime', 'frontend/runtime', 'frontend/web/assets', - 'shopify/runtime', - 'shopify/web/assets', ], 'setExecutable' => [ 'yii', diff --git a/frontend/models/forms/untested/ShopifyForm.php b/frontend/models/forms/untested/ShopifyForm.php deleted file mode 100644 index 62ed0201..00000000 --- a/frontend/models/forms/untested/ShopifyForm.php +++ /dev/null @@ -1,34 +0,0 @@ - 'Shopify Store URL', - 'apiKey' => 'API Key', - 'apiSecret' => 'API Secret', - 'locationID' => 'Location ID', - ]; - } -} \ No newline at end of file diff --git a/shopify/config/bootstrap.php b/shopify/config/bootstrap.php deleted file mode 100755 index b3d9bbc7..00000000 --- a/shopify/config/bootstrap.php +++ /dev/null @@ -1 +0,0 @@ - '{"id": 450789469,"admin_graphql_api_id": "gid://shopify/Order/450789469","app_id": null,"browser_ip": "0.0.0.0","buyer_accepts_marketing": false,"cancel_reason": null,"cancelled_at": null,"cart_token": "68778783ad298f1c80c3bafcddeea02f","checkout_id": 901414060,"checkout_token": "bd5a8aa1ecd019dd3520ff791ee3a24c","client_details": {"accept_language": null,"browser_height": null,"browser_ip": "0.0.0.0","browser_width": null,"session_hash": null,"user_agent": null},"closed_at": null,"confirmed": true,"contact_email": "bob.norman@hostmail.com","created_at": "2008-01-10T11:00:00-05:00","currency": "USD","customer_locale": null,"device_id": null,"discount_codes": [{"code": "TENOFF","amount": "10.00","type": "fixed_amount"}],"email": "bob.norman@hostmail.com","financial_status": "partially_refunded","fulfillment_status": null,"gateway": "authorize_net","landing_site": "http://www.example.com?source=abc","landing_site_ref": "abc","location_id": null,"name": "#1001","note": null,"note_attributes": [{"name": "custom engraving","value": "Happy Birthday"},{"name": "colour","value": "green"}],"number": 1,"order_number": 1001,"order_status_url": "https://apple.myshopify.com/690933842/orders/b1946ac92492d2347c6235b4d2611184/authenticate?key=imasecretipod","payment_gateway_names": ["bogus"],"phone": "+557734881234","presentment_currency": "USD","processed_at": "2008-01-10T11:00:00-05:00","processing_method": "direct","reference": "fhwdgads","referring_site": "http://www.otherexample.com","source_identifier": "fhwdgads","source_name": "web","source_url": null,"subtotal_price": "597.00","subtotal_price_set": {"shop_money": {"amount": "597.00","currency_code": "USD"},"presentment_money": {"amount": "597.00","currency_code": "USD"}},"tags": "","tax_lines": [{"price": "11.94","rate": 0.06,"title": "State Tax","price_set": {"shop_money": {"amount": "11.94","currency_code": "USD"},"presentment_money": {"amount": "11.94","currency_code": "USD"}}}],"taxes_included": false,"test": false,"token": "b1946ac92492d2347c6235b4d2611184","total_discounts": "10.00","total_discounts_set": {"shop_money": {"amount": "10.00","currency_code": "USD"},"presentment_money": {"amount": "10.00","currency_code": "USD"}},"total_line_items_price": "597.00","total_line_items_price_set": {"shop_money": {"amount": "597.00","currency_code": "USD"},"presentment_money": {"amount": "597.00","currency_code": "USD"}},"total_price": "598.94","total_price_set": {"shop_money": {"amount": "598.94","currency_code": "USD"},"presentment_money": {"amount": "598.94","currency_code": "USD"}},"total_price_usd": "598.94","total_shipping_price_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"total_tax": "11.94","total_tax_set": {"shop_money": {"amount": "11.94","currency_code": "USD"},"presentment_money": {"amount": "11.94","currency_code": "USD"}},"total_tip_received": "0.00","total_weight": 0,"updated_at": "2008-01-10T11:00:00-05:00","user_id": null,"billing_address": {"first_name": "Bob","address1": "Chestnut Street 92","phone": "555-625-1199","city": "Louisville","zip": "40202","province": "Kentucky","country": "United States","last_name": "Norman","address2": "","company": null,"latitude": 45.41634,"longitude": -75.6868,"name": "Bob Norman","country_code": "US","province_code": "KY"},"customer": {"id": 207119551,"email": "bob.norman@hostmail.com","accepts_marketing": false,"created_at": "2021-08-04T13:06:55-04:00","updated_at": "2021-08-04T13:06:55-04:00","first_name": "Bob","last_name": "Norman","orders_count": 1,"state": "disabled","total_spent": "199.65","last_order_id": 450789469,"note": null,"verified_email": true,"multipass_identifier": null,"tax_exempt": false,"phone": "+16136120707","tags": "","last_order_name": "#1001","currency": "USD","accepts_marketing_updated_at": "2005-06-12T11:57:11-04:00","marketing_opt_in_level": null,"tax_exemptions": [],"admin_graphql_api_id": "gid://shopify/Customer/207119551","default_address": {"id": 207119551,"customer_id": 207119551,"first_name": null,"last_name": null,"company": null,"address1": "Chestnut Street 92","address2": "","city": "Louisville","province": "Kentucky","country": "United States","zip": "40202","phone": "555-625-1199","name": "","province_code": "KY","country_code": "US","country_name": "United States","default": true}},"discount_applications": [{"target_type": "line_item","type": "discount_code","value": "10.0","value_type": "fixed_amount","allocation_method": "across","target_selection": "all","code": "TENOFF"}],"fulfillments": [{"id": 255858046,"admin_graphql_api_id": "gid://shopify/Fulfillment/255858046","created_at": "2021-08-04T13:06:55-04:00","location_id": 905684977,"name": "#1001.0","order_id": 450789469,"receipt": {"testcase": true,"authorization": "123456"},"service": "manual","shipment_status": null,"status": "failure","tracking_company": "USPS","tracking_number": "1Z2345","tracking_numbers": ["1Z2345"],"tracking_url": "https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345","tracking_urls": ["https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345"],"updated_at": "2021-08-04T13:06:55-04:00","line_items": [{"id": 466157049,"admin_graphql_api_id": "gid://shopify/LineItem/466157049","fulfillable_quantity": 0,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - green","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [{"name": "Custom Engraving Front","value": "Happy Birthday"},{"name": "Custom Engraving Back","value": "Merry Christmas"}],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008GREEN","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 39072856,"variant_inventory_management": "shopify","variant_title": "green","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.34","amount_set": {"shop_money": {"amount": "3.34","currency_code": "USD"},"presentment_money": {"amount": "3.34","currency_code": "USD"}},"discount_application_index": 0}]}]}],"line_items": [{"id": 466157049,"admin_graphql_api_id": "gid://shopify/LineItem/466157049","fulfillable_quantity": 0,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - green","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [{"name": "Custom Engraving Front","value": "Happy Birthday"},{"name": "Custom Engraving Back","value": "Merry Christmas"}],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008GREEN","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 39072856,"variant_inventory_management": "shopify","variant_title": "green","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.34","amount_set": {"shop_money": {"amount": "3.34","currency_code": "USD"},"presentment_money": {"amount": "3.34","currency_code": "USD"}},"discount_application_index": 0}]},{"id": 518995019,"admin_graphql_api_id": "gid://shopify/LineItem/518995019","fulfillable_quantity": 1,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - red","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008RED","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 49148385,"variant_inventory_management": "shopify","variant_title": "red","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.33","amount_set": {"shop_money": {"amount": "3.33","currency_code": "USD"},"presentment_money": {"amount": "3.33","currency_code": "USD"}},"discount_application_index": 0}]},{"id": 703073504,"admin_graphql_api_id": "gid://shopify/LineItem/703073504","fulfillable_quantity": 0,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - black","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008BLACK","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 457924702,"variant_inventory_management": "shopify","variant_title": "black","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.33","amount_set": {"shop_money": {"amount": "3.33","currency_code": "USD"},"presentment_money": {"amount": "3.33","currency_code": "USD"}},"discount_application_index": 0}]}],"payment_details": {"credit_card_bin": null,"avs_result_code": null,"cvv_result_code": null,"credit_card_number": "•••• •••• •••• 4242","credit_card_company": "Visa"},"refunds": [{"id": 509562969,"admin_graphql_api_id": "gid://shopify/Refund/509562969","created_at": "2021-08-04T13:06:55-04:00","note": "it broke during shipping","order_id": 450789469,"processed_at": "2021-08-04T13:06:55-04:00","restock": true,"user_id": 799407056,"order_adjustments": [],"transactions": [{"id": 179259969,"admin_graphql_api_id": "gid://shopify/OrderTransaction/179259969","amount": "209.00","authorization": "authorization-key","created_at": "2005-08-05T12:59:12-04:00","currency": "USD","device_id": null,"error_code": null,"gateway": "bogus","kind": "refund","location_id": null,"message": null,"order_id": 450789469,"parent_id": 801038806,"processed_at": "2005-08-05T12:59:12-04:00","receipt": {},"source_name": "web","status": "success","test": false,"user_id": null}],"refund_line_items": [{"id": 104689539,"line_item_id": 703073504,"location_id": 487838322,"quantity": 1,"restock_type": "legacy_restock","subtotal": 195.66,"subtotal_set": {"shop_money": {"amount": "195.66","currency_code": "USD"},"presentment_money": {"amount": "195.66","currency_code": "USD"}},"total_tax": 3.98,"total_tax_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"line_item": {"id": 703073504,"admin_graphql_api_id": "gid://shopify/LineItem/703073504","fulfillable_quantity": 0,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - black","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008BLACK","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 457924702,"variant_inventory_management": "shopify","variant_title": "black","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.33","amount_set": {"shop_money": {"amount": "3.33","currency_code": "USD"},"presentment_money": {"amount": "3.33","currency_code": "USD"}},"discount_application_index": 0}]}},{"id": 709875399,"line_item_id": 466157049,"location_id": 487838322,"quantity": 1,"restock_type": "legacy_restock","subtotal": 195.67,"subtotal_set": {"shop_money": {"amount": "195.67","currency_code": "USD"},"presentment_money": {"amount": "195.67","currency_code": "USD"}},"total_tax": 3.98,"total_tax_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"line_item": {"id": 466157049,"admin_graphql_api_id": "gid://shopify/LineItem/466157049","fulfillable_quantity": 0,"fulfillment_service": "manual","fulfillment_status": null,"gift_card": false,"grams": 200,"name": "IPod Nano - 8gb - green","price": "199.00","price_set": {"shop_money": {"amount": "199.00","currency_code": "USD"},"presentment_money": {"amount": "199.00","currency_code": "USD"}},"product_exists": true,"product_id": 632910392,"properties": [{"name": "Custom Engraving Front","value": "Happy Birthday"},{"name": "Custom Engraving Back","value": "Merry Christmas"}],"quantity": 1,"requires_shipping": true,"sku": "IPOD2008GREEN","taxable": true,"title": "IPod Nano - 8gb","total_discount": "0.00","total_discount_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"variant_id": 39072856,"variant_inventory_management": "shopify","variant_title": "green","vendor": null,"tax_lines": [{"price": "3.98","price_set": {"shop_money": {"amount": "3.98","currency_code": "USD"},"presentment_money": {"amount": "3.98","currency_code": "USD"}},"rate": 0.06,"title": "State Tax"}],"discount_allocations": [{"amount": "3.34","amount_set": {"shop_money": {"amount": "3.34","currency_code": "USD"},"presentment_money": {"amount": "3.34","currency_code": "USD"}},"discount_application_index": 0}]}}]}],"shipping_address": {"first_name": "Bob","address1": "Chestnut Street 92","phone": "555-625-1199","city": "Louisville","zip": "40202","province": "Kentucky","country": "United States","last_name": "Norman","address2": "","company": null,"latitude": 45.41634,"longitude": -75.6868,"name": "Bob Norman","country_code": "US","province_code": "KY"},"shipping_lines": [{"id": 369256396,"carrier_identifier": null,"code": "Free Shipping","delivery_category": null,"discounted_price": "0.00","discounted_price_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"phone": null,"price": "0.00","price_set": {"shop_money": {"amount": "0.00","currency_code": "USD"},"presentment_money": {"amount": "0.00","currency_code": "USD"}},"requested_fulfillment_service_id": null,"source": "shopify","title": "Free Shipping","tax_lines": [],"discount_allocations": []}]}', - 'cid' => 19, - 'expected-order' => new Order([ - 'status' => Status::OPEN, - 'customer_id' => 19, - 'customer_reference' => '1001', - 'uuid' => '450789469', - 'origin' => 'Shopify', - 'notes' => '', - 'address_id' => '', - 'carrier_id' => Carrier::findOne(condition: ["name" => "FedEx"])->id, - 'service_id' => Service::findOne(["shipwise_code" => "FedExGround"])->id, - ]), - 'expected-items' => [ - new Item([ - - ]), - ], - ], - ]; - - foreach($testCases as $testCase) - { - $testCase['adapter'] = new ShopifyAdapter(orderJSON: $testCase['json'], customer_id: $testCase['cid']); - $testCase['order'] = $testCase['adapter']->parse(); - - $this->assertEquals(expected: $testCase['expected-order'], actual: $testCase['order']); - $this->assertEquals(expected: $testCase['expected-items'], actual: $testCase['adapter']->parseItems($testCase['order']->id)); - } - } -} \ No newline at end of file From 1b54cc46883fd7b90ed794801c942f2ed64dfd63 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 22 Feb 2023 12:46:52 +0200 Subject: [PATCH 02/81] E-commerce Platforms --- common/models/EcommercePlatform.php | 44 +++++++ common/models/base/BaseEcommercePlatform.php | 56 +++++++++ .../models/search/EcommercePlatformSearch.php | 73 +++++++++++ ...124952_create_table_ecommerce_platform.php | 38 ++++++ ...43_add_shopify_mock_ecommerce_platfort.php | 35 ++++++ .../EcommercePlatformController.php | 118 ++++++++++++++++++ frontend/views/ecommerce-platform/_form.php | 39 ++++++ frontend/views/ecommerce-platform/index.php | 86 +++++++++++++ frontend/views/ecommerce-platform/update.php | 28 +++++ frontend/views/ecommerce-platform/view.php | 65 ++++++++++ frontend/views/layouts/main.php | 1 + 11 files changed, 583 insertions(+) create mode 100644 common/models/EcommercePlatform.php create mode 100644 common/models/base/BaseEcommercePlatform.php create mode 100644 common/models/search/EcommercePlatformSearch.php create mode 100644 console/migrations/m230221_124952_create_table_ecommerce_platform.php create mode 100644 console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php create mode 100644 frontend/controllers/EcommercePlatformController.php create mode 100644 frontend/views/ecommerce-platform/_form.php create mode 100644 frontend/views/ecommerce-platform/index.php create mode 100644 frontend/views/ecommerce-platform/update.php create mode 100644 frontend/views/ecommerce-platform/view.php diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php new file mode 100644 index 00000000..b28858fe --- /dev/null +++ b/common/models/EcommercePlatform.php @@ -0,0 +1,44 @@ + 'Active', + self::STATUS_PLATFORM_INACTIVE => 'Inactive', + ]; + } + + public function isActive(): bool + { + return $this->status === self::STATUS_PLATFORM_ACTIVE; + } + + public function switchStatus(): void + { + $this->status = ($this->isActive()) ? self::STATUS_PLATFORM_INACTIVE : self::STATUS_PLATFORM_ACTIVE; + $this->save(); + } + + /** + * TODO: implement this later + */ + public function getConnectedCustomersCounter(): int + { + return 0; + } +} diff --git a/common/models/base/BaseEcommercePlatform.php b/common/models/base/BaseEcommercePlatform.php new file mode 100644 index 00000000..559753df --- /dev/null +++ b/common/models/base/BaseEcommercePlatform.php @@ -0,0 +1,56 @@ + null], + [['name'], 'required'], + [['status'], 'integer'], + [['meta'], 'string'], + [['created_date', 'updated_date'], 'safe'], + [['name'], 'string', 'max' => 128], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels(): array + { + return [ + 'id' => 'ID', + 'name' => 'Platform', + 'status' => 'Status', + 'meta' => 'Meta Data', + 'created_date' => 'Created Date', + 'updated_date' => 'Updated Date', + ]; + } +} diff --git a/common/models/search/EcommercePlatformSearch.php b/common/models/search/EcommercePlatformSearch.php new file mode 100644 index 00000000..30e50408 --- /dev/null +++ b/common/models/search/EcommercePlatformSearch.php @@ -0,0 +1,73 @@ + $query, + 'pagination' => false + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $this->id, + 'status' => $this->status, + 'created_date' => $this->created_date, + 'updated_date' => $this->updated_date, + ]); + + $query->andFilterWhere(['like', 'name', $this->name]) + ->andFilterWhere(['like', 'meta', $this->meta]); + + return $dataProvider; + } +} diff --git a/console/migrations/m230221_124952_create_table_ecommerce_platform.php b/console/migrations/m230221_124952_create_table_ecommerce_platform.php new file mode 100644 index 00000000..1d467ff3 --- /dev/null +++ b/console/migrations/m230221_124952_create_table_ecommerce_platform.php @@ -0,0 +1,38 @@ +execute(" + CREATE TABLE `ecommerce_platform` ( + `id` INT NOT NULL AUTO_INCREMENT , + `name` VARCHAR(128) NOT NULL , + `status` TINYINT NOT NULL DEFAULT '1' , + `meta` MEDIUMTEXT NULL DEFAULT NULL , + `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , + `updated_date` DATETIME NULL DEFAULT NULL , + PRIMARY KEY (`id`)) ENGINE = InnoDB; + "); + + $this->execute(" + ALTER TABLE `ecommerce_platform` CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP; + "); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable("{{%ecommerce_platform}}"); + } +} diff --git a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php new file mode 100644 index 00000000..f88f376a --- /dev/null +++ b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php @@ -0,0 +1,35 @@ +name = EcommercePlatform::SHOPIFY_PLATFORM_NAME; + $ecommercePlatform->save(); + + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $ecommercePlatform = EcommercePlatform::find() + ->where(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME]) + ->one(); + + if ($ecommercePlatform) { + $ecommercePlatform->delete(); + } + } +} diff --git a/frontend/controllers/EcommercePlatformController.php b/frontend/controllers/EcommercePlatformController.php new file mode 100644 index 00000000..48b63a7e --- /dev/null +++ b/frontend/controllers/EcommercePlatformController.php @@ -0,0 +1,118 @@ + [ + 'class' => AccessControl::class, + 'ruleConfig' => [ + 'class' => AccessRuleFilter::class, + ], + 'rules' => [ + [ + 'allow' => true, + 'roles' => ['admin'], + ], + ], + ], + ]; + } + + /** + * Lists all EcommercePlatform models. + * @return string + */ + public function actionIndex(): string + { + $searchModel = new EcommercePlatformSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single EcommercePlatform model. + * @param integer $id + * @return string + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionView(int $id): string + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Updates an existing EcommercePlatform model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return string|Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionUpdate(int $id): string|Response + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + Yii::$app->session->setFlash('success', 'Platform has been updated.'); + return $this->redirect(['index']); + } + + return $this->render('update', [ + 'model' => $model, + ]); + } + + /** + * Switches the status of a specific EcommercePlatform model. + * @throws NotFoundHttpException + */ + public function actionStatus(int $id): Response + { + $model = $this->findModel($id); + $model->switchStatus(); + Yii::$app->session->setFlash('success', 'Status has been updated.'); + + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * Finds the EcommercePlatform model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return EcommercePlatform the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel(int $id): EcommercePlatform + { + if (($model = EcommercePlatform::findOne($id)) !== null) { + return $model; + } + + throw new NotFoundHttpException('The requested page does not exist.'); + } +} diff --git a/frontend/views/ecommerce-platform/_form.php b/frontend/views/ecommerce-platform/_form.php new file mode 100644 index 00000000..7e7b8f03 --- /dev/null +++ b/frontend/views/ecommerce-platform/_form.php @@ -0,0 +1,39 @@ + Status'; +$metaDataLabel = ' Meta Data'; +?> + + + +
+
+ field($model, 'name') + ->textInput(['maxlength' => true, 'disabled' => true, 'required' => true]) ?> +
+
+ field($model, 'status') + ->dropdownList(EcommercePlatform::getStatuses(), ['prompt' => ' Status', 'required' => true]) + ->label($statusLabel) ?> + +
+
+ field($model, 'meta') + ->textarea(['rows' => 6]) + ->label($metaDataLabel) ?> +
+
+ +
+ 'btn btn-success']) ?> +
+ + diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php new file mode 100644 index 00000000..8bfe7a17 --- /dev/null +++ b/frontend/views/ecommerce-platform/index.php @@ -0,0 +1,86 @@ +title = $title . ' - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = $title; +?> + +
+

+ + request->getQueryParam('EcommercePlatformSearch')) { ?> + Clear filters + +

+ + $dataProvider, + 'filterModel' => $searchModel, + 'layout' => '{items}', + 'columns' => [ + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + $string = Html::encode($model->name); + $string .= '
Connected customers: ' . $model->getConnectedCustomersCounter() . ''; + + if ($model->updated_date) { + $string .= '
Last update: ' . Yii::$app->formatter->asDatetime($model->updated_date) . ''; + } + + return $string; + }, + ], + [ + 'attribute' => 'status', + 'format' => 'raw', + 'filter' => Html::activeDropDownList( + $searchModel, + 'status', + EcommercePlatform::getStatuses(), + ['class' => 'form-control', 'prompt' => 'All Statuses'] + ), + 'value' => function($model) { + if ($model->status == EcommercePlatform::STATUS_PLATFORM_ACTIVE) { + $string = ' Active'; + } else { + $string = ' Inactive'; + } + + return $string; + }, + ], + [ + 'class' => 'yii\grid\ActionColumn', + 'headerOptions' => ['class' => 'text-center'], + 'header' => 'Actions', + 'template' => '
{view} {update} {status}
', + 'buttons' => [ + 'status' => function ($url, $model, $key) { + $url = Url::to(['/ecommerce-platform/status', 'id' => $model->id]); + $icon = ($model->isActive()) + ? '' + : ''; + $confirm = ($model->isActive()) + ? 'Are you sure you want to make this platform inactive? In this case, all integrations with the platform will be paused.' + : 'Are you sure you want to make this platform active?'; + + return '' . $icon . ''; + }, + ] + ], + ], + ]); ?> +
diff --git a/frontend/views/ecommerce-platform/update.php b/frontend/views/ecommerce-platform/update.php new file mode 100644 index 00000000..5a1f231e --- /dev/null +++ b/frontend/views/ecommerce-platform/update.php @@ -0,0 +1,28 @@ +name; +$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; +$this->params['breadcrumbs'][] = 'Update'; +?> + +
+
+

+ +

+
+ +
+ render('_form', [ + 'model' => $model, + ]) ?> +
+
diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php new file mode 100644 index 00000000..ba7c964b --- /dev/null +++ b/frontend/views/ecommerce-platform/view.php @@ -0,0 +1,65 @@ +name; +$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $title; +?> + +
+

+ + + + + + +

+ + $model, + 'attributes' => [ + 'name', + [ + 'attribute' => 'status', + 'format' => 'raw', + 'value' => function($model) { + if ($model->status == EcommercePlatform::STATUS_PLATFORM_ACTIVE) { + $string = ' Active'; + } else { + $string = ' Inactive'; + } + + return $string; + }, + ], + [ + 'label' => 'Connected Customers', + 'format' => 'raw', + 'value' => function($model) { + return $model->getConnectedCustomersCounter(); + }, + ], + [ + 'attribute' => 'meta', + 'format' => 'raw', + 'value' => function ($model) { + return ($model->meta) + ? '
' . HtmlPurifier::process(nl2br($model->meta)) . '
' + : null; + }, + ], + 'created_date:datetime', + 'updated_date:datetime', + ], + ]) ?> +
diff --git a/frontend/views/layouts/main.php b/frontend/views/layouts/main.php index f0999447..8bd2036f 100755 --- a/frontend/views/layouts/main.php +++ b/frontend/views/layouts/main.php @@ -158,6 +158,7 @@ ['label' => 'Subscriptions', 'url' => ['/subscription']], ['label' => 'One-Time Charges', 'url' => ['/one-time-charge']], ['label' => 'Invoices', 'url' => ['/invoice']], + ['label' => 'Ecommerce Platforms', 'url' => ['/ecommerce-platform']], ['label' => 'Integrations', 'url' => ['/integration']], ['label' => 'Behaviors', 'url' => ['/behavior']], ['label' => 'Jobs', 'url' => ['/monitor/jobs']], From 7ca9f6a707d14c8936063f07b567908a04de6495 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Thu, 23 Feb 2023 15:24:46 +0200 Subject: [PATCH 03/81] Ecommerce Integrations --- common/models/EcommerceIntegration.php | 40 +++++ common/models/EcommercePlatform.php | 8 +- .../models/base/BaseEcommerceIntegration.php | 94 ++++++++++ common/models/base/BaseEcommercePlatform.php | 9 + .../models/query/EcommercePlatformQuery.php | 42 +++++ .../search/EcommerceIntegrationSearch.php | 75 ++++++++ ...112448_add_ecommerce_integration_table.php | 68 ++++++++ .../EcommerceIntegrationController.php | 163 ++++++++++++++++++ .../views/ecommerce-integration/_platform.php | 119 +++++++++++++ .../views/ecommerce-integration/index.php | 24 +++ frontend/views/ecommerce-platform/index.php | 8 +- frontend/views/ecommerce-platform/view.php | 27 ++- frontend/views/layouts/main.php | 2 + 13 files changed, 667 insertions(+), 12 deletions(-) create mode 100644 common/models/EcommerceIntegration.php create mode 100644 common/models/base/BaseEcommerceIntegration.php create mode 100644 common/models/query/EcommercePlatformQuery.php create mode 100644 common/models/search/EcommerceIntegrationSearch.php create mode 100644 console/migrations/m230222_112448_add_ecommerce_integration_table.php create mode 100644 frontend/controllers/EcommerceIntegrationController.php create mode 100644 frontend/views/ecommerce-integration/_platform.php create mode 100644 frontend/views/ecommerce-integration/index.php diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php new file mode 100644 index 00000000..bb38d63d --- /dev/null +++ b/common/models/EcommerceIntegration.php @@ -0,0 +1,40 @@ + 'Disconnected', + self::STATUS_INTEGRATION_CONNECTED => 'Connected', + self::STATUS_INTEGRATION_PAUSED => 'Paused', + ]; + } + + public function isConnected(): bool + { + return $this->status === self::STATUS_INTEGRATION_CONNECTED; + } + + public function isDisconnected(): bool + { + return $this->status === self::STATUS_INTEGRATION_DISCONNECTED; + } + + public function isPaused(): bool + { + return $this->status === self::STATUS_INTEGRATION_PAUSED; + } +} diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php index b28858fe..d1b0056f 100644 --- a/common/models/EcommercePlatform.php +++ b/common/models/EcommercePlatform.php @@ -3,6 +3,7 @@ namespace common\models; use common\models\base\BaseEcommercePlatform; +use yii\db\ActiveQuery; /** * Class EcommercePlatform @@ -23,6 +24,11 @@ public static function getStatuses(): array ]; } + public function getEcommerceIntegration(): ActiveQuery + { + return $this->hasOne(EcommerceIntegration::class, ['platform_id' => 'id']); + } + public function isActive(): bool { return $this->status === self::STATUS_PLATFORM_ACTIVE; @@ -37,7 +43,7 @@ public function switchStatus(): void /** * TODO: implement this later */ - public function getConnectedCustomersCounter(): int + public function getConnectedUsersCounter(): int { return 0; } diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php new file mode 100644 index 00000000..7087fa75 --- /dev/null +++ b/common/models/base/BaseEcommerceIntegration.php @@ -0,0 +1,94 @@ + null], + [['user_id', 'platform_id'], 'required'], + [['user_id', 'customer_id', 'platform_id', 'status'], 'integer'], + [['meta'], 'string'], + [['created_date', 'updated_date'], 'safe'], + [['customer_id'], 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id']], + [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::class, 'targetAttribute' => ['platform_id' => 'id']], + [['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels(): array + { + return [ + 'id' => 'ID', + 'user_id' => 'User ID', + 'customer_id' => 'Customer ID', + 'platform_id' => 'Platform ID', + 'status' => 'Status', + 'meta' => 'Meta Data', + 'created_date' => 'Created Date', + 'updated_date' => 'Updated Date', + ]; + } + + /** + * @return ActiveQuery + */ + public function getCustomer(): ActiveQuery + { + return $this->hasOne(Customer::class, ['id' => 'customer_id']); + } + + /** + * @return ActiveQuery + */ + public function getPlatform(): ActiveQuery + { + return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']); + } + + /** + * @return ActiveQuery + */ + public function getUser(): ActiveQuery + { + return $this->hasOne(User::class, ['id' => 'user_id']); + } +} diff --git a/common/models/base/BaseEcommercePlatform.php b/common/models/base/BaseEcommercePlatform.php index 559753df..10bce9cf 100644 --- a/common/models/base/BaseEcommercePlatform.php +++ b/common/models/base/BaseEcommercePlatform.php @@ -3,6 +3,7 @@ namespace common\models\base; use yii\db\ActiveRecord; +use common\models\query\EcommercePlatformQuery; /** * This is the model class for table "ecommerce_platform". @@ -16,6 +17,14 @@ */ class BaseEcommercePlatform extends ActiveRecord { + /** + * @return EcommercePlatformQuery + */ + public static function find(): EcommercePlatformQuery + { + return new EcommercePlatformQuery(get_called_class()); + } + /** * {@inheritdoc} */ diff --git a/common/models/query/EcommercePlatformQuery.php b/common/models/query/EcommercePlatformQuery.php new file mode 100644 index 00000000..db7d97fb --- /dev/null +++ b/common/models/query/EcommercePlatformQuery.php @@ -0,0 +1,42 @@ +andWhere(['status' => EcommercePlatform::STATUS_PLATFORM_ACTIVE]); + } + + public function for(?int $userId = null, ?int $customerId = null): EcommercePlatformQuery + { + $this->joinWith([ + 'ecommerceIntegration' => function ($query) use ($userId, $customerId) { + if ($userId) { + $query->onCondition(['ecommerce_integration.user_id' => $userId]); + } + + if ($customerId) { + $query->onCondition(['ecommerce_integration.customer_id' => $customerId]); + } + } + ]); + + return $this; + } + + public function orderById(int $sort = SORT_ASC): EcommercePlatformQuery + { + return $this + ->orderBy(['id' => $sort]); + } +} diff --git a/common/models/search/EcommerceIntegrationSearch.php b/common/models/search/EcommerceIntegrationSearch.php new file mode 100644 index 00000000..98ff1d4f --- /dev/null +++ b/common/models/search/EcommerceIntegrationSearch.php @@ -0,0 +1,75 @@ + $query, + 'pagination' => false + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'customer_id' => $this->customer_id, + 'platform_id' => $this->platform_id, + 'status' => $this->status, + 'created_date' => $this->created_date, + 'updated_date' => $this->updated_date, + ]); + + $query->andFilterWhere(['like', 'meta', $this->meta]); + + return $dataProvider; + } +} diff --git a/console/migrations/m230222_112448_add_ecommerce_integration_table.php b/console/migrations/m230222_112448_add_ecommerce_integration_table.php new file mode 100644 index 00000000..8863fdb0 --- /dev/null +++ b/console/migrations/m230222_112448_add_ecommerce_integration_table.php @@ -0,0 +1,68 @@ +execute(" + CREATE TABLE `ecommerce_integration` ( + `id` INT NOT NULL AUTO_INCREMENT , + `user_id` INT NOT NULL , + `customer_id` INT NOT NULL , + `platform_id` INT NOT NULL , + `status` TINYINT NOT NULL DEFAULT '0' , + `meta` MEDIUMTEXT NULL DEFAULT NULL , + `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , + `updated_date` DATETIME NULL DEFAULT NULL , + PRIMARY KEY (`id`)) ENGINE = InnoDB; + "); + + $this->execute(" + ALTER TABLE `ecommerce_integration` + CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP; + "); + + $this->addForeignKey( + '{{%fk-ecommerce_integration-user_id}}', + '{{%ecommerce_integration}}', + 'user_id', + '{{%user}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_integration-customer_id}}', + '{{%ecommerce_integration}}', + 'customer_id', + '{{%customers}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_integration-platform_id}}', + '{{%ecommerce_integration}}', + 'platform_id', + '{{%ecommerce_platform}}', + 'id', + 'CASCADE' + ); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable("{{%ecommerce_integration}}"); + } +} diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php new file mode 100644 index 00000000..3b501323 --- /dev/null +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -0,0 +1,163 @@ + [ + 'class' => AccessControl::class, + 'ruleConfig' => [ + 'class' => AccessRuleFilter::class, + ], + 'rules' => [ + [ + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + ]; + } + + /** + * Lists all EcommercePlatform models with their EcommerceIntegration for the current user. + * @return string + */ + public function actionIndex(): string + { + $ecommercePlatforms = EcommercePlatform::find() + ->with(['ecommerceIntegration']) + ->for(Yii::$app->user->id) + ->orderById() + ->all(); + + return $this->render('index', [ + 'models' => $ecommercePlatforms, + ]); + } + + /** + * @throws NotFoundHttpException + * @throws ServerErrorHttpException + */ + public function actionConnect(): Response + { + $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); + + if (!$ecommercePlatform->ecommerceIntegration) { + $ecommerceIntegration = new EcommerceIntegration(); + $ecommerceIntegration->user_id = Yii::$app->user->id; + /** + * TODO: implement adding `customer_id` + */ + //$ecommerceIntegration->customer_id = 1; + $ecommerceIntegration->platform_id = $ecommercePlatform->id; + $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + + if (!$ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); + } + } else { + $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + + if (!$ecommercePlatform->ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); + } + } + + Yii::$app->session->setFlash('success', 'Ecommerce platform has been connected.'); + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * @throws NotFoundHttpException + * @throws ServerErrorHttpException + */ + public function actionDisconnect(): Response + { + $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); + $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; + + if (!$ecommercePlatform->ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Ecommerce platform is not disconnected. Something went wrong.'); + } + + Yii::$app->session->setFlash('success', 'Ecommerce platform has been disconnected.'); + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * @throws NotFoundHttpException + * @throws ServerErrorHttpException + */ + public function actionPause(): Response + { + $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); + $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_PAUSED; + + if (!$ecommercePlatform->ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Ecommerce platform is not paused. Something went wrong.'); + } + + Yii::$app->session->setFlash('success', 'Ecommerce platform has been paused.'); + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * @throws NotFoundHttpException + * @throws ServerErrorHttpException + */ + public function actionResume(): Response + { + $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); + $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + + if (!$ecommercePlatform->ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Ecommerce platform is not resumed. Something went wrong.'); + } + + Yii::$app->session->setFlash('success', 'Ecommerce platform has been resumed.'); + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * @throws NotFoundHttpException + * @throws ServerErrorHttpException + */ + protected function getEcommercePlatformByName(string $name): EcommercePlatform + { + /** + * @var EcommercePlatform $model + */ + $model = EcommercePlatform::find() + ->with(['ecommerceIntegration']) + ->for(Yii::$app->user->id) + ->where(['name' => $name]) + ->one(); + + if (!$model) { + throw new NotFoundHttpException('Ecommerce platform does not exist.'); + } + + if (!$model->isActive()) { + throw new ServerErrorHttpException('Ecommerce platform is not active.'); + } + + return $model; + } +} diff --git a/frontend/views/ecommerce-integration/_platform.php b/frontend/views/ecommerce-integration/_platform.php new file mode 100644 index 00000000..04ee4d7b --- /dev/null +++ b/frontend/views/ecommerce-integration/_platform.php @@ -0,0 +1,119 @@ + + + $model, + 'options' => [ + 'id' => $model->id, + 'class' => 'table table-striped table-bordered detail-view' + ], + 'attributes' => [ + [ + 'label' => 'Platform:', + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + if ($model->isActive()) { + $string = ''; + } else { + $string = ''; + } + + return Html::encode($model->name) . ' ' . $string; + }, + ], + [ + 'label' => 'Status:', + 'format' => 'raw', + 'value' => function($model) { + /* @var $ecommerceIntegration EcommerceIntegration */ + $ecommerceIntegration = $model->ecommerceIntegration; + + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED] . ' ' . $icon . ''; + + if ($ecommerceIntegration && $ecommerceIntegration->status != EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED) { + if ($ecommerceIntegration->isConnected()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; + } elseif ($ecommerceIntegration->isPaused()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; + } + } + + return $string; + }, + ], + [ + 'label' => 'Processed orders:', + 'format' => 'raw', + 'value' => function($model) { + return 0; + }, + ], + [ + 'label' => 'Pending orders:', + 'format' => 'raw', + 'value' => function($model) { + return 0; + }, + ], + [ + 'label' => 'Actions:', + 'format' => 'raw', + 'visible' => $model->isActive(), + 'value' => function($model) use ($pauseConfirm, $disconnectConfirm) { + /* @var $ecommerceIntegration EcommerceIntegration */ + $ecommerceIntegration = $model->ecommerceIntegration; + + if (!$ecommerceIntegration) { + $status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; + } else { + $status = $ecommerceIntegration->status; + } + + switch ($status) { + case EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED: + $url = Url::to(['/ecommerce-integration/connect', 'platform' => $model->name]); + $buttons = 'Connect'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_CONNECTED: + $url = Url::to(['/ecommerce-integration/pause', 'platform' => $model->name]); + $buttons = 'Pause'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); + $buttons .= ' Disconnect'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: + $url = Url::to(['/ecommerce-integration/resume', 'platform' => $model->name]); + $buttons = 'Resume'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); + $buttons .= ' Disconnect'; + break; + default: + $buttons = ''; + } + + return $buttons; + }, + ], + ], +]) ?> diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php new file mode 100644 index 00000000..6ebda07b --- /dev/null +++ b/frontend/views/ecommerce-integration/index.php @@ -0,0 +1,24 @@ +title = $title . ' - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = $title; +?> + +
+

+ +

+ + + render('_platform', [ + 'model' => $model, + ]) ?> + +
diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php index 8bfe7a17..bb07649d 100644 --- a/frontend/views/ecommerce-platform/index.php +++ b/frontend/views/ecommerce-platform/index.php @@ -34,7 +34,7 @@ 'format' => 'raw', 'value' => function($model) { $string = Html::encode($model->name); - $string .= '
Connected customers: ' . $model->getConnectedCustomersCounter() . ''; + $string .= '
Connected users: ' . $model->getConnectedUsersCounter() . ''; if ($model->updated_date) { $string .= '
Last update: ' . Yii::$app->formatter->asDatetime($model->updated_date) . ''; @@ -54,9 +54,9 @@ ), 'value' => function($model) { if ($model->status == EcommercePlatform::STATUS_PLATFORM_ACTIVE) { - $string = ' Active'; + $string = 'Active '; } else { - $string = ' Inactive'; + $string = 'Inactive '; } return $string; @@ -74,7 +74,7 @@ ? '' : ''; $confirm = ($model->isActive()) - ? 'Are you sure you want to make this platform inactive? In this case, all integrations with the platform will be paused.' + ? 'Are you sure you want to make this platform inactive? In this case, all integrations (orders) with the platform will not be processed.' : 'Are you sure you want to make this platform active?'; return '' . $icon . ''; diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php index ba7c964b..522fadb6 100644 --- a/frontend/views/ecommerce-platform/view.php +++ b/frontend/views/ecommerce-platform/view.php @@ -28,28 +28,33 @@ $model, 'attributes' => [ - 'name', [ + 'label' => 'Platform:', + 'attribute' => 'name', + ], + [ + 'label' => 'Status:', 'attribute' => 'status', 'format' => 'raw', 'value' => function($model) { if ($model->status == EcommercePlatform::STATUS_PLATFORM_ACTIVE) { - $string = ' Active'; + $string = 'Active '; } else { - $string = ' Inactive'; + $string = 'Inactive '; } return $string; }, ], [ - 'label' => 'Connected Customers', + 'label' => 'Connected users:', 'format' => 'raw', 'value' => function($model) { - return $model->getConnectedCustomersCounter(); + return $model->getConnectedUsersCounter(); }, ], [ + 'label' => 'Meta data:', 'attribute' => 'meta', 'format' => 'raw', 'value' => function ($model) { @@ -58,8 +63,16 @@ : null; }, ], - 'created_date:datetime', - 'updated_date:datetime', + [ + 'label' => 'Created date:', + 'attribute' => 'created_date', + 'format' => 'datetime', + ], + [ + 'label' => 'Updated date:', + 'attribute' => 'updated_date', + 'format' => 'datetime', + ], ], ]) ?> diff --git a/frontend/views/layouts/main.php b/frontend/views/layouts/main.php index 8bd2036f..f4ff30f9 100755 --- a/frontend/views/layouts/main.php +++ b/frontend/views/layouts/main.php @@ -175,6 +175,8 @@ 'items' => [ ['label' => 'Account', 'url' => ['/user/settings/profile']], + ['label' => 'Integrations', 'url' => ['/ecommerce-integration']], + ['label' => 'Items', 'url' => ['/sku']], [ From 6716abb71c193cc19485bdee8d7d86cb7857d04c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 24 Feb 2023 15:06:49 +0200 Subject: [PATCH 04/81] Update console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php Co-authored-by: Alexander Makarov --- .../m230221_134343_add_shopify_mock_ecommerce_platfort.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php index f88f376a..85968837 100644 --- a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php +++ b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php @@ -6,7 +6,7 @@ /** * Class m230221_134343_add_shopify_mock_ecommerce_platfort */ -class m230221_134343_add_shopify_mock_ecommerce_platfort extends Migration +class m230221_134343_add_shopify_mock_ecommerce_platform extends Migration { /** * {@inheritdoc} From c7efa45ebf4880819ee6f322edf7111582b96756 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 24 Feb 2023 15:19:38 +0200 Subject: [PATCH 05/81] Update m230221_134343_add_shopify_mock_ecommerce_platfort.php --- ...343_add_shopify_mock_ecommerce_platfort.php | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php index 85968837..82166e55 100644 --- a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php +++ b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php @@ -1,7 +1,6 @@ name = EcommercePlatform::SHOPIFY_PLATFORM_NAME; - $ecommercePlatform->save(); - + $this->insert('{{%ecommerce_platform}}', [ + 'name' => 'Shopify' + ]); } /** @@ -24,12 +22,8 @@ public function safeUp() */ public function safeDown() { - $ecommercePlatform = EcommercePlatform::find() - ->where(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME]) - ->one(); - - if ($ecommercePlatform) { - $ecommercePlatform->delete(); - } + $this->delete('{{%ecommerce_platform}}', [ + 'name' => 'Shopify' + ]); } } From 09104d565e7e322a5fadd2330ba72e21659125e9 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 25 Feb 2023 21:12:00 +0200 Subject: [PATCH 06/81] Shopify OAuth implementation --- common/config/params.php | 16 +++ .../platforms/ConnectShopifyStoreForm.php | 100 ++++++++++++++++++ common/services/platforms/ShopifyService.php | 96 +++++++++++++++++ composer.json | 3 +- composer.lock | 53 +++++++++- .../EcommerceIntegrationController.php | 48 ++++++++- .../views/ecommerce-integration/index.php | 17 ++- .../views/ecommerce-integration/shopify.php | 58 ++++++++++ frontend/web/css/site.css | 8 ++ 9 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 common/models/forms/platforms/ConnectShopifyStoreForm.php create mode 100644 common/services/platforms/ShopifyService.php create mode 100644 frontend/views/ecommerce-integration/shopify.php diff --git a/common/config/params.php b/common/config/params.php index 496ad838..22038d9b 100755 --- a/common/config/params.php +++ b/common/config/params.php @@ -63,4 +63,20 @@ 'csvBoxS3Bucket' => '', 'csvBoxS3Path' => '', 'csvBoxImportKey' => '', + + /** + * Shopify + */ + 'shopify' => [ + /** + * Visit https://partners.shopify.com/ -> Apps -> Your App -> Overview -> Client credentials + */ + 'client_id' => '', + 'client_secret' => '', + /** + * You can set `https://shipwise.ngrok.io` to test it locally. + * Otherwise, if leave the default value, the current server's domain name will be used automatically. + */ + 'override_redirect_domain' => false, + ], ]; diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php new file mode 100644 index 00000000..3a0dad45 --- /dev/null +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -0,0 +1,100 @@ + ['name', 'url'], + self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'], + ]; + } + + public function rules(): array + { + return [ + [['name', 'url', 'code'], 'filter', 'filter' => 'trim'], + [['name', 'url', 'code'], 'string', 'max' => 128], + /** @see https://www.regextester.com/104785 */ + ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'], + + [['name', 'url'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], + [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST], + [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST], + + [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], + ]; + } + + public function attributeLabels(): array + { + return [ + 'name' => 'Shop Name', + 'url' => 'Shop URL', + 'code' => 'Code', + ]; + } + + public function validateShopName(): void + { + if (EcommerceIntegration::find() + ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%'])) + ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->exists()) + { + $this->addError('name', 'Shop name already exists.'); + } + } + + public function validateShopUrl(): void + { + if (EcommerceIntegration::find() + ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%'])) + ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->exists()) + { + $this->addError('url', 'Shop URL already exists.'); + } + } + + public function auth(): void + { + $this->saveShopName(); + + // Step 1 - Send request to receive access token + $shopifyService = new ShopifyService($this->url); + $shopifyService->auth(); + } + + /** + * @throws ServerErrorHttpException + */ + public function saveAccessToken(): void + { + // Step 2 - Receive and save access token: + $shopifyService = new ShopifyService($this->url); + $shopifyService->accessToken(Yii::$app->session->get('shop_name', 'Shop Name')); + } + + protected function saveShopName() + { + Yii::$app->session->set('shop_name', $this->name); + } +} diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php new file mode 100644 index 00000000..7d944889 --- /dev/null +++ b/common/services/platforms/ShopifyService.php @@ -0,0 +1,96 @@ +shopUrl = $shopUrl; + $this->token = $token; + + if (!$this->token) { // Authorize user's shop + $config = [ + 'ShopUrl' => $this->shopUrl, + 'ApiKey' => Yii::$app->params['shopify']['client_id'], + 'SharedSecret' => Yii::$app->params['shopify']['client_secret'], + ]; + } else { // Use existing user's shop + $config = [ + 'ShopUrl' => $this->shopUrl, + 'AccessToken' => $this->token, + ]; + } + + $this->shopify = new ShopifySDK($config); + } + + public function auth(): void + { + $redirectDomain = trim(Url::to(['/'], true), '/'); + + if (Yii::$app->params['shopify']['override_redirect_domain'] != false) { + $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain']; + } + + // Step 1 - Send request to receive access token: + AuthHelper::createAuthRequest($this->scopes, $redirectDomain . $this->redirectUrl); + exit; + } + + public function accessToken(string $shopName) + { + $accessToken = AuthHelper::createAuthRequest($this->scopes); + + $meta = [ + 'platform' => EcommercePlatform::SHOPIFY_PLATFORM_NAME, + 'shop_url' => $this->shopUrl, + 'shop_name' => $shopName, + 'access_token' => $accessToken, + ]; + + $ecommerceIntegration = new EcommerceIntegration(); + $ecommerceIntegration->user_id = Yii::$app->user->id; + $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; + $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT); + + if (!$ecommerceIntegration->save()) { + throw new ServerErrorHttpException('Shopify integration is not added. Something went wrong.'); + } + } + + public function makeReq() + { + echo '
';
+        print_r($this->shopify->Product->get());
+        exit;
+    }
+}
diff --git a/composer.json b/composer.json
index 99e72293..fe11121a 100755
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,8 @@
         "2amigos/2fa-library": "^2.0",
         "2amigos/qrcode-library": "^2.0",
         "frostealth/yii2-aws-s3": "~2.0",
-        "league/csv": "^9.8"
+        "league/csv": "^9.8",
+        "phpclassic/php-shopify": "^1.2"
     },
     "require-dev": {
         "yiisoft/yii2-debug": "~2.1.0",
diff --git a/composer.lock b/composer.lock
index 58928c02..c4484637 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "1367a46f8214849610c956ec18d57da1",
+    "content-hash": "4a8bcf4bc3f9f7b9dce6864dd222eee9",
     "packages": [
         {
             "name": "2amigos/2fa-library",
@@ -2649,6 +2649,57 @@
             },
             "time": "2020-07-07T09:29:14+00:00"
         },
+        {
+            "name": "phpclassic/php-shopify",
+            "version": "v1.2.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpclassic/php-shopify.git",
+                "reference": "1d05bc9c662b01e804b12ab6049cf707a43d4285"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpclassic/php-shopify/zipball/1d05bc9c662b01e804b12ab6049cf707a43d4285",
+                "reference": "1d05bc9c662b01e804b12ab6049cf707a43d4285",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPShopify\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Tareq Mahmood",
+                    "email": "tareqtms@yahoo.com"
+                }
+            ],
+            "description": "PHP SDK for Shopify API",
+            "homepage": "https://github.com/phpclassic/php-shopify",
+            "keywords": [
+                "php",
+                "sdk",
+                "shopify"
+            ],
+            "support": {
+                "issues": "https://github.com/phpclassic/php-shopify/issues",
+                "source": "https://github.com/phpclassic/php-shopify/tree/v1.2.5"
+            },
+            "time": "2023-02-13T03:51:33+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "3.0.0",
diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index 3b501323..cc71544b 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -3,13 +3,13 @@
 namespace frontend\controllers;
 
 use Yii;
+use yii\helpers\Url;
 use yii\web\Response;
-use common\models\EcommercePlatform;
 use yii\filters\AccessControl;
-use yii\web\NotFoundHttpException;
 use Da\User\Filter\AccessRuleFilter;
-use common\models\EcommerceIntegration;
-use yii\web\ServerErrorHttpException;
+use common\models\{EcommercePlatform, EcommerceIntegration};
+use common\models\forms\platforms\ConnectShopifyStoreForm;
+use yii\web\{NotFoundHttpException, ServerErrorHttpException};
 
 class EcommerceIntegrationController extends Controller
 {
@@ -34,6 +34,46 @@ public function behaviors(): array
         ];
     }
 
+    /**
+     * Connects Shopify shop.
+     * @return string|Response
+     * @throws ServerErrorHttpException
+     */
+    public function actionShopify(): string|Response
+    {
+        if (Yii::$app->request->isPost) {
+            // Step 1 - Send request to receive access token:
+            $model = new ConnectShopifyStoreForm([
+                'scenario' => ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST
+            ]);
+            $model->load(Yii::$app->request->post());
+
+            if ($model->validate()) {
+                $model->auth();
+            }
+        } elseif (Yii::$app->request->get('code')) {
+            // Step 2 - Receive and save access token:
+            $model = new ConnectShopifyStoreForm([
+                'scenario' => ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN,
+                'url' => Yii::$app->request->get('shop'),
+                'code' => Yii::$app->request->get('code')
+            ]);
+
+            if ($model->validate()) {
+                $model->saveAccessToken();
+
+                Yii::$app->session->setFlash('success', 'Shopify shop has been connected.');
+                return $this->redirect(['index']);
+            }
+        } else {
+            $model = new ConnectShopifyStoreForm();
+        }
+
+        return $this->render('shopify', [
+            'model' => $model,
+        ]);
+    }
+
     /**
      * Lists all EcommercePlatform models with their EcommerceIntegration for the current user.
      * @return string
diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php
index 6ebda07b..c2886e78 100644
--- a/frontend/views/ecommerce-integration/index.php
+++ b/frontend/views/ecommerce-integration/index.php
@@ -1,6 +1,7 @@
 
     
 
-    
-        render('_platform', [
-            'model' => $model,
-        ]) ?>
-    
+    
+
+    
+ + render('_platform', [ + 'model' => $model, + ]) ?> + +
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php new file mode 100644 index 00000000..e5293e29 --- /dev/null +++ b/frontend/views/ecommerce-integration/shopify.php @@ -0,0 +1,58 @@ +title = $title . ' - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Integrations', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $title; +?> + +
+

+ +

+ +
+
+ Please input your Shopify shop name and its URL without http(s). + Example: myshop.myshopify.com. +
+ +
+
+ field($model, 'name') + ->textInput([ + 'class' => 'form-control', + 'placeholder' => $model->getAttributeLabel('name') . '...', + 'required' => true, + 'autofocus' => true, + 'maxlength' => true + ]) ?> +
+
+ field($model, 'url') + ->textInput([ + 'class' => 'form-control', + 'placeholder' => $model->getAttributeLabel('url') . '...', + 'required' => true, + 'maxlength' => true + ]) ?> +
+
+ 'btn btn-success', + ]) ?> +
+
+ +
+
diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 48a16aa3..499177ee 100755 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -383,3 +383,11 @@ pre { .form-actions { margin: 0 0 15px 0 !important; } + +.mb-2 { + margin-bottom: 20px; +} + +.mt-2 { + margin-top: 20px; +} From 770e859078f1086e989cc572851b4032fcab61f2 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 17:15:35 +0200 Subject: [PATCH 07/81] Some changes in the logic + actions {disconnect/pause/resume} --- common/models/EcommerceIntegration.php | 40 +++++- common/models/EcommercePlatform.php | 5 + .../models/base/BaseEcommerceIntegration.php | 11 +- common/models/base/BaseEcommercePlatform.php | 9 -- .../platforms/ConnectShopifyStoreForm.php | 15 ++- .../query/EcommerceIntegrationQuery.php | 31 +++++ .../models/query/EcommercePlatformQuery.php | 42 ------ common/services/platforms/ShopifyService.php | 11 +- .../EcommerceIntegrationController.php | 125 ++++++++---------- .../views/ecommerce-integration/_platform.php | 124 ++--------------- .../views/ecommerce-integration/_shopify.php | 107 +++++++++++++++ .../views/ecommerce-integration/index.php | 23 +++- .../views/ecommerce-integration/shopify.php | 3 +- frontend/views/ecommerce-platform/index.php | 2 +- frontend/views/ecommerce-platform/update.php | 4 +- frontend/views/ecommerce-platform/view.php | 4 +- frontend/views/layouts/main.php | 2 +- 17 files changed, 295 insertions(+), 263 deletions(-) create mode 100644 common/models/query/EcommerceIntegrationQuery.php delete mode 100644 common/models/query/EcommercePlatformQuery.php create mode 100644 frontend/views/ecommerce-integration/_shopify.php diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index bb38d63d..a066ffc4 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -2,6 +2,7 @@ namespace common\models; +use yii\helpers\Json; use common\models\base\BaseEcommerceIntegration; /** @@ -10,14 +11,20 @@ */ class EcommerceIntegration extends BaseEcommerceIntegration { - public const STATUS_INTEGRATION_DISCONNECTED = 0; public const STATUS_INTEGRATION_CONNECTED = 1; - public const STATUS_INTEGRATION_PAUSED = -1; + public const STATUS_INTEGRATION_PAUSED = 0; + + public array $array_meta_data = []; + + public function init(): void + { + parent::init(); + $this->on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']); + } public static function getStatuses(): array { return [ - self::STATUS_INTEGRATION_DISCONNECTED => 'Disconnected', self::STATUS_INTEGRATION_CONNECTED => 'Connected', self::STATUS_INTEGRATION_PAUSED => 'Paused', ]; @@ -28,13 +35,32 @@ public function isConnected(): bool return $this->status === self::STATUS_INTEGRATION_CONNECTED; } - public function isDisconnected(): bool + public function isPaused(): bool { - return $this->status === self::STATUS_INTEGRATION_DISCONNECTED; + return $this->status === self::STATUS_INTEGRATION_PAUSED; } - public function isPaused(): bool + public function disconnect(): bool|int { - return $this->status === self::STATUS_INTEGRATION_PAUSED; + return $this->delete(); + } + + public function pause(): bool + { + $this->status = self::STATUS_INTEGRATION_PAUSED; + return $this->save(); + } + + public function resume(): bool + { + $this->status = self::STATUS_INTEGRATION_CONNECTED; + return $this->save(); + } + + protected function convertMetaData(): void + { + if ($this->meta) { + $this->array_meta_data = Json::decode($this->meta); + } } } diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php index d1b0056f..ec5ba303 100644 --- a/common/models/EcommercePlatform.php +++ b/common/models/EcommercePlatform.php @@ -47,4 +47,9 @@ public function getConnectedUsersCounter(): int { return 0; } + + public static function getShopifyObject(): ?EcommercePlatform + { + return self::findOne(['name' => self::SHOPIFY_PLATFORM_NAME]); + } } diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php index 7087fa75..6e5ad507 100644 --- a/common/models/base/BaseEcommerceIntegration.php +++ b/common/models/base/BaseEcommerceIntegration.php @@ -4,6 +4,7 @@ use yii\db\ActiveRecord; use yii\db\ActiveQuery; +use common\models\query\EcommerceIntegrationQuery; use common\models\EcommercePlatform; use common\models\Customer; use frontend\models\User; @@ -26,6 +27,14 @@ */ class BaseEcommerceIntegration extends ActiveRecord { + /** + * @return EcommerceIntegrationQuery + */ + public static function find(): EcommerceIntegrationQuery + { + return new EcommerceIntegrationQuery(get_called_class()); + } + /** * {@inheritdoc} */ @@ -79,7 +88,7 @@ public function getCustomer(): ActiveQuery /** * @return ActiveQuery */ - public function getPlatform(): ActiveQuery + public function getEcommercePlatform(): ActiveQuery { return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']); } diff --git a/common/models/base/BaseEcommercePlatform.php b/common/models/base/BaseEcommercePlatform.php index 10bce9cf..559753df 100644 --- a/common/models/base/BaseEcommercePlatform.php +++ b/common/models/base/BaseEcommercePlatform.php @@ -3,7 +3,6 @@ namespace common\models\base; use yii\db\ActiveRecord; -use common\models\query\EcommercePlatformQuery; /** * This is the model class for table "ecommerce_platform". @@ -17,14 +16,6 @@ */ class BaseEcommercePlatform extends ActiveRecord { - /** - * @return EcommercePlatformQuery - */ - public static function find(): EcommercePlatformQuery - { - return new EcommercePlatformQuery(get_called_class()); - } - /** * {@inheritdoc} */ diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index 3a0dad45..efc8b81c 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -5,11 +5,16 @@ use Yii; use yii\base\Model; use yii\db\Expression; +use PHPShopify\Exception\SdkException; +use yii\web\ServerErrorHttpException; use common\services\platforms\ShopifyService; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; -use yii\web\ServerErrorHttpException; +/** + * Class ConnectShopifyStoreForm + * @package common\models\forms\platforms + */ class ConnectShopifyStoreForm extends Model { public const SCENARIO_AUTH_REQUEST = 'scenarioAuthRequest'; @@ -56,7 +61,7 @@ public function validateShopName(): void { if (EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) ->exists()) { $this->addError('name', 'Shop name already exists.'); @@ -67,13 +72,16 @@ public function validateShopUrl(): void { if (EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) ->exists()) { $this->addError('url', 'Shop URL already exists.'); } } + /** + * @throws SdkException + */ public function auth(): void { $this->saveShopName(); @@ -85,6 +93,7 @@ public function auth(): void /** * @throws ServerErrorHttpException + * @throws SdkException */ public function saveAccessToken(): void { diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php new file mode 100644 index 00000000..8b905414 --- /dev/null +++ b/common/models/query/EcommerceIntegrationQuery.php @@ -0,0 +1,31 @@ +andWhere(['user_id' => $userId]); + } + + if ($customerId) { + $this->andWhere(['customer_id' => $customerId]); + } + + return $this; + } + + public function orderById(int $sort = SORT_ASC): EcommerceIntegrationQuery + { + return $this + ->orderBy(['id' => $sort]); + } +} diff --git a/common/models/query/EcommercePlatformQuery.php b/common/models/query/EcommercePlatformQuery.php deleted file mode 100644 index db7d97fb..00000000 --- a/common/models/query/EcommercePlatformQuery.php +++ /dev/null @@ -1,42 +0,0 @@ -andWhere(['status' => EcommercePlatform::STATUS_PLATFORM_ACTIVE]); - } - - public function for(?int $userId = null, ?int $customerId = null): EcommercePlatformQuery - { - $this->joinWith([ - 'ecommerceIntegration' => function ($query) use ($userId, $customerId) { - if ($userId) { - $query->onCondition(['ecommerce_integration.user_id' => $userId]); - } - - if ($customerId) { - $query->onCondition(['ecommerce_integration.customer_id' => $customerId]); - } - } - ]); - - return $this; - } - - public function orderById(int $sort = SORT_ASC): EcommercePlatformQuery - { - return $this - ->orderBy(['id' => $sort]); - } -} diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 7d944889..181e78d8 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -10,6 +10,7 @@ use yii\helpers\Json; use yii\helpers\Url; use yii\web\ServerErrorHttpException; +use PHPShopify\Exception\SdkException; /** * Class ShopifyService @@ -52,6 +53,9 @@ public function __construct(string $shopUrl, string $token = null) $this->shopify = new ShopifySDK($config); } + /** + * @throws SdkException + */ public function auth(): void { $redirectDomain = trim(Url::to(['/'], true), '/'); @@ -65,8 +69,13 @@ public function auth(): void exit; } - public function accessToken(string $shopName) + /** + * @throws SdkException + * @throws ServerErrorHttpException + */ + public function accessToken(string $shopName): void { + // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); $meta = [ diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index cc71544b..a92250a1 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -3,10 +3,10 @@ namespace frontend\controllers; use Yii; -use yii\helpers\Url; use yii\web\Response; use yii\filters\AccessControl; use Da\User\Filter\AccessRuleFilter; +use PHPShopify\Exception\SdkException; use common\models\{EcommercePlatform, EcommerceIntegration}; use common\models\forms\platforms\ConnectShopifyStoreForm; use yii\web\{NotFoundHttpException, ServerErrorHttpException}; @@ -35,12 +35,32 @@ public function behaviors(): array } /** - * Connects Shopify shop. - * @return string|Response + * Lists all EcommerceIntegration models for the current user. + * @return string + */ + public function actionIndex(): string + { + $ecommerceIntegrations = EcommerceIntegration::find() + ->with(['ecommercePlatform']) + ->for(Yii::$app->user->id) + ->orderById() + ->all(); + + return $this->render('index', [ + 'models' => $ecommerceIntegrations, + ]); + } + + /** + * Connects a new Shopify shop. + * @throws SdkException + * @throws NotFoundHttpException * @throws ServerErrorHttpException */ public function actionShopify(): string|Response { + $this->checkEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME); + if (Yii::$app->request->isPost) { // Step 1 - Send request to receive access token: $model = new ConnectShopifyStoreForm([ @@ -75,118 +95,79 @@ public function actionShopify(): string|Response } /** - * Lists all EcommercePlatform models with their EcommerceIntegration for the current user. - * @return string - */ - public function actionIndex(): string - { - $ecommercePlatforms = EcommercePlatform::find() - ->with(['ecommerceIntegration']) - ->for(Yii::$app->user->id) - ->orderById() - ->all(); - - return $this->render('index', [ - 'models' => $ecommercePlatforms, - ]); - } - - /** + * Disconnects a needed EcommerceIntegration model. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionConnect(): Response + public function actionDisconnect(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - - if (!$ecommercePlatform->ecommerceIntegration) { - $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = Yii::$app->user->id; - /** - * TODO: implement adding `customer_id` - */ - //$ecommerceIntegration->customer_id = 1; - $ecommerceIntegration->platform_id = $ecommercePlatform->id; - $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; - - if (!$ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); - } - } else { - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->disconnect(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); - } - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been disconnected.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been connected.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Makes a needed EcommerceIntegration model paused. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionDisconnect(): Response + public function actionPause(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->pause(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not disconnected. Something went wrong.'); - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been paused.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been disconnected.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Makes a needed EcommerceIntegration model active again. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionPause(): Response + public function actionResume(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_PAUSED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->resume(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not paused. Something went wrong.'); - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been resumed.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been paused.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Returns a needed EcommerceIntegration model by its ID. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionResume(): Response + protected function getEcommerceIntegrationById(int $id): EcommerceIntegration { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + $model = EcommerceIntegration::find() + ->for(Yii::$app->user->id) + ->where(['id' => $id]) + ->one(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not resumed. Something went wrong.'); + if (!$model) { + throw new NotFoundHttpException('Ecommerce integration does not exist.'); } - Yii::$app->session->setFlash('success', 'Ecommerce platform has been resumed.'); - return $this->redirect(Yii::$app->request->referrer); + /** + * @var $model EcommerceIntegration + */ + return $model; } /** + * Checks a needed EcommercePlatform model by the provided name. It must exist and be active. * @throws NotFoundHttpException * @throws ServerErrorHttpException */ - protected function getEcommercePlatformByName(string $name): EcommercePlatform + protected function checkEcommercePlatformByName(string $name): void { /** * @var EcommercePlatform $model */ $model = EcommercePlatform::find() - ->with(['ecommerceIntegration']) - ->for(Yii::$app->user->id) ->where(['name' => $name]) ->one(); @@ -197,7 +178,5 @@ protected function getEcommercePlatformByName(string $name): EcommercePlatform if (!$model->isActive()) { throw new ServerErrorHttpException('Ecommerce platform is not active.'); } - - return $model; } } diff --git a/frontend/views/ecommerce-integration/_platform.php b/frontend/views/ecommerce-integration/_platform.php index 04ee4d7b..997573bd 100644 --- a/frontend/views/ecommerce-integration/_platform.php +++ b/frontend/views/ecommerce-integration/_platform.php @@ -1,119 +1,19 @@ - $model, - 'options' => [ - 'id' => $model->id, - 'class' => 'table table-striped table-bordered detail-view' - ], - 'attributes' => [ - [ - 'label' => 'Platform:', - 'attribute' => 'name', - 'format' => 'raw', - 'value' => function($model) { - if ($model->isActive()) { - $string = ''; - } else { - $string = ''; - } - - return Html::encode($model->name) . ' ' . $string; - }, - ], - [ - 'label' => 'Status:', - 'format' => 'raw', - 'value' => function($model) { - /* @var $ecommerceIntegration EcommerceIntegration */ - $ecommerceIntegration = $model->ecommerceIntegration; - - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED] . ' ' . $icon . ''; - - if ($ecommerceIntegration && $ecommerceIntegration->status != EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED) { - if ($ecommerceIntegration->isConnected()) { - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; - } elseif ($ecommerceIntegration->isPaused()) { - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; - } - } - - return $string; - }, - ], - [ - 'label' => 'Processed orders:', - 'format' => 'raw', - 'value' => function($model) { - return 0; - }, - ], - [ - 'label' => 'Pending orders:', - 'format' => 'raw', - 'value' => function($model) { - return 0; - }, - ], - [ - 'label' => 'Actions:', - 'format' => 'raw', - 'visible' => $model->isActive(), - 'value' => function($model) use ($pauseConfirm, $disconnectConfirm) { - /* @var $ecommerceIntegration EcommerceIntegration */ - $ecommerceIntegration = $model->ecommerceIntegration; - - if (!$ecommerceIntegration) { - $status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; - } else { - $status = $ecommerceIntegration->status; - } - - switch ($status) { - case EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED: - $url = Url::to(['/ecommerce-integration/connect', 'platform' => $model->name]); - $buttons = 'Connect'; - break; - case EcommerceIntegration::STATUS_INTEGRATION_CONNECTED: - $url = Url::to(['/ecommerce-integration/pause', 'platform' => $model->name]); - $buttons = 'Pause'; - - $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); - $buttons .= ' Disconnect'; - break; - case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: - $url = Url::to(['/ecommerce-integration/resume', 'platform' => $model->name]); - $buttons = 'Resume'; - - $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); - $buttons .= ' Disconnect'; - break; - default: - $buttons = ''; - } - - return $buttons; - }, - ], - ], -]) ?> +ecommercePlatform->name) { + case EcommercePlatform::SHOPIFY_PLATFORM_NAME: + echo $this->render('_shopify', [ + 'model' => $model, + ]); + break; + default: + } +?> diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php new file mode 100644 index 00000000..51519016 --- /dev/null +++ b/frontend/views/ecommerce-integration/_shopify.php @@ -0,0 +1,107 @@ + + + $model, + 'options' => [ + 'id' => $model->id, + 'class' => 'table table-striped table-bordered detail-view' + ], + 'attributes' => [ + [ + 'label' => 'Platform:', + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + if ($model->ecommercePlatform->isActive()) { + $string = ''; + } else { + $string = ''; + } + + return Html::encode($model->ecommercePlatform->name) . ' ' . $string; + }, + ], + [ + 'label' => 'Status:', + 'format' => 'raw', + 'value' => function($model) { + if ($model->isConnected()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; + } elseif ($model->isPaused()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; + } else { + $string = null; + } + + return $string; + }, + ], + [ + 'label' => 'Shop name:', + 'format' => 'raw', + 'value' => function($model) { + $name = isset($model->array_meta_data['shop_name']) ? Html::encode($model->array_meta_data['shop_name']) : null; + return $name; + }, + ], + [ + 'label' => 'Shop URL:', + 'format' => 'raw', + 'value' => function($model) { + return '' . $model->array_meta_data['shop_url'] . ' '; + }, + ], + [ + 'label' => 'Connected:', + 'attribute' => 'created_date', + 'format' => 'datetime', + ], + [ + 'label' => 'Actions:', + 'format' => 'raw', + 'visible' => $model->ecommercePlatform->isActive(), + 'value' => function($model) use ($pauseConfirm, $disconnectConfirm) { + $status = $model->status; + + switch ($status) { + case EcommerceIntegration::STATUS_INTEGRATION_CONNECTED: + $url = Url::to(['/ecommerce-integration/pause', 'id' => $model->id]); + $buttons = 'Pause'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Disconnect'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: + $url = Url::to(['/ecommerce-integration/resume', 'id' => $model->id]); + $buttons = 'Resume'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Disconnect'; + break; + default: + $buttons = ''; + } + + return $buttons; + }, + ], + ], +]) ?> diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php index c2886e78..0de06906 100644 --- a/frontend/views/ecommerce-integration/index.php +++ b/frontend/views/ecommerce-integration/index.php @@ -2,12 +2,12 @@ use yii\web\View; use yii\helpers\Html; use yii\helpers\Url; -use common\models\EcommercePlatform; +use common\models\EcommerceIntegration; /* @var $this View */ -/* @var $models EcommercePlatform[] */ +/* @var $models EcommerceIntegration[] */ -$title = 'Ecommerce Integrations'; +$title = 'E-commerce Integrations'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = $title; ?> @@ -22,10 +22,19 @@
- - render('_platform', [ - 'model' => $model, - ]) ?> + + + render('_platform', [ + 'model' => $model, + ]) ?> + + +
+

+ No connected shops yet. + Please connect your first shop. +

+
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index e5293e29..998a0943 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -1,7 +1,6 @@ title = $title . ' - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Integrations', 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php index bb07649d..3e6a09c1 100644 --- a/frontend/views/ecommerce-platform/index.php +++ b/frontend/views/ecommerce-platform/index.php @@ -11,7 +11,7 @@ /* @var $searchModel EcommercePlatformSearch */ /* @var $dataProvider ActiveDataProvider */ -$title = 'Ecommerce Platforms'; +$title = 'E-commerce Platforms'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/ecommerce-platform/update.php b/frontend/views/ecommerce-platform/update.php index 5a1f231e..cf245d99 100644 --- a/frontend/views/ecommerce-platform/update.php +++ b/frontend/views/ecommerce-platform/update.php @@ -7,8 +7,8 @@ /* @var $model EcommercePlatform */ $title = 'Update ' . $model->name; -$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->title = $title . ' - E-commerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Platforms', 'url' => ['index']]; $this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; $this->params['breadcrumbs'][] = 'Update'; ?> diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php index 522fadb6..2d50506b 100644 --- a/frontend/views/ecommerce-platform/view.php +++ b/frontend/views/ecommerce-platform/view.php @@ -10,8 +10,8 @@ /* @var $model EcommercePlatform */ $title = $model->name; -$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->title = $title . ' - E-commerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Platforms', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/layouts/main.php b/frontend/views/layouts/main.php index f4ff30f9..799c256c 100755 --- a/frontend/views/layouts/main.php +++ b/frontend/views/layouts/main.php @@ -158,7 +158,7 @@ ['label' => 'Subscriptions', 'url' => ['/subscription']], ['label' => 'One-Time Charges', 'url' => ['/one-time-charge']], ['label' => 'Invoices', 'url' => ['/invoice']], - ['label' => 'Ecommerce Platforms', 'url' => ['/ecommerce-platform']], + ['label' => 'E-commerce Platforms', 'url' => ['/ecommerce-platform']], ['label' => 'Integrations', 'url' => ['/integration']], ['label' => 'Behaviors', 'url' => ['/behavior']], ['label' => 'Jobs', 'url' => ['/monitor/jobs']], From d1a56eb4d4294df3034e7a47782be5c7012a788a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 19:35:14 +0200 Subject: [PATCH 08/81] Shopify documentation --- intro-docs/ecommerce-platfroms.md | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 intro-docs/ecommerce-platfroms.md diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md new file mode 100644 index 00000000..2366593c --- /dev/null +++ b/intro-docs/ecommerce-platfroms.md @@ -0,0 +1,54 @@ +# E-commerce Platforms + +### How to connect a new platform: + +1. Create a new `migration`. + +2. Implement a new `insert` query for the table `ecommerce_platform`. Specify the needed platform. Example - `console\migrations\m230221_134343_add_shopify_mock_ecommerce_platform.php`. + +3. Add implementation to: +- `common\models\EcommercePlatform.php` +- `common\models\EcommerceIntegration.php` +- `frontend\controllers\EcommercePlatformController.php` +- `frontend\controllers\EcommerceIntegrationController.php` + +### How to manage existing platforms: + +1. Visit our website - `/ecommerce-platform` (you must be an `Admin`). + +# Shopify + +### App: + +1. Register a new partner account - `https://www.shopify.com/partners`. + +2. Go to `Apps` -> `Create app` -> `Create app manually`. Copy `Client ID` and `Client secret`. +Insert them in `common\config\params-local.php` (`Shopify section`). + +3. Go to `Apps` -> `App setup`. In the `URLs` section specify the parameters for `App URL` and `Allowed redirection URL(s)`. +If you're going to **test it locally**, specify: + +- `App URL`: `https://shipwise.ngrok.io/` +- `Allowed redirection URL(s)`: `https://shipwise.ngrok.io/ecommerce-integration/shopify` + +4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain` +to `https://shipwise.ngrok.io`. + +### Test shop(s): + +1. Go to `https://partners.shopify.com/` -> `Stores`. + +2. Press `Add store` -> `Create developemrnt store`. + +3. Choose `Development store use`, specify `Store name`, specify `Store URL`. +In `Data and configurations`, choose `Start with test data`. + +4. You can create several test shops if needed. + +### Connect a test shop: + +1. Visit our website - `/ecommerce-integration/index`. Press the button `Connect Shopify shop`. + +2. In the form, specify your test shop's details (`name` and `URL`). + +3. If you want to remove the Shopify app from your test shop, visit `https://admin.shopify.com/` -> `Apps` -> `Apps and sales channels` -> and press `Uninstall`. From 11d73ba378e223f9f85e66c5d1e7d145a7772664 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 20:03:02 +0200 Subject: [PATCH 09/81] Connected shops --- common/models/EcommercePlatform.php | 9 ++++----- frontend/views/ecommerce-platform/index.php | 2 +- frontend/views/ecommerce-platform/view.php | 4 ++-- intro-docs/ecommerce-platfroms.md | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php index ec5ba303..6895fa9c 100644 --- a/common/models/EcommercePlatform.php +++ b/common/models/EcommercePlatform.php @@ -40,12 +40,11 @@ public function switchStatus(): void $this->save(); } - /** - * TODO: implement this later - */ - public function getConnectedUsersCounter(): int + public function getConnectedShopsCounter(): int { - return 0; + return EcommerceIntegration::find() + ->where(['platform_id' => $this->id]) + ->count(); } public static function getShopifyObject(): ?EcommercePlatform diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php index 3e6a09c1..76471650 100644 --- a/frontend/views/ecommerce-platform/index.php +++ b/frontend/views/ecommerce-platform/index.php @@ -34,7 +34,7 @@ 'format' => 'raw', 'value' => function($model) { $string = Html::encode($model->name); - $string .= '
Connected users: ' . $model->getConnectedUsersCounter() . ''; + $string .= '
Connected shops: ' . $model->getConnectedShopsCounter() . ''; if ($model->updated_date) { $string .= '
Last update: ' . Yii::$app->formatter->asDatetime($model->updated_date) . ''; diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php index 2d50506b..ef5de95e 100644 --- a/frontend/views/ecommerce-platform/view.php +++ b/frontend/views/ecommerce-platform/view.php @@ -47,10 +47,10 @@ }, ], [ - 'label' => 'Connected users:', + 'label' => 'Connected shops:', 'format' => 'raw', 'value' => function($model) { - return $model->getConnectedUsersCounter(); + return $model->getConnectedShopsCounter(); }, ], [ diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 2366593c..0fb6888f 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -38,7 +38,7 @@ to `https://shipwise.ngrok.io`. 1. Go to `https://partners.shopify.com/` -> `Stores`. -2. Press `Add store` -> `Create developemrnt store`. +2. Press `Add store` -> `Create development store`. 3. Choose `Development store use`, specify `Store name`, specify `Store URL`. In `Data and configurations`, choose `Start with test data`. From 0e1338ab346bbd642065ea9885a419194ce65208 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Thu, 2 Mar 2023 20:14:12 +0200 Subject: [PATCH 10/81] Update --- .../models/base/BaseEcommerceIntegration.php | 2 +- .../platforms/ConnectShopifyStoreForm.php | 22 ++- .../query/EcommerceIntegrationQuery.php | 6 + .../services/platforms/CreateOrderService.php | 128 +++++++++++++ common/services/platforms/ShopifyService.php | 131 ++++++++++--- .../jobs/platforms/ParseShopifyOrderJob.php | 180 ++++++++++++++++++ .../EcommerceIntegrationController.php | 29 +++ .../{_platform.php => _integration.php} | 0 .../views/ecommerce-integration/_shopify.php | 7 + .../views/ecommerce-integration/index.php | 7 +- .../views/ecommerce-integration/shopify.php | 18 ++ intro-docs/ecommerce-platfroms.md | 7 +- 12 files changed, 507 insertions(+), 30 deletions(-) create mode 100644 common/services/platforms/CreateOrderService.php create mode 100644 console/jobs/platforms/ParseShopifyOrderJob.php rename frontend/views/ecommerce-integration/{_platform.php => _integration.php} (100%) diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php index 6e5ad507..c4de3a64 100644 --- a/common/models/base/BaseEcommerceIntegration.php +++ b/common/models/base/BaseEcommerceIntegration.php @@ -50,7 +50,7 @@ public function rules(): array { return [ [['meta'], 'default', 'value' => null], - [['user_id', 'platform_id'], 'required'], + [['user_id', 'customer_id', 'platform_id'], 'required'], [['user_id', 'customer_id', 'platform_id', 'status'], 'integer'], [['meta'], 'string'], [['created_date', 'updated_date'], 'safe'], diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index efc8b81c..45cf9498 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -3,6 +3,7 @@ namespace common\models\forms\platforms; use Yii; +use yii\base\InvalidConfigException; use yii\base\Model; use yii\db\Expression; use PHPShopify\Exception\SdkException; @@ -10,6 +11,7 @@ use common\services\platforms\ShopifyService; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; +use common\models\Customer; /** * Class ConnectShopifyStoreForm @@ -22,12 +24,13 @@ class ConnectShopifyStoreForm extends Model public ?string $name = null; public ?string $url = null; + public ?int $customer_id = null; public ?string $code = null; public function scenarios(): array { return [ - self::SCENARIO_AUTH_REQUEST => ['name', 'url'], + self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'customer_id'], self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'], ]; } @@ -40,9 +43,10 @@ public function rules(): array /** @see https://www.regextester.com/104785 */ ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'], - [['name', 'url'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], + [['name', 'url', 'customer_id'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST], [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST], + ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], 'on' => self::SCENARIO_AUTH_REQUEST], [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], ]; @@ -53,6 +57,7 @@ public function attributeLabels(): array return [ 'name' => 'Shop Name', 'url' => 'Shop URL', + 'customer_id' => 'Customer', 'code' => 'Code', ]; } @@ -81,10 +86,12 @@ public function validateShopUrl(): void /** * @throws SdkException + * @throws InvalidConfigException */ public function auth(): void { $this->saveShopName(); + $this->saveCustomerId(); // Step 1 - Send request to receive access token $shopifyService = new ShopifyService($this->url); @@ -94,16 +101,25 @@ public function auth(): void /** * @throws ServerErrorHttpException * @throws SdkException + * @throws InvalidConfigException */ public function saveAccessToken(): void { // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken(Yii::$app->session->get('shop_name', 'Shop Name')); + $shopifyService->accessToken( + Yii::$app->session->get('shop_name', 'Shop Name'), + Yii::$app->user->id, + Yii::$app->session->get('customer_id')); } protected function saveShopName() { Yii::$app->session->set('shop_name', $this->name); } + + protected function saveCustomerId() + { + Yii::$app->session->set('customer_id', $this->customer_id); + } } diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php index 8b905414..ed04d0aa 100644 --- a/common/models/query/EcommerceIntegrationQuery.php +++ b/common/models/query/EcommerceIntegrationQuery.php @@ -2,6 +2,7 @@ namespace common\models\query; +use common\models\EcommerceIntegration; use yii\db\ActiveQuery; /** @@ -10,6 +11,11 @@ */ class EcommerceIntegrationQuery extends ActiveQuery { + public function active(): EcommerceIntegrationQuery + { + return $this->andWhere(['status' => EcommerceIntegration::STATUS_INTEGRATION_CONNECTED]); + } + public function for(?int $userId = null, ?int $customerId = null): EcommerceIntegrationQuery { if ($userId) { diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php new file mode 100644 index 00000000..a1b5ad8f --- /dev/null +++ b/common/services/platforms/CreateOrderService.php @@ -0,0 +1,128 @@ +customerId = $customerId; + $this->order = new Order(); + $this->address = new Address(); + } + + public function setOrder(array $attributes): void + { + $this->order->setAttributes($attributes); + // Skip validation by `address_id` for the moment (will be set later): + $this->order->address_id = 0; + } + + public function setCarrier() + { + + } + + public function setAddress(array $attributes): void + { + $this->address->setAttributes($attributes); + } + + public function setItems(array $items): void + { + $this->items = $items; + + foreach ($this->items as $k => $item) { + // Skip validation by `order_id` for the moment (will be set later): + $this->items[$k]['order_id'] = 0; + } + + $excluded = $this->getExcludedItems(); + + foreach ($this->items as $k => $item) { + if (in_array($item['sku'], $excluded)) { + unset($this->items[$k]); + } + } + } + + public function getOrderErrors(): array + { + return $this->order->getErrors(); + } + + public function getAddressErrors(): array + { + return $this->address->getErrors(); + } + + public function getItemsErrors(): array + { + return $this->itemsErrors; + } + + public function isValid(): bool + { + $orderIsValid = $this->order->validate(); + $addressIsValid = $this->address->validate(); + $itemsAreValid = true; + + foreach ($this->items as $key => $item) { + $orderItem = new Item(); + $orderItem->setAttributes($item); + + if (!$orderItem->validate()) { + $this->itemsErrors[$key] = $orderItem->getErrors(); + $itemsAreValid = false; + } + } + + return ($orderIsValid && $addressIsValid && $itemsAreValid); + } + + public function create(): bool + { + if (!$this->isValid()) { + return false; + } + + $this->order->save(); + $this->address->save(); + + $this->order->address_id = $this->address->id; + $this->order->save(); + + foreach ($this->items as $item) { + $orderItem = new Item(); + $orderItem->setAttributes($item); + $orderItem->order_id = $this->order->id; + $orderItem->save(); + } + + return true; + } + + protected function getExcludedItems(): array + { + return ArrayHelper::map( + Sku::find() + ->where(['customer_id' => $this->customerId, 'excluded' => 1]) + ->all(), 'id','sku'); + } +} diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 181e78d8..4513abaa 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -3,12 +3,15 @@ namespace common\services\platforms; use Yii; +use common\models\Order; +use yii\base\InvalidConfigException; +use yii\helpers\Json; +use yii\helpers\Url; +use console\jobs\platforms\ParseShopifyOrderJob; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; use PHPShopify\ShopifySDK; use PHPShopify\AuthHelper; -use yii\helpers\Json; -use yii\helpers\Url; use yii\web\ServerErrorHttpException; use PHPShopify\Exception\SdkException; @@ -26,33 +29,44 @@ */ class ShopifyService { - protected ?string $token = null; + protected const API_VERSION = '2023-01'; protected string $shopUrl; protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; protected string $redirectUrl = '/ecommerce-integration/shopify'; protected ShopifySDK $shopify; + protected ?EcommerceIntegration $ecommerceIntegration = null; - public function __construct(string $shopUrl, string $token = null) + /** + * @throws InvalidConfigException + */ + public function __construct(string $shopUrl, EcommerceIntegration $ecommerceIntegration = null) { $this->shopUrl = $shopUrl; - $this->token = $token; - - if (!$this->token) { // Authorize user's shop - $config = [ - 'ShopUrl' => $this->shopUrl, - 'ApiKey' => Yii::$app->params['shopify']['client_id'], - 'SharedSecret' => Yii::$app->params['shopify']['client_secret'], - ]; - } else { // Use existing user's shop - $config = [ - 'ShopUrl' => $this->shopUrl, - 'AccessToken' => $this->token, - ]; + $this->ecommerceIntegration = $ecommerceIntegration; + $config = [ + 'ApiVersion' => self::API_VERSION, + 'ShopUrl' => $this->shopUrl, + ]; + + if (!$this->ecommerceIntegration) { // Authorize user's shop: + $config['ApiKey'] = Yii::$app->params['shopify']['client_id']; + $config['SharedSecret'] = Yii::$app->params['shopify']['client_secret']; + } else { // Use existing user's shop: + $config['AccessToken'] = $this->ecommerceIntegration->array_meta_data['access_token']; } $this->shopify = new ShopifySDK($config); + + // Check if the provided token is valid: + if ($this->ecommerceIntegration) { + $this->isTokenValid(); + } } + ######### + # Auth: # + ######### + /** * @throws SdkException */ @@ -73,7 +87,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(string $shopName): void + public function accessToken(string $shopName, int $userId, int $customerId): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -86,7 +100,8 @@ public function accessToken(string $shopName): void ]; $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = Yii::$app->user->id; + $ecommerceIntegration->user_id = $userId; + $ecommerceIntegration->customer_id = $customerId; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT); @@ -96,10 +111,80 @@ public function accessToken(string $shopName): void } } - public function makeReq() + /** + * @throws InvalidConfigException + */ + protected function isTokenValid() { - echo '
';
-        print_r($this->shopify->Product->get());
-        exit;
+        try {
+            $this->getProductsList();
+        } catch (\Exception $e) {
+            throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+        }
+    }
+
+    #####################
+    # Get data via API: #
+    #####################
+
+    public function getProductsList(): array
+    {
+        return $this->shopify->Product->get();
+    }
+
+    public function getProductById(int $id): array
+    {
+        return $this->shopify->Product($id)->get();
+    }
+
+    public function getOrdersList(array $params = []): array
+    {
+        return $this->shopify->Order->get($params);
+    }
+
+    public function getOrderById(int $id): array
+    {
+        return $this->shopify->Order($id)->get();
+    }
+
+    public function getCustomerById(int $id): array
+    {
+        return $this->shopify->Customer($id)->get();
+    }
+
+    public function getCustomerAddressById(int $customerId, int $addressId): array
+    {
+        return $this->shopify->Customer($customerId)->Address($addressId)->get();
+    }
+
+    ##################
+    # Order parsing: #
+    ##################
+
+    public function parseRawOrderJob(array $order): void
+    {
+        if ($this->canBeParsed($order) && $this->isNotDuplicate($order)) {
+            Yii::$app->queue->push(
+                new ParseShopifyOrderJob([
+                    'rawOrder' => $order,
+                    'ecommerceIntegrationId' => $this->ecommerceIntegration->id
+                ])
+            );
+        }
+    }
+
+    protected function canBeParsed(array $order): bool
+    {
+        return (isset($order['shipping_address']) && isset($order['customer']));
+    }
+
+    protected function isNotDuplicate(array $order): bool
+    {
+        return !Order::find()->where([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'order_reference' => $order['name'],
+            'customer_reference' => $order['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])->exists();
     }
 }
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php
new file mode 100644
index 00000000..09e97f5d
--- /dev/null
+++ b/console/jobs/platforms/ParseShopifyOrderJob.php
@@ -0,0 +1,180 @@
+setEcommerceIntegration();
+        $this->parseOrderData();
+        $this->parseAddressData();
+        $this->parseItemsData();
+        $this->saveOrder();
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceIntegration(): void
+    {
+        $ecommerceIntegration = EcommerceIntegration::findOne($this->ecommerceIntegrationId);
+
+        if (!$ecommerceIntegration) {
+            throw new NotFoundHttpException('E-commerce integration not found.');
+        }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
+    }
+
+    protected function parseOrderData(): void
+    {
+        $this->parsedOrderAttributes = [
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+            'customer_reference' => (string)$this->rawOrder['id'],
+            'order_reference' => $this->rawOrder['name'],
+            'status_id' => Status::OPEN,
+            'uuid' => (string)$this->rawOrder['id'],
+            'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'notes' => $this->rawOrder['tags'],
+            'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
+        ];
+    }
+
+    protected function parseAddressData(): void
+    {
+        $notProvided = 'Not provided.';
+        $name = null;
+        $address1 = null;
+        $address2 = null;
+        $company = null;
+        $city = null;
+        $phone = null;
+        $stateId = 0;
+        $zip = null;
+        $countryCode = State::DEFAULT_COUNTRY_ABBR;
+
+        if (isset($this->rawOrder['shipping_address']['name'])) {
+            $name = trim($this->rawOrder['shipping_address']['name']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['address1'])) {
+            $address1 = trim($this->rawOrder['shipping_address']['address1']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['address2'])) {
+            $address2 = trim($this->rawOrder['shipping_address']['address2']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['company'])) {
+            $company = trim($this->rawOrder['shipping_address']['company']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['city'])) {
+            $city = trim($this->rawOrder['shipping_address']['city']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['phone'])) {
+            $phone = trim($this->rawOrder['shipping_address']['phone']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['zip'])) {
+            $zip = trim($this->rawOrder['shipping_address']['zip']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['province_code'])) {
+            $state = State::find()->where([
+                'abbreviation' => trim($this->rawOrder['shipping_address']['province_code'])
+            ])->one();
+
+            if ($state) {
+                $stateId = $state->id;
+            }
+        }
+
+        if (isset($this->rawOrder['shipping_address']['country_code'])) {
+            $country = Country::find()->where([
+                'abbreviation' => trim($this->rawOrder['shipping_address']['country_code'])
+            ])->one();
+
+            if ($country) {
+                $countryCode = $country->abbreviation;
+            }
+        }
+
+        $this->parsedAddressAttributes = [
+            'name' => ($name) ?: $notProvided,
+            'address1' => ($address1) ?: $notProvided,
+            'address2' => $address2,
+            'company' => $company,
+            'city' => ($city) ?: $notProvided,
+            'phone' => ($phone) ?: $notProvided,
+            'state_id' => $stateId,
+            'zip' => ($zip) ?: $notProvided,
+            'country' => $countryCode,
+        ];
+    }
+
+    protected function parseItemsData(): void
+    {
+        foreach ($this->rawOrder['line_items'] as $item) {
+            $this->parsedItemsAttributes[] = [
+                'quantity' => $item['fulfillable_quantity'],
+                'sku' => ($item['sku']) ?: 'Not provided.',
+                'name' => $item['name'],
+                'uuid' => (string)$item['id'],
+            ];
+        }
+    }
+
+    protected function saveOrder(): void
+    {
+        $createOrderService = new CreateOrderService($this->ecommerceIntegration->customer_id);
+        $createOrderService->setOrder($this->parsedOrderAttributes);
+        $createOrderService->setCarrier();
+        $createOrderService->setAddress($this->parsedAddressAttributes);
+        $createOrderService->setItems($this->parsedItemsAttributes);
+
+        if ($createOrderService->isValid()) {
+            $createOrderService->create();
+        }
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index a92250a1..b9fbfed3 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -2,6 +2,7 @@
 
 namespace frontend\controllers;
 
+use common\services\platforms\ShopifyService;
 use Yii;
 use yii\web\Response;
 use yii\filters\AccessControl;
@@ -34,6 +35,34 @@ public function behaviors(): array
         ];
     }
 
+    public function actionTest()
+    {
+        $integrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($integrations as $integration) {
+            $accessToken = $integration->array_meta_data['access_token'];
+
+            if ($accessToken) {
+                $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration);
+
+                $params = [
+                    'status' => 'open',
+                    'fufillment_status' => 'unfulfilled',
+                    'limit' => 250,
+                ];
+
+                $orders = $shopifyService->getOrdersList($params);
+
+                foreach ($orders as $order) {
+                    $shopifyService->parseRawOrderJob($order);
+                }
+            }
+        }
+    }
+
     /**
      * Lists all EcommerceIntegration models for the current user.
      * @return string
diff --git a/frontend/views/ecommerce-integration/_platform.php b/frontend/views/ecommerce-integration/_integration.php
similarity index 100%
rename from frontend/views/ecommerce-integration/_platform.php
rename to frontend/views/ecommerce-integration/_integration.php
diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php
index 51519016..e21dc489 100644
--- a/frontend/views/ecommerce-integration/_shopify.php
+++ b/frontend/views/ecommerce-integration/_shopify.php
@@ -69,6 +69,13 @@
                 return '' . $model->array_meta_data['shop_url'] . ' ';
             },
         ],
+        [
+            'label' => 'Customer',
+            'format' => 'raw',
+            'value' => function($model) {
+                return Html::encode($model->customer->name);
+            },
+        ],
         [
             'label' => 'Connected:',
             'attribute' => 'created_date',
diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php
index 0de06906..fee504d6 100644
--- a/frontend/views/ecommerce-integration/index.php
+++ b/frontend/views/ecommerce-integration/index.php
@@ -3,6 +3,7 @@
 use yii\helpers\Html;
 use yii\helpers\Url;
 use common\models\EcommerceIntegration;
+use common\models\EcommercePlatform;
 
 /* @var $this View */
 /* @var $models EcommerceIntegration[] */
@@ -18,13 +19,15 @@
     
 
     
- Connect Shopify shop + isActive()) { ?> + Connect Shopify shop +
- render('_platform', [ + render('_integration', [ 'model' => $model, ]) ?> diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index 998a0943..ef35a748 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -1,8 +1,10 @@ title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; + +$customersList = ArrayHelper::map( + Customer::find() + ->orderBy(['name' => SORT_ASC]) + ->all(), 'id','name'); ?>
@@ -46,6 +53,17 @@ 'maxlength' => true ]) ?>
+
+ field($model, 'customer_id') + ->dropdownList($customersList, [ + 'class' => 'form-control', + 'prompt' => $model->getAttributeLabel('customer_id') . '...', + 'placeholder' => $model->getAttributeLabel('customer_id') . '...', + 'required' => true, + 'maxlength' => true + ]) ?> +
'btn btn-success', diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 0fb6888f..1167f39a 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -16,7 +16,12 @@ 1. Visit our website - `/ecommerce-platform` (you must be an `Admin`). -# Shopify +# Constraints + +1. A new integration can be added by a user only if the needed e-commerce platform +has the status `Active`. + +# E-commerce Integrations - Shopify ### App: From 14fa575cf2e32e57563a46a81a60cfc4aa5bbfad Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 3 Mar 2023 16:47:52 +0200 Subject: [PATCH 11/81] Added: Order Statuses, Financial Statuses, Fulfillment Statuses --- common/models/EcommerceIntegration.php | 5 ++ .../platforms/ConnectShopifyStoreForm.php | 57 ++++++++++++------- common/services/platforms/ShopifyService.php | 44 ++++++++++++-- .../EcommerceIntegrationController.php | 21 ++++++- .../views/ecommerce-integration/_shopify.php | 42 +++++++++++++- .../views/ecommerce-integration/shopify.php | 48 ++++++++++++++++ frontend/web/css/site.css | 12 ++++ 7 files changed, 203 insertions(+), 26 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index a066ffc4..60b30598 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -57,6 +57,11 @@ public function resume(): bool return $this->save(); } + public function isMetaKeyExistsAndNotEmpty(string $key): bool + { + return (isset($this->array_meta_data[$key]) && !empty($this->array_meta_data[$key])); + } + protected function convertMetaData(): void { if ($this->meta) { diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index 45cf9498..d370754f 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -24,13 +24,16 @@ class ConnectShopifyStoreForm extends Model public ?string $name = null; public ?string $url = null; + public string|array|null $order_statuses = null; + public string|array|null $financial_statuses = null; + public string|array|null $fulfillment_statuses = null; public ?int $customer_id = null; public ?string $code = null; public function scenarios(): array { return [ - self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'customer_id'], + self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'order_statuses', 'financial_statuses', 'fulfillment_statuses', 'customer_id'], self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'], ]; } @@ -43,12 +46,23 @@ public function rules(): array /** @see https://www.regextester.com/104785 */ ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'], - [['name', 'url', 'customer_id'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], - [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST], - [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST], - ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], 'on' => self::SCENARIO_AUTH_REQUEST], - - [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], + [['name', 'url', 'customer_id'], 'required', + 'on' => self::SCENARIO_AUTH_REQUEST], + [['name'], 'validateShopName', + 'on' => self::SCENARIO_AUTH_REQUEST], + [['url'], 'validateShopUrl', + 'on' => self::SCENARIO_AUTH_REQUEST], + ['order_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$orderStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['financial_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$financialStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['fulfillment_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$fulfillmentStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], + 'on' => self::SCENARIO_AUTH_REQUEST], + + [['url', 'code'], 'required', + 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], ]; } @@ -57,6 +71,9 @@ public function attributeLabels(): array return [ 'name' => 'Shop Name', 'url' => 'Shop URL', + 'order_statuses' => 'Order Statuses', + 'financial_statuses' => 'Financial Statuses', + 'fulfillment_statuses' => 'Fulfillment Statuses', 'customer_id' => 'Customer', 'code' => 'Code', ]; @@ -90,8 +107,7 @@ public function validateShopUrl(): void */ public function auth(): void { - $this->saveShopName(); - $this->saveCustomerId(); + $this->saveDataForSecondStep(); // Step 1 - Send request to receive access token $shopifyService = new ShopifyService($this->url); @@ -105,21 +121,24 @@ public function auth(): void */ public function saveAccessToken(): void { + $data = unserialize(Yii::$app->session->get('shopify_connection_second_step')); + // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken( - Yii::$app->session->get('shop_name', 'Shop Name'), - Yii::$app->user->id, - Yii::$app->session->get('customer_id')); + $shopifyService->accessToken($data); } - protected function saveShopName() + protected function saveDataForSecondStep() { - Yii::$app->session->set('shop_name', $this->name); - } + $data = [ + 'shop_name' => $this->name, + 'customer_id' => $this->customer_id, + 'user_id' => Yii::$app->user->id, + 'order_statuses' => $this->order_statuses, + 'financial_statuses' => $this->financial_statuses, + 'fulfillment_statuses' => $this->fulfillment_statuses, + ]; - protected function saveCustomerId() - { - Yii::$app->session->set('customer_id', $this->customer_id); + Yii::$app->session->set('shopify_connection_second_step', serialize($data)); } } diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 4513abaa..4ec55d1a 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -29,6 +29,39 @@ */ class ShopifyService { + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any-examples + */ + public static array $orderStatuses = [ + 'open' => 'Open', + 'closed' => 'Closed', + 'cancelled' => 'Cancelled', + ]; + + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any + */ + public static array $financialStatuses = [ + 'authorized' => 'Authorized', + 'pending' => 'Pending', + 'paid' => 'Paid', + 'partially_paid' => 'Partially paid', + 'refunded' => 'Refunded', + 'voided' => 'Voided', + 'partially_refunded' => 'Partially refunded', + 'unpaid' => 'Unpaid', + ]; + + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any + */ + public static array $fulfillmentStatuses = [ + 'shipped' => 'Shipped', // Show orders that have been shipped. Returns orders with `fulfillment_status` of `fulfilled` + 'partial' => 'Partial', // Show partially shipped orders + 'unshipped' => 'Unshipped', // Show orders that have not yet been shipped. Returns orders with `fulfillment_status` of `null` + 'unfulfilled' => 'Unfulfilled', // Returns orders with `fulfillment_status` of `null` or `partial` + ]; + protected const API_VERSION = '2023-01'; protected string $shopUrl; protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; @@ -87,7 +120,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(string $shopName, int $userId, int $customerId): void + public function accessToken(array $data): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -95,13 +128,16 @@ public function accessToken(string $shopName, int $userId, int $customerId): voi $meta = [ 'platform' => EcommercePlatform::SHOPIFY_PLATFORM_NAME, 'shop_url' => $this->shopUrl, - 'shop_name' => $shopName, + 'shop_name' => $data['shop_name'], + 'order_statuses' => $data['order_statuses'], + 'financial_statuses' => $data['financial_statuses'], + 'fulfillment_statuses' => $data['fulfillment_statuses'], 'access_token' => $accessToken, ]; $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = $userId; - $ecommerceIntegration->customer_id = $customerId; + $ecommerceIntegration->user_id = $data['user_id']; + $ecommerceIntegration->customer_id = $data['customer_id']; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT); diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index b9fbfed3..64bd0c2a 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -48,14 +48,31 @@ public function actionTest() if ($accessToken) { $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration); + /** + * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any + */ $params = [ - 'status' => 'open', - 'fufillment_status' => 'unfulfilled', 'limit' => 250, ]; + if ($integration->isMetaKeyExistsAndNotEmpty('order_statuses')) { + $params['status'] = implode(',', $integration->array_meta_data['order_statuses']); + } + + if ($integration->isMetaKeyExistsAndNotEmpty('financial_statuses')) { + $params['financial_status'] = implode(',', $integration->array_meta_data['financial_statuses']); + } + + if ($integration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) { + $params['fulfillment_status'] = implode(',', $integration->array_meta_data['fulfillment_statuses']); + } + $orders = $shopifyService->getOrdersList($params); + echo '
';
+                print_r($orders);
+                exit;
+
                 foreach ($orders as $order) {
                     $shopifyService->parseRawOrderJob($order);
                 }
diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php
index e21dc489..85fe3926 100644
--- a/frontend/views/ecommerce-integration/_shopify.php
+++ b/frontend/views/ecommerce-integration/_shopify.php
@@ -4,6 +4,7 @@
 use yii\helpers\Url;
 use yii\widgets\DetailView;
 use common\models\EcommerceIntegration;
+use common\services\platforms\ShopifyService;
 
 /* @var $this View */
 /* @var $model EcommerceIntegration */
@@ -70,7 +71,46 @@
             },
         ],
         [
-            'label' => 'Customer',
+            'label' => 'Order Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('order_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$orderStatuses[$el];
+                    }, $model->array_meta_data['order_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Financial Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$financialStatuses[$el];
+                    }, $model->array_meta_data['financial_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Fulfillment Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$fulfillmentStatuses[$el];
+                    }, $model->array_meta_data['fulfillment_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Customer:',
             'format' => 'raw',
             'value' => function($model) {
                 return Html::encode($model->customer->name);
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php
index ef35a748..c89ec3f5 100644
--- a/frontend/views/ecommerce-integration/shopify.php
+++ b/frontend/views/ecommerce-integration/shopify.php
@@ -4,6 +4,7 @@
 use yii\helpers\ArrayHelper;
 use yii\widgets\ActiveForm;
 use common\models\forms\platforms\ConnectShopifyStoreForm;
+use common\services\platforms\ShopifyService;
 use common\models\Customer;
 
 /* @var $this View */
@@ -18,6 +19,10 @@
     Customer::find()
         ->orderBy(['name' => SORT_ASC])
         ->all(), 'id','name');
+
+$orderStatuses = ' Order Statuses';
+$financialStatusesLabel = ' Financial Statuses';
+$fulfillmentStatusesLabel = ' Fulfillment Statuses';
 ?>
 
 
@@ -53,6 +58,49 @@ 'maxlength' => true ]) ?>
+ +
+ field($model, 'order_statuses') + ->checkboxList(ShopifyService::$orderStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('order_statuses') . '...', + ]) + ->label($orderStatuses) ?> +
+ +
+ field($model, 'financial_statuses') + ->checkboxList(ShopifyService::$financialStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('financial_statuses') . '...', + ]) + ->label($financialStatusesLabel) ?> +
+ +
+ field($model, 'fulfillment_statuses') + ->checkboxList(ShopifyService::$fulfillmentStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('fulfillment_statuses') . '...', + ]) + ->label($fulfillmentStatusesLabel) ?> +
+
field($model, 'customer_id') diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 499177ee..fee2a6c9 100755 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -384,6 +384,10 @@ pre { margin: 0 0 15px 0 !important; } +.font-weight-normal { + font-weight: normal; +} + .mb-2 { margin-bottom: 20px; } @@ -391,3 +395,11 @@ pre { .mt-2 { margin-top: 20px; } + +.ml-1 { + margin-left: 10px; +} + +.mr-1 { + margin-right: 10px; +} From 702e08f7fb9fc2d091849daa9b1da4873b454eb4 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 3 Mar 2023 18:09:26 +0200 Subject: [PATCH 12/81] Customers drop-down only for the current user --- .../EcommerceIntegrationController.php | 21 +++++++++++++++++++ .../views/ecommerce-integration/shopify.php | 6 +----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index 64bd0c2a..5c02dfa4 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -3,7 +3,9 @@ namespace frontend\controllers; use common\services\platforms\ShopifyService; +use frontend\models\Customer; use Yii; +use yii\helpers\ArrayHelper; use yii\web\Response; use yii\filters\AccessControl; use Da\User\Filter\AccessRuleFilter; @@ -137,6 +139,7 @@ public function actionShopify(): string|Response return $this->render('shopify', [ 'model' => $model, + 'customersList' => $this->getCustomersList(), ]); } @@ -203,6 +206,24 @@ protected function getEcommerceIntegrationById(int $id): EcommerceIntegration return $model; } + protected function getCustomersList(): array + { + if (Yii::$app->user->identity->isAdmin) { + $data = Customer::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + } else { + $data = Customer::find() + ->where("`id` IN(SELECT DISTINCT(`customer_id`) FROM `user_customer` WHERE `user_id` = :user_id)", [ + 'user_id' => Yii::$app->user->id + ]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + } + + return ArrayHelper::map($data, 'id','name'); + } + /** * Checks a needed EcommercePlatform model by the provided name. It must exist and be active. * @throws NotFoundHttpException diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index c89ec3f5..d377682f 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -9,17 +9,13 @@ /* @var $this View */ /* @var $model ConnectShopifyStoreForm */ +/* @var $customersList array */ $title = 'Shopify Integration'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; -$customersList = ArrayHelper::map( - Customer::find() - ->orderBy(['name' => SORT_ASC]) - ->all(), 'id','name'); - $orderStatuses = ' Order Statuses'; $financialStatusesLabel = ' Financial Statuses'; $fulfillmentStatusesLabel = ' Fulfillment Statuses'; From db9692096aee59da01f4266f4748b1b4df88b4e9 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 15:26:33 +0200 Subject: [PATCH 13/81] Reconnect uninstalled shop --- common/models/EcommerceIntegration.php | 13 +++++ .../platforms/ConnectShopifyStoreForm.php | 34 +++++++++--- common/services/platforms/ShopifyService.php | 12 +++-- .../EcommerceIntegrationController.php | 52 ++++++++++++++----- .../views/ecommerce-integration/_shopify.php | 26 +++++++--- intro-docs/ecommerce-platfroms.md | 6 +++ 6 files changed, 113 insertions(+), 30 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index 60b30598..2112a852 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -13,6 +13,7 @@ class EcommerceIntegration extends BaseEcommerceIntegration { public const STATUS_INTEGRATION_CONNECTED = 1; public const STATUS_INTEGRATION_PAUSED = 0; + public const STATUS_INTEGRATION_UNINSTALLED = -1; public array $array_meta_data = []; @@ -27,6 +28,7 @@ public static function getStatuses(): array return [ self::STATUS_INTEGRATION_CONNECTED => 'Connected', self::STATUS_INTEGRATION_PAUSED => 'Paused', + self::STATUS_INTEGRATION_UNINSTALLED => 'Uninstalled', ]; } @@ -40,11 +42,22 @@ public function isPaused(): bool return $this->status === self::STATUS_INTEGRATION_PAUSED; } + public function isUninstalled(): bool + { + return $this->status === self::STATUS_INTEGRATION_UNINSTALLED; + } + public function disconnect(): bool|int { return $this->delete(); } + public function uninstall(): void + { + $this->status = self::STATUS_INTEGRATION_UNINSTALLED; + $this->save(); + } + public function pause(): bool { $this->status = self::STATUS_INTEGRATION_PAUSED; diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index d370754f..916f3788 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -22,6 +22,8 @@ class ConnectShopifyStoreForm extends Model public const SCENARIO_AUTH_REQUEST = 'scenarioAuthRequest'; public const SCENARIO_SAVE_ACCESS_TOKEN = 'scenarioSaveAccessToken'; + public ?EcommerceIntegration $ecommerceIntegration = null; + public ?string $name = null; public ?string $url = null; public string|array|null $order_statuses = null; @@ -81,10 +83,15 @@ public function attributeLabels(): array public function validateShopName(): void { - if (EcommerceIntegration::find() + $query = EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) - ->exists()) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]); + + if ($this->ecommerceIntegration) { + $query->andWhere('id != :id', ['id' => $this->ecommerceIntegration->id]); + } + + if ($query->exists()) { $this->addError('name', 'Shop name already exists.'); } @@ -92,10 +99,15 @@ public function validateShopName(): void public function validateShopUrl(): void { - if (EcommerceIntegration::find() + $query = EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) - ->exists()) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]); + + if ($this->ecommerceIntegration) { + $query->andWhere('id != :id', ['id' => $this->ecommerceIntegration->id]); + } + + if ($query->exists()) { $this->addError('url', 'Shop URL already exists.'); } @@ -123,9 +135,13 @@ public function saveAccessToken(): void { $data = unserialize(Yii::$app->session->get('shopify_connection_second_step')); + if (isset($data['integration_id'])) { + $this->ecommerceIntegration = EcommerceIntegration::findOne($data['integration_id']); + } + // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken($data); + $shopifyService->accessToken($data, $this->ecommerceIntegration); } protected function saveDataForSecondStep() @@ -139,6 +155,10 @@ protected function saveDataForSecondStep() 'fulfillment_statuses' => $this->fulfillment_statuses, ]; + if ($this->ecommerceIntegration) { + $data['integration_id'] = $this->ecommerceIntegration->id; + } + Yii::$app->session->set('shopify_connection_second_step', serialize($data)); } } diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 4ec55d1a..9cbb55a2 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -64,7 +64,7 @@ class ShopifyService protected const API_VERSION = '2023-01'; protected string $shopUrl; - protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; + protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,'; protected string $redirectUrl = '/ecommerce-integration/shopify'; protected ShopifySDK $shopify; protected ?EcommerceIntegration $ecommerceIntegration = null; @@ -120,7 +120,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(array $data): void + public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegration = null): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -135,7 +135,9 @@ public function accessToken(array $data): void 'access_token' => $accessToken, ]; - $ecommerceIntegration = new EcommerceIntegration(); + if (!$ecommerceIntegration) { + $ecommerceIntegration = new EcommerceIntegration(); + } $ecommerceIntegration->user_id = $data['user_id']; $ecommerceIntegration->customer_id = $data['customer_id']; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; @@ -155,6 +157,10 @@ protected function isTokenValid() try { $this->getProductsList(); } catch (\Exception $e) { + if (!$this->ecommerceIntegration->isUninstalled()) { + $this->ecommerceIntegration->uninstall(); + } + throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.'); } } diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index 5c02dfa4..fca638a3 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -5,6 +5,7 @@ use common\services\platforms\ShopifyService; use frontend\models\Customer; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\ArrayHelper; use yii\web\Response; use yii\filters\AccessControl; @@ -101,19 +102,37 @@ public function actionIndex(): string /** * Connects a new Shopify shop. - * @throws SdkException + * @param int|null $id Reconnect a shop by ID. + * @return string|Response + * @throws InvalidConfigException * @throws NotFoundHttpException + * @throws SdkException * @throws ServerErrorHttpException */ - public function actionShopify(): string|Response + public function actionShopify(?int $id = null): string|Response { $this->checkEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME); + $model = new ConnectShopifyStoreForm(); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST; + + if ($id) { // Edit existing model (Reconnect action): + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $model->ecommerceIntegration = $ecommerceIntegration; + + $model->setAttributes([ + 'name' => $ecommerceIntegration->array_meta_data['shop_name'], + 'url' => $ecommerceIntegration->array_meta_data['shop_url'], + 'customer_id' => $ecommerceIntegration->customer_id, + 'order_statuses' => $ecommerceIntegration->array_meta_data['order_statuses'], + 'financial_statuses' => $ecommerceIntegration->array_meta_data['financial_statuses'], + 'fulfillment_statuses' => $ecommerceIntegration->array_meta_data['fulfillment_statuses'], + ]); + } + if (Yii::$app->request->isPost) { // Step 1 - Send request to receive access token: - $model = new ConnectShopifyStoreForm([ - 'scenario' => ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST - ]); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST; $model->load(Yii::$app->request->post()); if ($model->validate()) { @@ -121,11 +140,9 @@ public function actionShopify(): string|Response } } elseif (Yii::$app->request->get('code')) { // Step 2 - Receive and save access token: - $model = new ConnectShopifyStoreForm([ - 'scenario' => ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN, - 'url' => Yii::$app->request->get('shop'), - 'code' => Yii::$app->request->get('code') - ]); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN; + $model->url = Yii::$app->request->get('shop'); + $model->code = Yii::$app->request->get('code'); if ($model->validate()) { $model->saveAccessToken(); @@ -133,8 +150,6 @@ public function actionShopify(): string|Response Yii::$app->session->setFlash('success', 'Shopify shop has been connected.'); return $this->redirect(['index']); } - } else { - $model = new ConnectShopifyStoreForm(); } return $this->render('shopify', [ @@ -143,6 +158,19 @@ public function actionShopify(): string|Response ]); } + /** + * @throws NotFoundHttpException + */ + public function actionReconnect(int $id): Response + { + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + + return match ($ecommerceIntegration->ecommercePlatform->name) { + EcommercePlatform::SHOPIFY_PLATFORM_NAME => $this->redirect(['shopify', 'id' => $ecommerceIntegration->id]), + default => throw new NotFoundHttpException('Ecommerce platform not found.'), + }; + } + /** * Disconnects a needed EcommerceIntegration model. * @throws NotFoundHttpException diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php index 85fe3926..e92e0dd7 100644 --- a/frontend/views/ecommerce-integration/_shopify.php +++ b/frontend/views/ecommerce-integration/_shopify.php @@ -9,12 +9,12 @@ /* @var $this View */ /* @var $model EcommerceIntegration */ -$disconnectConfirm = 'Are you sure you want to disconnect this platform?'; -$disconnectConfirm .= ' In this case, all orders related to the platform will not be processed.'; -$disconnectConfirm .= ' Also, you will lose all your current credentials and will need to reconnect this platform again.'; +$disconnectConfirm = 'Are you sure you want to remove this shop?'; +$disconnectConfirm .= ' In this case, all orders related to the shop will not be processed.'; +$disconnectConfirm .= ' Also, you will lose all your current credentials and will need to reconnect the shop again.'; -$pauseConfirm = 'Are you sure you want to pause this platform?'; -$pauseConfirm .= ' In this case, all orders with the platform will not be processed.'; +$pauseConfirm = 'Are you sure you want to pause this shop?'; +$pauseConfirm .= ' In this case, all orders related to the shop will not be processed.'; ?> '; $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; } elseif ($model->isPaused()) { - $icon = ''; + $icon = ''; $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; + } elseif ($model->isUninstalled()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_UNINSTALLED] . ' ' . $icon . ''; } else { $string = null; } @@ -134,14 +137,21 @@ $buttons = 'Pause'; $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); - $buttons .= ' Disconnect'; + $buttons .= ' Remove'; break; case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: $url = Url::to(['/ecommerce-integration/resume', 'id' => $model->id]); $buttons = 'Resume'; $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); - $buttons .= ' Disconnect'; + $buttons .= ' Remove'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_UNINSTALLED: + $url = Url::to(['/ecommerce-integration/reconnect', 'id' => $model->id]); + $buttons = 'Reconnect'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Remove'; break; default: $buttons = ''; diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 1167f39a..b0dc92a8 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -39,6 +39,12 @@ If you're going to **test it locally**, specify: 4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain` to `https://shipwise.ngrok.io`. +5. You need to request `Protected customer data access`. Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` -> +Press the button `Request access`. On the page, select and request access for: + +- `Protected customer data` +- `Protected customer fields (optional)` -- all the fields + ### Test shop(s): 1. Go to `https://partners.shopify.com/` -> `Stores`. From d16f7b53434b035544ea7111a81fa2bf10bd4c20 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 16:20:29 +0200 Subject: [PATCH 14/81] Send notification once a shop is uninstalled --- common/models/EcommerceIntegration.php | 23 +++++++++++++++++++- common/services/platforms/ShopifyService.php | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index 2112a852..fe590942 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -2,8 +2,11 @@ namespace common\models; +use Yii; +use yii\helpers\Html; use yii\helpers\Json; use common\models\base\BaseEcommerceIntegration; +use console\jobs\NotificationJob; /** * Class EcommerceIntegration @@ -52,10 +55,28 @@ public function disconnect(): bool|int return $this->delete(); } - public function uninstall(): void + public function uninstall(bool $withNotification = false): void { $this->status = self::STATUS_INTEGRATION_UNINSTALLED; $this->save(); + + if ($withNotification) { + $shopUrl = Html::encode($this->array_meta_data['shop_url']); + $subject = '⚠️ Problem pulling data from ' . $shopUrl; + $message = 'We were not able to pull data from the Shopify shop ' . $shopUrl .'.'; + $message .= ' The status of the shop is changed to `Uninstalled`.'; + $message .= ' Please click the link below and try to reconnect the shop.'; + + Yii::$app->queue->push( + new NotificationJob([ + 'customer_id' => $this->customer_id, + 'subject' => $subject, + 'message' => $message, + 'url' => ['/ecommerce-integration/index'], + 'urlText' => 'Reconnect the shop', + ]) + ); + } } public function pause(): bool diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 9cbb55a2..e08b3174 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -158,7 +158,7 @@ protected function isTokenValid() $this->getProductsList(); } catch (\Exception $e) { if (!$this->ecommerceIntegration->isUninstalled()) { - $this->ecommerceIntegration->uninstall(); + $this->ecommerceIntegration->uninstall(true); } throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.'); From acfd526995c7427057241b2952368a0d4e16c77f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 20:58:50 +0200 Subject: [PATCH 15/81] E-commerce orders logs --- common/models/EcommerceOrderLog.php | 48 +++++++++ common/models/base/BaseEcommerceOrderLog.php | 101 ++++++++++++++++++ .../services/platforms/CreateOrderService.php | 4 +- common/services/platforms/ShopifyService.php | 11 +- .../jobs/platforms/ParseShopifyOrderJob.php | 39 +++++-- ...44546_create_ecommerce_order_log_table.php | 66 ++++++++++++ .../EcommerceIntegrationController.php | 6 +- 7 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 common/models/EcommerceOrderLog.php create mode 100644 common/models/base/BaseEcommerceOrderLog.php create mode 100644 console/migrations/m230306_144546_create_ecommerce_order_log_table.php diff --git a/common/models/EcommerceOrderLog.php b/common/models/EcommerceOrderLog.php new file mode 100644 index 00000000..5ef65f44 --- /dev/null +++ b/common/models/EcommerceOrderLog.php @@ -0,0 +1,48 @@ + 'Success', + self::STATUS_FAILED => 'Failed', + ]; + } + + public static function success(EcommerceIntegration $ecommerceIntegration, array $payload, Order $order): void + { + $ecommerceOrderLog = new EcommerceOrderLog(); + $ecommerceOrderLog->platform_id = $ecommerceIntegration->ecommercePlatform->id; + $ecommerceOrderLog->integration_id = $ecommerceIntegration->id; + $ecommerceOrderLog->original_order_id = (string)$payload['id']; + $ecommerceOrderLog->internal_order_id = $order->id; + $ecommerceOrderLog->status = self::STATUS_SUCCESS; + $ecommerceOrderLog->payload = Json::encode($payload, JSON_PRETTY_PRINT); + $ecommerceOrderLog->save(); + } + + public static function failed(EcommerceIntegration $ecommerceIntegration, array $payload, ?array $meta = null): void + { + $ecommerceOrderLog = new EcommerceOrderLog(); + $ecommerceOrderLog->platform_id = $ecommerceIntegration->ecommercePlatform->id; + $ecommerceOrderLog->integration_id = $ecommerceIntegration->id; + $ecommerceOrderLog->original_order_id = (string)$payload['id']; + $ecommerceOrderLog->status = self::STATUS_FAILED; + $ecommerceOrderLog->payload = Json::encode($payload, JSON_PRETTY_PRINT); + $ecommerceOrderLog->meta = ($meta) ? Json::encode($meta, JSON_PRETTY_PRINT) : null; + $ecommerceOrderLog->save(); + } +} diff --git a/common/models/base/BaseEcommerceOrderLog.php b/common/models/base/BaseEcommerceOrderLog.php new file mode 100644 index 00000000..d6a49bdb --- /dev/null +++ b/common/models/base/BaseEcommerceOrderLog.php @@ -0,0 +1,101 @@ + null], + [['platform_id', 'integration_id', 'original_order_id', 'status', 'payload'], 'required'], + [['platform_id', 'integration_id', 'internal_order_id'], 'integer'], + [['payload', 'meta'], 'string'], + [['created_date', 'updated_date'], 'safe'], + [['status'], 'string', 'max' => 64], + [['original_order_id'], 'string', 'max' => 256], + [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::className(), 'targetAttribute' => ['integration_id' => 'id']], + [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::className(), 'targetAttribute' => ['internal_order_id' => 'id']], + [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels(): array + { + return [ + 'id' => 'ID', + 'platform_id' => 'Platform ID', + 'integration_id' => 'Integration ID', + 'original_order_id' => 'Original Order ID', + 'internal_order_id' => 'Internal Order ID', + 'status' => 'Status', + 'payload' => 'Payload', + 'meta' => 'Meta Data', + 'created_date' => 'Created Date', + 'updated_date' => 'Updated Date', + ]; + } + + /** + * @return ActiveQuery + */ + public function getIntegration(): ActiveQuery + { + return $this->hasOne(EcommerceIntegration::className(), ['id' => 'integration_id']); + } + + /** + * @return ActiveQuery + */ + public function getInternalOrder(): ActiveQuery + { + return $this->hasOne(Order::className(), ['id' => 'internal_order_id']); + } + + /** + * @return ActiveQuery + */ + public function getPlatform(): ActiveQuery + { + return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']); + } +} diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php index a1b5ad8f..dd59dba4 100644 --- a/common/services/platforms/CreateOrderService.php +++ b/common/services/platforms/CreateOrderService.php @@ -96,7 +96,7 @@ public function isValid(): bool return ($orderIsValid && $addressIsValid && $itemsAreValid); } - public function create(): bool + public function create(): bool|Order { if (!$this->isValid()) { return false; @@ -115,7 +115,7 @@ public function create(): bool $orderItem->save(); } - return true; + return $this->order; } protected function getExcludedItems(): array diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index e08b3174..a01c6dd8 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -2,6 +2,7 @@ namespace common\services\platforms; +use common\models\EcommerceOrderLog; use Yii; use common\models\Order; use yii\base\InvalidConfigException; @@ -205,7 +206,7 @@ public function getCustomerAddressById(int $customerId, int $addressId): array public function parseRawOrderJob(array $order): void { - if ($this->canBeParsed($order) && $this->isNotDuplicate($order)) { + if ($this->isNotDuplicate($order)) { Yii::$app->queue->push( new ParseShopifyOrderJob([ 'rawOrder' => $order, @@ -215,17 +216,11 @@ public function parseRawOrderJob(array $order): void } } - protected function canBeParsed(array $order): bool - { - return (isset($order['shipping_address']) && isset($order['customer'])); - } - protected function isNotDuplicate(array $order): bool { return !Order::find()->where([ 'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME, - 'order_reference' => $order['name'], - 'customer_reference' => $order['id'], + 'uuid' => (string)$order['id'], 'customer_id' => $this->ecommerceIntegration->customer_id, ])->exists(); } diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php index 09e97f5d..ce4b65bd 100644 --- a/console/jobs/platforms/ParseShopifyOrderJob.php +++ b/console/jobs/platforms/ParseShopifyOrderJob.php @@ -4,7 +4,9 @@ use common\models\Country; use common\models\EcommerceIntegration; +use common\models\EcommerceOrderLog; use common\models\EcommercePlatform; +use common\models\Order; use common\models\State; use common\models\Status; use common\services\platforms\CreateOrderService; @@ -34,10 +36,18 @@ class ParseShopifyOrderJob extends BaseObject implements RetryableJobInterface public function execute($queue): void { $this->setEcommerceIntegration(); - $this->parseOrderData(); - $this->parseAddressData(); - $this->parseItemsData(); - $this->saveOrder(); + $parsingErrors = $this->getParsingErrors(); + + if (!$parsingErrors) { + $this->parseOrderData(); + $this->parseAddressData(); + $this->parseItemsData(); + $order = $this->saveOrder(); + + EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order); + } else { + EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]); + } } /** @@ -54,6 +64,21 @@ protected function setEcommerceIntegration(): void $this->ecommerceIntegration = $ecommerceIntegration; } + protected function getParsingErrors(): array + { + $errors = []; + + if (!isset($this->rawOrder['shipping_address'])) { + $errors[] = 'Shipping address is missed.'; + } + + if (!isset($this->rawOrder['customer'])) { + $errors[] = 'Customer is missed.'; + } + + return $errors; + } + protected function parseOrderData(): void { $this->parsedOrderAttributes = [ @@ -71,7 +96,7 @@ protected function parseOrderData(): void protected function parseAddressData(): void { - $notProvided = 'Not provided.'; + $notProvided = 'Not provided'; $name = null; $address1 = null; $address2 = null; @@ -155,7 +180,7 @@ protected function parseItemsData(): void } } - protected function saveOrder(): void + protected function saveOrder(): Order|bool { $createOrderService = new CreateOrderService($this->ecommerceIntegration->customer_id); $createOrderService->setOrder($this->parsedOrderAttributes); @@ -164,7 +189,7 @@ protected function saveOrder(): void $createOrderService->setItems($this->parsedItemsAttributes); if ($createOrderService->isValid()) { - $createOrderService->create(); + return $createOrderService->create(); } } diff --git a/console/migrations/m230306_144546_create_ecommerce_order_log_table.php b/console/migrations/m230306_144546_create_ecommerce_order_log_table.php new file mode 100644 index 00000000..5d3f240a --- /dev/null +++ b/console/migrations/m230306_144546_create_ecommerce_order_log_table.php @@ -0,0 +1,66 @@ +execute(" + CREATE TABLE `ecommerce_order_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `platform_id` int NOT NULL, + `integration_id` int NOT NULL, + `original_order_id` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `internal_order_id` int DEFAULT NULL, + `status` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `meta` MEDIUMTEXT COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL , + `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_date` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + + $this->addForeignKey( + '{{%fk-ecommerce_order-platform_id}}', + '{{%ecommerce_order_log}}', + 'platform_id', + '{{%ecommerce_platform}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_order-integration_id}}', + '{{%ecommerce_order_log}}', + 'integration_id', + '{{%ecommerce_integration}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_order-internal_order_id}}', + '{{%ecommerce_order_log}}', + 'internal_order_id', + '{{%orders}}', + 'id', + 'CASCADE' + ); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable('{{%ecommerce_order_log}}'); + } +} diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index fca638a3..2e9e9f44 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -72,9 +72,9 @@ public function actionTest() $orders = $shopifyService->getOrdersList($params); - echo '
';
-                print_r($orders);
-                exit;
+//                echo '
';
+//                print_r($orders);
+//                exit;
 
                 foreach ($orders as $order) {
                     $shopifyService->parseRawOrderJob($order);

From 62575e6c3ea1e627370940a4c033342886a295ef Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 18:05:24 +0200
Subject: [PATCH 16/81] Added Shopify orders parsing to Cron

---
 common/models/OrderHistory.php                |  9 +++-
 .../services/platforms/CreateOrderService.php | 19 ++++++++
 common/services/platforms/ShopifyService.php  | 44 +++++++++++++-----
 console/controllers/CronController.php        | 46 ++++++++++++++++++-
 .../jobs/platforms/ParseShopifyOrderJob.php   | 30 +++++++-----
 5 files changed, 121 insertions(+), 27 deletions(-)

diff --git a/common/models/OrderHistory.php b/common/models/OrderHistory.php
index 58aac294..f9c3edd8 100755
--- a/common/models/OrderHistory.php
+++ b/common/models/OrderHistory.php
@@ -77,8 +77,13 @@ protected function orderHistoryPopulate(): void
         }
 
         if (!$this->user_id) {
-            if (!Yii::$app->user->isGuest) {
-                $this->user_id = Yii::$app->user->id;
+            /**
+             * TODO: Replace with something like `system`
+             */
+            if (Yii::$app->request->isConsoleRequest) {
+                $this->user_id = 1;
+            } else {
+                $this->user_id = (!Yii::$app->user->isGuest) ? Yii::$app->user->id : 1;
             }
         }
 
diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php
index dd59dba4..5e8814e7 100644
--- a/common/services/platforms/CreateOrderService.php
+++ b/common/services/platforms/CreateOrderService.php
@@ -125,4 +125,23 @@ protected function getExcludedItems(): array
                 ->where(['customer_id' => $this->customerId, 'excluded' => 1])
                 ->all(), 'id','sku');
     }
+
+    public static function isOrderExists(array $params): bool
+    {
+        $exists = Order::find();
+
+        if (isset($params['origin'])) {
+            $exists->andWhere(['origin' => $params['origin']]);
+        }
+
+        if (isset($params['uuid'])) {
+            $exists->andWhere(['uuid' => (string)$params['uuid']]);
+        }
+
+        if (isset($params['customer_id'])) {
+            $exists->andWhere(['customer_id' => $params['customer_id']]);
+        }
+
+        return $exists->exists();
+    }
 }
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index a01c6dd8..854817c2 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -180,9 +180,9 @@ public function getProductById(int $id): array
         return $this->shopify->Product($id)->get();
     }
 
-    public function getOrdersList(array $params = []): array
+    public function getOrdersList(): array
     {
-        return $this->shopify->Order->get($params);
+        return $this->shopify->Order->get($this->getRequestParamsForOrders());
     }
 
     public function getOrderById(int $id): array
@@ -200,13 +200,42 @@ public function getCustomerAddressById(int $customerId, int $addressId): array
         return $this->shopify->Customer($customerId)->Address($addressId)->get();
     }
 
+    /**
+     * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any
+     * @return array
+     */
+    protected function getRequestParamsForOrders(): array
+    {
+        $params = [
+            'limit' => 250,
+        ];
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('order_statuses')) {
+            $params['status'] = implode(',', $this->ecommerceIntegration->array_meta_data['order_statuses']);
+        }
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
+            $params['financial_status'] = implode(',', $this->ecommerceIntegration->array_meta_data['financial_statuses']);
+        }
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
+            $params['fulfillment_status'] = implode(',', $this->ecommerceIntegration->array_meta_data['fulfillment_statuses']);
+        }
+
+        return $params;
+    }
+
     ##################
     # Order parsing: #
     ##################
 
     public function parseRawOrderJob(array $order): void
     {
-        if ($this->isNotDuplicate($order)) {
+        if (!CreateOrderService::isOrderExists([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'uuid' => (string)$order['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])) {
             Yii::$app->queue->push(
                 new ParseShopifyOrderJob([
                     'rawOrder' => $order,
@@ -215,13 +244,4 @@ public function parseRawOrderJob(array $order): void
             );
         }
     }
-
-    protected function isNotDuplicate(array $order): bool
-    {
-        return !Order::find()->where([
-            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'uuid' => (string)$order['id'],
-            'customer_id' => $this->ecommerceIntegration->customer_id,
-        ])->exists();
-    }
 }
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index c0d8dfc6..10325b83 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -3,13 +3,17 @@
 namespace console\controllers;
 
 use common\models\BulkAction;
+use common\models\EcommerceIntegration;
+use common\models\EcommercePlatform;
 use common\models\FulfillmentMeta;
 use common\models\Order;
 use common\models\ScheduledOrder;
+use common\services\platforms\ShopifyService;
 use console\jobs\orders\FetchJob;
 use console\jobs\orders\SendTo3PLJob;
 use yii\console\{Controller, ExitCode};
 use common\models\Integration;
+use yii\base\InvalidConfigException;
 use yii\db\Exception;
 
 // To create/edit crontab file: crontab -e
@@ -39,7 +43,7 @@ class CronController extends Controller
      * Action Index
      * @return int exit code
      */
-    public function actionIndex()
+    public function actionIndex(): int
     {
         $this->stdout('Yes, service cron is running');
         return ExitCode::OK;
@@ -49,8 +53,9 @@ public function actionIndex()
      * Action Frequent
      * Called every five minutes
      * @return int exit code
+     * @throws InvalidConfigException
      */
-    public function actionFrequent()
+    public function actionFrequent(): int
     {
         /**
          * 1. Loop through customers and customer meta data to find ecommerce site
@@ -67,9 +72,46 @@ public function actionFrequent()
         $this->runIntegrations(Integration::ACTIVE);
         $this->runScheduledOrders();
 
+        $this->runEcommerceIntegrations();
+
         return ExitCode::OK;
     }
 
+    /**
+     * @throws InvalidConfigException
+     */
+    protected function runEcommerceIntegrations(): void
+    {
+        $ecommerceIntegrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
+            switch ($ecommerceIntegration->ecommercePlatform->name) {
+
+                /**
+                 * Shopify:
+                 */
+                case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+
+                    $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
+
+                    if ($accessToken) {
+                        $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
+                        $orders = $shopifyService->getOrdersList();
+
+                        foreach ($orders as $order) {
+                            $shopifyService->parseRawOrderJob($order);
+                        }
+                    }
+
+                    break;
+
+            }
+        }
+    }
+
     public function runIntegrations($status)
     {
         /** @var Integration $integration */
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php
index ce4b65bd..35975089 100644
--- a/console/jobs/platforms/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/ParseShopifyOrderJob.php
@@ -36,17 +36,25 @@ class ParseShopifyOrderJob extends BaseObject implements RetryableJobInterface
     public function execute($queue): void
     {
         $this->setEcommerceIntegration();
-        $parsingErrors = $this->getParsingErrors();
 
-        if (!$parsingErrors) {
-            $this->parseOrderData();
-            $this->parseAddressData();
-            $this->parseItemsData();
-            $order = $this->saveOrder();
-
-            EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
-        } else {
-            EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
+        // Parse the order only if it doesn't exist in our table:
+        if (!CreateOrderService::isOrderExists([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'uuid' => (string)$this->rawOrder['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])) {
+            $parsingErrors = $this->getParsingErrors();
+
+            if (!$parsingErrors) {
+                $this->parseOrderData();
+                $this->parseAddressData();
+                $this->parseItemsData();
+                $order = $this->saveOrder();
+
+                EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+            } else {
+                EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
+            }
         }
     }
 
@@ -173,7 +181,7 @@ protected function parseItemsData(): void
         foreach ($this->rawOrder['line_items'] as $item) {
             $this->parsedItemsAttributes[] = [
                 'quantity' => $item['fulfillable_quantity'],
-                'sku' => ($item['sku']) ?: 'Not provided.',
+                'sku' => ($item['sku']) ?: 'Not provided',
                 'name' => $item['name'],
                 'uuid' => (string)$item['id'],
             ];

From 604978ccd976b996ce256a032ea9c5f1ecfde82b Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:25:36 +0200
Subject: [PATCH 17/81] Updated docs

---
 common/services/platforms/ShopifyService.php |  2 --
 intro-docs/ecommerce-platfroms.md            | 30 ++++++++++++++++----
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 854817c2..c5ebcd94 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,9 +2,7 @@
 
 namespace common\services\platforms;
 
-use common\models\EcommerceOrderLog;
 use Yii;
-use common\models\Order;
 use yii\base\InvalidConfigException;
 use yii\helpers\Json;
 use yii\helpers\Url;
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index b0dc92a8..4be6b583 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -7,19 +7,37 @@
 2. Implement a new `insert` query for the table `ecommerce_platform`. Specify the needed platform. Example - `console\migrations\m230221_134343_add_shopify_mock_ecommerce_platform.php`.
 
 3. Add implementation to:
-- `common\models\EcommercePlatform.php`
-- `common\models\EcommerceIntegration.php`
-- `frontend\controllers\EcommercePlatformController.php`
-- `frontend\controllers\EcommerceIntegrationController.php`
+- `common\models\EcommercePlatform`
+- `common\models\EcommerceIntegration`
+- `frontend\controllers\EcommercePlatformController`
+- `frontend\controllers\EcommerceIntegrationController`
+
+4. Create a Service similar to `common\services\platforms\ShopifyService`.
+
+5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
+
+### Cron:
+
+1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
+We need this method to pull existing orders from a needed E-commerce platform.
 
 ### How to manage existing platforms:
 
-1. Visit our website - `/ecommerce-platform` (you must be an `Admin`).
+1. Visit our website - URL: `/ecommerce-platform` (you must be an `Admin`).
 
 # Constraints
 
-1. A new integration can be added by a user only if the needed e-commerce platform
+1. A new integration (URL: `/ecommerce-integration`) can be added by a user only if the needed e-commerce platform
 has the status `Active`.
+   
+2. Try to use `common\services\platforms\CreateOrderService` when you parse raw orders from E-commerce platforms.
+
+3. In the cron method `runEcommerceIntegrations()`, only active (`status=connected`) E-commerce integrations are used for
+order pulling.
+   
+4. Once we cannot pull orders from an E-commerce platform like Shopify, the E-commerce integration must become `uninstalled` automatically.
+See `common\services\platforms\ShopifyService` -> `isTokenValid()` as an example. So we will not make any requests for the
+E-commerce integration until the user reconnects the shop (URL: `/ecommerce-integration`).
 
 # E-commerce Integrations - Shopify
 

From 188ac991e2d3c1265f0ff5333f58c0417d54e397 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:29:33 +0200
Subject: [PATCH 18/81] Updated docs

---
 intro-docs/ecommerce-platfroms.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 4be6b583..80b46c35 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -38,6 +38,9 @@ order pulling.
 4. Once we cannot pull orders from an E-commerce platform like Shopify, the E-commerce integration must become `uninstalled` automatically.
 See `common\services\platforms\ShopifyService` -> `isTokenValid()` as an example. So we will not make any requests for the
 E-commerce integration until the user reconnects the shop (URL: `/ecommerce-integration`).
+   
+5. Each E-commerce platform-integration can have specific user-based settings (access token, specific order statuses, etc.). 
+For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceIntegration`.
 
 # E-commerce Integrations - Shopify
 

From f96667f8bcc0db4dfe4576fdf840e6b82630765c Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:37:18 +0200
Subject: [PATCH 19/81] Removed test action

---
 .../EcommerceIntegrationController.php        | 45 -------------------
 intro-docs/ecommerce-platfroms.md             |  4 ++
 2 files changed, 4 insertions(+), 45 deletions(-)

diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index 2e9e9f44..2355e0e1 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -38,51 +38,6 @@ public function behaviors(): array
         ];
     }
 
-    public function actionTest()
-    {
-        $integrations = EcommerceIntegration::find()
-            ->active()
-            ->orderById()
-            ->all();
-
-        foreach ($integrations as $integration) {
-            $accessToken = $integration->array_meta_data['access_token'];
-
-            if ($accessToken) {
-                $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration);
-
-                /**
-                 * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any
-                 */
-                $params = [
-                    'limit' => 250,
-                ];
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('order_statuses')) {
-                    $params['status'] = implode(',', $integration->array_meta_data['order_statuses']);
-                }
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
-                    $params['financial_status'] = implode(',', $integration->array_meta_data['financial_statuses']);
-                }
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
-                    $params['fulfillment_status'] = implode(',', $integration->array_meta_data['fulfillment_statuses']);
-                }
-
-                $orders = $shopifyService->getOrdersList($params);
-
-//                echo '
';
-//                print_r($orders);
-//                exit;
-
-                foreach ($orders as $order) {
-                    $shopifyService->parseRawOrderJob($order);
-                }
-            }
-        }
-    }
-
     /**
      * Lists all EcommerceIntegration models for the current user.
      * @return string
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 80b46c35..377b072e 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -16,10 +16,14 @@
 
 5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
 
+> php yii queue/listen --verbose
+
 ### Cron:
 
 1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
 We need this method to pull existing orders from a needed E-commerce platform.
+   
+> php yii cron/frequent
 
 ### How to manage existing platforms:
 

From 1561462151cb980fc125982992cd0dd666e58858 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 9 Mar 2023 16:01:21 +0200
Subject: [PATCH 20/81] Register Shopify webhook listeners logic

---
 .../platforms/ConnectShopifyStoreForm.php     |   7 +-
 common/services/platforms/ShopifyService.php  |  91 ++++++++++++++-
 .../RegisterShopifyWebhookListenersJob.php    | 108 ++++++++++++++++++
 .../EcommerceWebhookController.php            |  67 +++++++++++
 intro-docs/ecommerce-platfroms.md             |   5 +
 5 files changed, 274 insertions(+), 4 deletions(-)
 create mode 100644 console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
 create mode 100644 frontend/controllers/EcommerceWebhookController.php

diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
index 916f3788..6384bd9d 100644
--- a/common/models/forms/platforms/ConnectShopifyStoreForm.php
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -131,7 +131,7 @@ public function auth(): void
      * @throws SdkException
      * @throws InvalidConfigException
      */
-    public function saveAccessToken(): void
+    public function saveAccessToken(bool $addWebHookListeners = true): void
     {
         $data = unserialize(Yii::$app->session->get('shopify_connection_second_step'));
 
@@ -142,6 +142,11 @@ public function saveAccessToken(): void
         // Step 2 - Receive and save access token:
         $shopifyService = new ShopifyService($this->url);
         $shopifyService->accessToken($data, $this->ecommerceIntegration);
+
+        // Add Webhook listeners:
+        if ($addWebHookListeners) {
+            $shopifyService->addWebhookListenersJob();
+        }
     }
 
     protected function saveDataForSecondStep()
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index c5ebcd94..39a2054e 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,6 +2,7 @@
 
 namespace common\services\platforms;
 
+use console\jobs\platforms\RegisterShopifyWebhookListenersJob;
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\Json;
@@ -23,6 +24,7 @@
  * @see https://shopify.dev/docs/api/usage/access-scopes
  * @see https://shopify.dev/docs/apps/webhooks
  * @see https://shopify.dev/docs/apps/webhooks/configuration
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook
  * @see https://github.com/phpclassic/php-shopify
  * @see https://community.shopify.com/c/shopify-apis-and-sdks/will-access-token-expired/td-p/559870
  */
@@ -61,6 +63,22 @@ class ShopifyService
         'unfulfilled' => 'Unfulfilled', // Returns orders with `fulfillment_status` of `null` or `partial`
     ];
 
+    public static string $webhooksUrl = '/ecommerce-webhook/shopify';
+
+    /**
+     * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics
+     */
+    public static array $webhookListeners = [
+        'orders/create',
+        'orders/cancelled',
+        'orders/updated',
+        'orders/delete',
+        'orders/fulfilled',
+        'orders/partially_fulfilled',
+        'orders/paid',
+        'app/uninstalled'
+    ];
+
     protected const API_VERSION = '2023-01';
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
@@ -146,6 +164,8 @@ public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegra
         if (!$ecommerceIntegration->save()) {
             throw new ServerErrorHttpException('Shopify integration is not added. Something went wrong.');
         }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
     }
 
     /**
@@ -168,6 +188,11 @@ protected function isTokenValid()
     # Get data via API: #
     #####################
 
+    public function getShop(): array
+    {
+        return $this->shopify->Shop->get();
+    }
+
     public function getProductsList(): array
     {
         return $this->shopify->Product->get();
@@ -223,9 +248,9 @@ protected function getRequestParamsForOrders(): array
         return $params;
     }
 
-    ##################
-    # Order parsing: #
-    ##################
+    #########
+    # Jobs: #
+    #########
 
     public function parseRawOrderJob(array $order): void
     {
@@ -242,4 +267,64 @@ public function parseRawOrderJob(array $order): void
             );
         }
     }
+
+    public function addWebhookListenersJob(): void
+    {
+        Yii::$app->queue->push(
+            new RegisterShopifyWebhookListenersJob([
+                'ecommerceIntegrationId' => $this->ecommerceIntegration->id
+            ])
+        );
+    }
+
+    #############
+    # Webhooks: #
+    #############
+
+    public function getWebhooksList(): array
+    {
+        return $this->shopify->Webhook()->get();
+    }
+
+    public function createWebhook($params): array
+    {
+        return $this->shopify->Webhook()->post($params);
+    }
+
+    public function getWebhookById(int $id): array
+    {
+        return $this->shopify->Webhook($id)->get();
+    }
+
+    public function deleteWebhookById(int $id): array
+    {
+        return $this->shopify->Webhook($id)->delete();
+    }
+
+//
+//    public function createWebhookOrderCreated()
+//    {
+//        $redirectDomain = trim(Url::to(['/'], true), '/');
+//
+//        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
+//            $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain'];
+//        }
+//
+////        $res = $this->shopify->Webhook()->post([
+////            'topic' => 'orders/updated',
+////            'address' => $redirectDomain . '/ecommerce-webhook/shopify?event=order_updated',
+////            'format' => 'json',
+////        ]);
+//
+//
+//
+////        $res = $this->shopify->Webhook(1266747506984)->get();
+// //       $res = $this->shopify->Webhook(1266742624552)->delete();
+//
+//        $res = $this->getWebhooksList();
+//
+//        echo '
';
+//        print_r($res);
+//        exit;
+//    }
 }
diff --git a/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php b/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
new file mode 100644
index 00000000..8e31f73b
--- /dev/null
+++ b/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
@@ -0,0 +1,108 @@
+setEcommerceIntegration();
+        $this->setShopifyService();
+        $this->setDomain();
+
+        // Register listeners:
+        foreach (ShopifyService::$webhookListeners as $listener) {
+            $this->sendRequest($listener);
+        }
+
+        // Update integration:
+        $this->updateIntegrationMetaData();
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceIntegration(): void
+    {
+        $ecommerceIntegration = EcommerceIntegration::findOne($this->ecommerceIntegrationId);
+
+        if (!$ecommerceIntegration) {
+            throw new NotFoundHttpException('E-commerce integration not found.');
+        }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
+    }
+
+    /**
+     * @throws InvalidConfigException
+     */
+    protected function setShopifyService(): void
+    {
+        $this->shopifyService = new ShopifyService($this->ecommerceIntegration->array_meta_data['shop_url'], $this->ecommerceIntegration);
+    }
+
+    protected function setDomain(): void
+    {
+        $this->domain = trim(Url::to(['/'], true), '/');
+
+        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
+            $this->domain = Yii::$app->params['shopify']['override_redirect_domain'];
+        }
+    }
+
+    protected function updateIntegrationMetaData(): void
+    {
+        $this->ecommerceIntegration->array_meta_data['connected_webhook_listeners'] = ShopifyService::$webhookListeners;
+        $this->ecommerceIntegration->meta = Json::encode($this->ecommerceIntegration->array_meta_data, JSON_PRETTY_PRINT);
+        $this->ecommerceIntegration->save();
+    }
+
+    protected function sendRequest(string $event): void
+    {
+        $res = $this->shopifyService->createWebhook([
+            'topic' => $event,
+            'address' => $this->domain . ShopifyService::$webhooksUrl . '?event=' . $event,
+            'format' => 'json',
+        ]);
+
+//        echo '
' . $event . ': ';
+//        print_r($res);
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
new file mode 100644
index 00000000..7b7be857
--- /dev/null
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -0,0 +1,67 @@
+enableCsrfValidation = false;
+        Yii::$app->response->format = Response::FORMAT_JSON;
+        return parent::beforeAction($action);
+    }
+
+    /**
+     * @return array
+     * @throws NotFoundHttpException
+     */
+    public function actionShopify(): array
+    {
+        $event = Yii::$app->request->get('event');
+
+        if (!in_array($event, ShopifyService::$webhookListeners)) {
+            throw new NotFoundHttpException('Event not found.');
+        }
+
+        return [
+            'result' => 'success',
+        ];
+    }
+
+    public function actionTest()
+    {
+        $ecommerceIntegrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
+            $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
+
+            if ($accessToken) {
+                $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
+                return $shopifyService->getWebhooksList();
+            }
+        }
+    }
+}
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 377b072e..1ebca6e5 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -46,6 +46,11 @@ E-commerce integration until the user reconnects the shop (URL: `/ecommerce-inte
 5. Each E-commerce platform-integration can have specific user-based settings (access token, specific order statuses, etc.). 
 For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceIntegration`.
 
+6. For storing webhook listeners list, also use the `meta` field.
+
+7. When you save orders to the table `orders`, use the field `uuid` for storing the Order ID of the original E-commerce platform.
+
+
 # E-commerce Integrations - Shopify
 
 ### App:

From 29618aa07a8e04a88bb7dbb085581d8c203e8c5a Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 9 Mar 2023 18:12:27 +0200
Subject: [PATCH 21/81] E-commerce webhooks + MetaDataFieldTrait + updated docs

---
 common/models/EcommerceIntegration.php        | 18 +----
 common/models/EcommerceOrderLog.php           |  3 +
 common/models/EcommercePlatform.php           |  5 +-
 common/models/EcommerceWebhook.php            | 56 +++++++++++++++
 common/models/base/BaseEcommerceWebhook.php   | 71 +++++++++++++++++++
 .../platforms/ConnectShopifyStoreForm.php     |  9 +--
 common/services/platforms/ShopifyService.php  | 39 ++--------
 common/traits/MetaDataFieldTrait.php          | 26 +++++++
 ..._141820_create_table_ecommerce_webhook.php | 55 ++++++++++++++
 .../EcommerceWebhookController.php            | 61 +++++++++++++---
 intro-docs/ecommerce-platfroms.md             |  5 ++
 11 files changed, 280 insertions(+), 68 deletions(-)
 create mode 100644 common/models/EcommerceWebhook.php
 create mode 100644 common/models/base/BaseEcommerceWebhook.php
 create mode 100644 common/traits/MetaDataFieldTrait.php
 create mode 100644 console/migrations/m230309_141820_create_table_ecommerce_webhook.php

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index fe590942..2fd148c5 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -4,9 +4,9 @@
 
 use Yii;
 use yii\helpers\Html;
-use yii\helpers\Json;
 use common\models\base\BaseEcommerceIntegration;
 use console\jobs\NotificationJob;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommerceIntegration
@@ -14,12 +14,12 @@
  */
 class EcommerceIntegration extends BaseEcommerceIntegration
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_INTEGRATION_CONNECTED = 1;
     public const STATUS_INTEGRATION_PAUSED = 0;
     public const STATUS_INTEGRATION_UNINSTALLED = -1;
 
-    public array $array_meta_data = [];
-
     public function init(): void
     {
         parent::init();
@@ -90,16 +90,4 @@ public function resume(): bool
         $this->status = self::STATUS_INTEGRATION_CONNECTED;
         return $this->save();
     }
-
-    public function isMetaKeyExistsAndNotEmpty(string $key): bool
-    {
-        return (isset($this->array_meta_data[$key]) && !empty($this->array_meta_data[$key]));
-    }
-
-    protected function convertMetaData(): void
-    {
-        if ($this->meta) {
-            $this->array_meta_data = Json::decode($this->meta);
-        }
-    }
 }
diff --git a/common/models/EcommerceOrderLog.php b/common/models/EcommerceOrderLog.php
index 5ef65f44..131659a0 100644
--- a/common/models/EcommerceOrderLog.php
+++ b/common/models/EcommerceOrderLog.php
@@ -4,6 +4,7 @@
 
 use yii\helpers\Json;
 use common\models\base\BaseEcommerceOrderLog;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommerceOrderLog
@@ -11,6 +12,8 @@
  */
 class EcommerceOrderLog extends BaseEcommerceOrderLog
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_SUCCESS = 'success';
     public const STATUS_FAILED = 'failed';
 
diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php
index 6895fa9c..7854a88e 100644
--- a/common/models/EcommercePlatform.php
+++ b/common/models/EcommercePlatform.php
@@ -2,8 +2,9 @@
 
 namespace common\models;
 
-use common\models\base\BaseEcommercePlatform;
 use yii\db\ActiveQuery;
+use common\models\base\BaseEcommercePlatform;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommercePlatform
@@ -11,6 +12,8 @@
  */
 class EcommercePlatform extends BaseEcommercePlatform
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_PLATFORM_ACTIVE = 1;
     public const STATUS_PLATFORM_INACTIVE = 0;
 
diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
new file mode 100644
index 00000000..53dbbf8d
--- /dev/null
+++ b/common/models/EcommerceWebhook.php
@@ -0,0 +1,56 @@
+on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']);
+    }
+
+    public static function getStatuses(): array
+    {
+        return [
+            self::STATUS_RECEIVED => 'Received',
+            self::STATUS_PROCESSING => 'Processing',
+            self::STATUS_SUCCESS => 'Success',
+            self::STATUS_FAILED => 'Failed',
+        ];
+    }
+
+    public function isReceived(): bool
+    {
+        return $this->status === self::STATUS_RECEIVED;
+    }
+
+    public function isProcessing(): bool
+    {
+        return $this->status === self::STATUS_PROCESSING;
+    }
+
+    public function isSuccess(): bool
+    {
+        return $this->status === self::STATUS_SUCCESS;
+    }
+
+    public function isFailed(): bool
+    {
+        return $this->status === self::STATUS_FAILED;
+    }
+}
diff --git a/common/models/base/BaseEcommerceWebhook.php b/common/models/base/BaseEcommerceWebhook.php
new file mode 100644
index 00000000..eba91ed1
--- /dev/null
+++ b/common/models/base/BaseEcommerceWebhook.php
@@ -0,0 +1,71 @@
+ 64],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels(): array
+    {
+        return [
+            'id' => 'ID',
+            'platform_id' => 'Platform ID',
+            'status' => 'Status',
+            'event' => 'Event',
+            'payload' => 'Payload',
+            'meta' => 'Meta',
+            'created_date' => 'Created Date',
+            'updated_date' => 'Updated Date',
+        ];
+    }
+
+    /**
+     * @return ActiveQuery
+     */
+    public function getPlatform(): ActiveQuery
+    {
+        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+    }
+}
diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
index 6384bd9d..07ed3670 100644
--- a/common/models/forms/platforms/ConnectShopifyStoreForm.php
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -3,15 +3,12 @@
 namespace common\models\forms\platforms;
 
 use Yii;
-use yii\base\InvalidConfigException;
-use yii\base\Model;
+use yii\base\{InvalidConfigException, Model};
 use yii\db\Expression;
 use PHPShopify\Exception\SdkException;
 use yii\web\ServerErrorHttpException;
 use common\services\platforms\ShopifyService;
-use common\models\EcommerceIntegration;
-use common\models\EcommercePlatform;
-use common\models\Customer;
+use common\models\{EcommerceIntegration, EcommercePlatform, Customer};
 
 /**
  * Class ConnectShopifyStoreForm
@@ -149,7 +146,7 @@ public function saveAccessToken(bool $addWebHookListeners = true): void
         }
     }
 
-    protected function saveDataForSecondStep()
+    protected function saveDataForSecondStep(): void
     {
         $data = [
             'shop_name' => $this->name,
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 39a2054e..c2e064af 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,16 +2,12 @@
 
 namespace common\services\platforms;
 
-use console\jobs\platforms\RegisterShopifyWebhookListenersJob;
 use Yii;
 use yii\base\InvalidConfigException;
-use yii\helpers\Json;
-use yii\helpers\Url;
-use console\jobs\platforms\ParseShopifyOrderJob;
-use common\models\EcommerceIntegration;
-use common\models\EcommercePlatform;
-use PHPShopify\ShopifySDK;
-use PHPShopify\AuthHelper;
+use yii\helpers\{Json, Url};
+use console\jobs\platforms\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use common\models\{EcommerceIntegration, EcommercePlatform};
+use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
 use PHPShopify\Exception\SdkException;
 
@@ -300,31 +296,4 @@ public function deleteWebhookById(int $id): array
     {
         return $this->shopify->Webhook($id)->delete();
     }
-
-//
-//    public function createWebhookOrderCreated()
-//    {
-//        $redirectDomain = trim(Url::to(['/'], true), '/');
-//
-//        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
-//            $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain'];
-//        }
-//
-////        $res = $this->shopify->Webhook()->post([
-////            'topic' => 'orders/updated',
-////            'address' => $redirectDomain . '/ecommerce-webhook/shopify?event=order_updated',
-////            'format' => 'json',
-////        ]);
-//
-//
-//
-////        $res = $this->shopify->Webhook(1266747506984)->get();
-// //       $res = $this->shopify->Webhook(1266742624552)->delete();
-//
-//        $res = $this->getWebhooksList();
-//
-//        echo '
';
-//        print_r($res);
-//        exit;
-//    }
 }
diff --git a/common/traits/MetaDataFieldTrait.php b/common/traits/MetaDataFieldTrait.php
new file mode 100644
index 00000000..bc74a932
--- /dev/null
+++ b/common/traits/MetaDataFieldTrait.php
@@ -0,0 +1,26 @@
+array_meta_data[$key]) && !empty($this->array_meta_data[$key]));
+    }
+
+    protected function convertMetaData(): void
+    {
+        if ($this->meta) {
+            $this->array_meta_data = Json::decode($this->meta);
+        }
+    }
+}
diff --git a/console/migrations/m230309_141820_create_table_ecommerce_webhook.php b/console/migrations/m230309_141820_create_table_ecommerce_webhook.php
new file mode 100644
index 00000000..9e7fd28f
--- /dev/null
+++ b/console/migrations/m230309_141820_create_table_ecommerce_webhook.php
@@ -0,0 +1,55 @@
+execute("
+            CREATE TABLE `ecommerce_webhook` (
+              `id` int NOT NULL AUTO_INCREMENT,
+              `platform_id` int NOT NULL,
+              `status` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+              `event` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+              `payload` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+                `meta` MEDIUMTEXT NULL DEFAULT NULL, 
+                `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 
+                `updated_date` DATETIME NULL DEFAULT NULL,
+              PRIMARY KEY (`id`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+        ");
+
+        $this->execute("
+            ALTER TABLE `ecommerce_webhook` 
+                CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
+        ");
+
+        $this->execute("
+            ALTER TABLE `ecommerce_webhook` ADD INDEX(`status`);
+        ");
+
+        $this->addForeignKey(
+            '{{%fk-ecommerce_webhook-platform_id}}',
+            '{{%ecommerce_webhook}}',
+            'platform_id',
+            '{{%ecommerce_platform}}',
+            'id',
+            'CASCADE'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function safeDown()
+    {
+        $this->dropTable("{{%ecommerce_webhook}}");
+    }
+}
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 7b7be857..7a093109 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -2,18 +2,10 @@
 
 namespace frontend\controllers;
 
-use common\models\EcommerceIntegration;
-use common\services\platforms\ShopifyService;
 use Yii;
-use yii\filters\VerbFilter;
-use yii\web\BadRequestHttpException;
-use yii\web\Response;
-use Da\User\Filter\AccessRuleFilter;
-use common\models\EcommercePlatform;
-use common\models\search\EcommercePlatformSearch;
-use yii\filters\AccessControl;
-use yii\web\Controller;
-use yii\web\NotFoundHttpException;
+use yii\web\{BadRequestHttpException, Response, Controller, NotFoundHttpException, ServerErrorHttpException};
+use common\models\{EcommercePlatform, EcommerceWebhook, EcommerceIntegration};
+use common\services\platforms\ShopifyService;
 
 /**
  * Class EcommerceWebhookController
@@ -32,22 +24,69 @@ public function beforeAction($action): bool
     }
 
     /**
+     * Receives and saves webhooks from the Shopify e-commerce platform.
      * @return array
      * @throws NotFoundHttpException
+     * @throws ServerErrorHttpException
      */
     public function actionShopify(): array
     {
+        // file_put_contents('shopify.txt', "\r\n\r\n\r\n" . Yii::$app->request->rawBody, FILE_APPEND);
+
+
+        $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
         if (!in_array($event, ShopifyService::$webhookListeners)) {
             throw new NotFoundHttpException('Event not found.');
         }
 
+        $ecommerceWebhook = $this->getNewEcommerceWebhookObject($ecommercePlatform->id);
+        $ecommerceWebhook->event = $event;
+        $ecommerceWebhook->payload = Yii::$app->request->rawBody;
+
+        if (!$ecommerceWebhook->save()) {
+            throw new ServerErrorHttpException('Event not saved.');
+        }
+
         return [
             'result' => 'success',
         ];
     }
 
+    /**
+     * @throws NotFoundHttpException
+     * @throws ServerErrorHttpException
+     */
+    protected function getEcommercePlatformByName(string $name): EcommercePlatform
+    {
+        /**
+         * @var EcommercePlatform $model
+         */
+        $model = EcommercePlatform::find()
+            ->where(['name' => $name])
+            ->one();
+
+        if (!$model) {
+            throw new NotFoundHttpException('Ecommerce platform does not exist.');
+        }
+
+        if (!$model->isActive()) {
+            throw new ServerErrorHttpException('Ecommerce platform is not active.');
+        }
+
+        return $model;
+    }
+
+    protected function getNewEcommerceWebhookObject(int $ecommercePlatformId): EcommerceWebhook
+    {
+        $ecommerceWebhook = new EcommerceWebhook();
+        $ecommerceWebhook->platform_id = $ecommercePlatformId;
+        $ecommerceWebhook->status = EcommerceWebhook::STATUS_RECEIVED;
+
+        return $ecommerceWebhook;
+    }
+
     public function actionTest()
     {
         $ecommerceIntegrations = EcommerceIntegration::find()
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 1ebca6e5..0d3344b3 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -11,6 +11,7 @@
 - `common\models\EcommerceIntegration`
 - `frontend\controllers\EcommercePlatformController`
 - `frontend\controllers\EcommerceIntegrationController`
+- `frontend\controllers\EcommerceWebhookController`
 
 4. Create a Service similar to `common\services\platforms\ShopifyService`.
 
@@ -50,6 +51,10 @@ For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceI
 
 7. When you save orders to the table `orders`, use the field `uuid` for storing the Order ID of the original E-commerce platform.
 
+8. Use the controller `frontend\controllers\EcommerceWebhookController` for accepting webhook requests from E-commerce platforms.
+
+9. Don't accept webhooks if our E-commerce Platform `is not active`. See `frontend\controllers\EcommerceWebhookController` -> `getEcommercePlatformByName()`. 
+
 
 # E-commerce Integrations - Shopify
 

From e964d064be10a54aa004bed0daaadb6f895368d8 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Fri, 10 Mar 2023 14:50:28 +0200
Subject: [PATCH 22/81] Mandatory webhooks + docs updated

---
 common/services/platforms/ShopifyService.php  |  9 ++++++++
 .../EcommerceWebhookController.php            |  4 +++-
 intro-docs/ecommerce-platfroms.md             | 22 +++++++++++++++++++
 3 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index c2e064af..b8e2c05b 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -75,6 +75,15 @@ class ShopifyService
         'app/uninstalled'
     ];
 
+    /**
+     * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks
+     */
+    public static array $mandatoryWebhookListeners = [
+        'customers/data_request',
+        'customers/redact',
+        'shop/redact',
+    ];
+
     protected const API_VERSION = '2023-01';
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 7a093109..4b870b3c 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -37,7 +37,8 @@ public function actionShopify(): array
         $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
-        if (!in_array($event, ShopifyService::$webhookListeners)) {
+        if (!in_array($event, ShopifyService::$webhookListeners) &&
+            !in_array($event, ShopifyService::$mandatoryWebhookListeners)) {
             throw new NotFoundHttpException('Event not found.');
         }
 
@@ -50,6 +51,7 @@ public function actionShopify(): array
         }
 
         return [
+            'status' => 200,
             'result' => 'success',
         ];
     }
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 0d3344b3..bbcb0676 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -98,3 +98,25 @@ In `Data and configurations`, choose `Start with test data`.
 2. In the form, specify your test shop's details (`name` and `URL`).
 
 3. If you want to remove the Shopify app from your test shop, visit `https://admin.shopify.com/` -> `Apps` -> `Apps and sales channels` -> and press `Uninstall`.
+
+### Important URLs:
+
+*Common:*
+
+- https://partners.shopify.com/
+- https://github.com/phpclassic/php-shopify
+  
+*API:*
+
+- https://shopify.dev/docs/apps/auth/oauth/getting-started
+- https://shopify.dev/docs/api/usage/access-scopes
+- https://community.shopify.com/c/shopify-apis-and-sdks/will-access-token-expired/td-p/559870
+- https://shopify.dev/docs/apps/store/data-protection/protected-customer-data
+
+*Webhooks:*
+
+- https://shopify.dev/docs/apps/webhooks
+- https://shopify.dev/docs/apps/webhooks/configuration
+- https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook
+- https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics
+- https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks

From 7b95cf2c07037b88b6303d3a628172b08f9e06db Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Fri, 10 Mar 2023 20:09:22 +0200
Subject: [PATCH 23/81] Cron -> runEcommerceWebhooks(), EcommerceWebhook ->
 statuses, BaseWebhookProcessingJob, initial Jobs for Shopify webhooks
 processing

---
 common/models/EcommerceWebhook.php            | 110 ++++++++++++++++++
 .../models/base/BaseEcommerceIntegration.php  |   6 +-
 common/models/base/BaseEcommerceOrderLog.php  |  19 ++-
 common/models/base/BaseEcommerceWebhook.php   |  13 ++-
 common/models/query/EcommerceWebhookQuery.php |  29 +++++
 common/services/platforms/ShopifyService.php  |   2 +-
 console/controllers/CronController.php        |  24 ++++
 .../{ => shopify}/ParseShopifyOrderJob.php    |  20 ++--
 .../RegisterShopifyWebhookListenersJob.php    |  10 +-
 .../webhooks/BaseWebhookProcessingJob.php     |  73 ++++++++++++
 .../shopify/ShopifyAppUninstalledJob.php      |  18 +++
 .../shopify/ShopifyCustomerDataRequestJob.php |  18 +++
 .../shopify/ShopifyCustomerRedactJob.php      |  18 +++
 .../shopify/ShopifyOrderCancelledJob.php      |  18 +++
 .../shopify/ShopifyOrderCreatedJob.php        |  18 +++
 .../shopify/ShopifyOrderDeletedJob.php        |  18 +++
 .../shopify/ShopifyOrderFulfilledJob.php      |  18 +++
 .../webhooks/shopify/ShopifyOrderPaidJob.php  |  18 +++
 .../ShopifyOrderPartiallyFulfilledJob.php     |  18 +++
 .../shopify/ShopifyOrderUpdatedJob.php        |  18 +++
 .../webhooks/shopify/ShopifyShopRedactJob.php |  18 +++
 21 files changed, 470 insertions(+), 34 deletions(-)
 create mode 100644 common/models/query/EcommerceWebhookQuery.php
 rename console/jobs/platforms/{ => shopify}/ParseShopifyOrderJob.php (95%)
 rename console/jobs/platforms/{ => shopify}/RegisterShopifyWebhookListenersJob.php (94%)
 create mode 100644 console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php

diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
index 53dbbf8d..6c4f8abc 100644
--- a/common/models/EcommerceWebhook.php
+++ b/common/models/EcommerceWebhook.php
@@ -2,8 +2,21 @@
 
 namespace common\models;
 
+use Yii;
 use common\models\base\BaseEcommerceWebhook;
 use common\traits\MetaDataFieldTrait;
+use common\services\platforms\ShopifyService;
+use console\jobs\platforms\webhooks\shopify\{ShopifyAppUninstalledJob,
+    ShopifyCustomerDataRequestJob,
+    ShopifyCustomerRedactJob,
+    ShopifyOrderCancelledJob,
+    ShopifyOrderCreatedJob,
+    ShopifyOrderDeletedJob,
+    ShopifyOrderFulfilledJob,
+    ShopifyOrderPaidJob,
+    ShopifyOrderPartiallyFulfilledJob,
+    ShopifyOrderUpdatedJob,
+    ShopifyShopRedactJob};
 
 /**
  * Class EcommerceWebhook
@@ -24,6 +37,10 @@ public function init(): void
         $this->on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']);
     }
 
+    #############
+    # Statuses: #
+    #############
+
     public static function getStatuses(): array
     {
         return [
@@ -53,4 +70,97 @@ public function isFailed(): bool
     {
         return $this->status === self::STATUS_FAILED;
     }
+
+    public function setReceived(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_RECEIVED;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setProcessing(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_PROCESSING;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setSuccess(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_SUCCESS;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setFailed(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_FAILED;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    #########
+    # Jobs: #
+    #########
+
+    public function createJob(): bool
+    {
+        if ($this->isReceived() && $this->isJobExecutable()) {
+            switch ($this->platform->name) {
+                case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+                    $this->createJobForShopify();
+                    break;
+            }
+
+            //$this->setProcessing();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function isJobExecutable(): bool
+    {
+        // All webhook events from all E-commerce platforms:
+        $availableEvents = array_merge(
+            ShopifyService::$webhookListeners,
+            ShopifyService::$mandatoryWebhookListeners
+        );
+
+        if ($this->platform->isActive() && in_array($this->event, $availableEvents) && $this->payload) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function createJobForShopify()
+    {
+        $job = match ($this->event) {
+            'orders/create' => new ShopifyOrderCreatedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/cancelled' => new ShopifyOrderCancelledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/updated' => new ShopifyOrderUpdatedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/delete' => new ShopifyOrderDeletedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/fulfilled' => new ShopifyOrderFulfilledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/partially_fulfilled' => new ShopifyOrderPartiallyFulfilledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/paid' => new ShopifyOrderPaidJob(['ecommerceWebhookId' => $this->id]),
+            'app/uninstalled' => new ShopifyAppUninstalledJob(['ecommerceWebhookId' => $this->id]),
+            'customers/data_request' => new ShopifyCustomerDataRequestJob(['ecommerceWebhookId' => $this->id]),
+            'customers/redact' => new ShopifyCustomerRedactJob(['ecommerceWebhookId' => $this->id]),
+            'shop/redact' => new ShopifyShopRedactJob(['ecommerceWebhookId' => $this->id]),
+        };
+
+        if (isset($job)) {
+            Yii::$app->queue->push($job);
+        }
+    }
 }
diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php
index c4de3a64..cb211e84 100644
--- a/common/models/base/BaseEcommerceIntegration.php
+++ b/common/models/base/BaseEcommerceIntegration.php
@@ -2,11 +2,9 @@
 
 namespace common\models\base;
 
-use yii\db\ActiveRecord;
-use yii\db\ActiveQuery;
+use yii\db\{ActiveRecord, ActiveQuery};
+use common\models\{EcommercePlatform, Customer};
 use common\models\query\EcommerceIntegrationQuery;
-use common\models\EcommercePlatform;
-use common\models\Customer;
 use frontend\models\User;
 
 /**
diff --git a/common/models/base/BaseEcommerceOrderLog.php b/common/models/base/BaseEcommerceOrderLog.php
index d6a49bdb..a37bc58b 100644
--- a/common/models/base/BaseEcommerceOrderLog.php
+++ b/common/models/base/BaseEcommerceOrderLog.php
@@ -3,11 +3,8 @@
 namespace common\models\base;
 
 use Yii;
-use yii\db\ActiveQuery;
-use yii\db\ActiveRecord;
-use common\models\EcommercePlatform;
-use common\models\EcommerceIntegration;
-use common\models\Order;
+use yii\db\{ActiveQuery, ActiveRecord};
+use common\models\{EcommercePlatform, EcommerceIntegration, Order};
 
 /**
  * This is the model class for table "ecommerce_order_log".
@@ -50,9 +47,9 @@ public function rules(): array
             [['created_date', 'updated_date'], 'safe'],
             [['status'], 'string', 'max' => 64],
             [['original_order_id'], 'string', 'max' => 256],
-            [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::className(), 'targetAttribute' => ['integration_id' => 'id']],
-            [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::className(), 'targetAttribute' => ['internal_order_id' => 'id']],
-            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+            [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::class, 'targetAttribute' => ['integration_id' => 'id']],
+            [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::class, 'targetAttribute' => ['internal_order_id' => 'id']],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::class, 'targetAttribute' => ['platform_id' => 'id']],
         ];
     }
 
@@ -80,7 +77,7 @@ public function attributeLabels(): array
      */
     public function getIntegration(): ActiveQuery
     {
-        return $this->hasOne(EcommerceIntegration::className(), ['id' => 'integration_id']);
+        return $this->hasOne(EcommerceIntegration::class, ['id' => 'integration_id']);
     }
 
     /**
@@ -88,7 +85,7 @@ public function getIntegration(): ActiveQuery
      */
     public function getInternalOrder(): ActiveQuery
     {
-        return $this->hasOne(Order::className(), ['id' => 'internal_order_id']);
+        return $this->hasOne(Order::class, ['id' => 'internal_order_id']);
     }
 
     /**
@@ -96,6 +93,6 @@ public function getInternalOrder(): ActiveQuery
      */
     public function getPlatform(): ActiveQuery
     {
-        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+        return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']);
     }
 }
diff --git a/common/models/base/BaseEcommerceWebhook.php b/common/models/base/BaseEcommerceWebhook.php
index eba91ed1..c8042d77 100644
--- a/common/models/base/BaseEcommerceWebhook.php
+++ b/common/models/base/BaseEcommerceWebhook.php
@@ -4,6 +4,7 @@
 
 use yii\db\{ActiveQuery, ActiveRecord};
 use common\models\{EcommercePlatform};
+use common\models\query\EcommerceWebhookQuery;
 
 /**
  * This is the model class for table "ecommerce_webhook".
@@ -21,6 +22,14 @@
  */
 class BaseEcommerceWebhook extends ActiveRecord
 {
+    /**
+     * @return EcommerceWebhookQuery
+     */
+    public static function find(): EcommerceWebhookQuery
+    {
+        return new EcommerceWebhookQuery(get_called_class());
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -40,7 +49,7 @@ public function rules(): array
             [['payload', 'meta'], 'string'],
             [['created_date', 'updated_date'], 'safe'],
             [['status', 'event'], 'string', 'max' => 64],
-            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::class, 'targetAttribute' => ['platform_id' => 'id']],
         ];
     }
 
@@ -66,6 +75,6 @@ public function attributeLabels(): array
      */
     public function getPlatform(): ActiveQuery
     {
-        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+        return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']);
     }
 }
diff --git a/common/models/query/EcommerceWebhookQuery.php b/common/models/query/EcommerceWebhookQuery.php
new file mode 100644
index 00000000..f1811973
--- /dev/null
+++ b/common/models/query/EcommerceWebhookQuery.php
@@ -0,0 +1,29 @@
+andWhere(['status' => EcommerceWebhook::STATUS_RECEIVED]);
+    }
+
+    public function forPlatformId(int $platformId): EcommerceWebhookQuery
+    {
+        $this->andWhere(['platform_id' => $platformId]);
+        return $this;
+    }
+
+    public function orderById(int $sort = SORT_ASC): EcommerceWebhookQuery
+    {
+        return $this->orderBy(['id' => $sort]);
+    }
+}
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index b8e2c05b..225c5315 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -5,7 +5,7 @@
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\{Json, Url};
-use console\jobs\platforms\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use console\jobs\platforms\shopify\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
 use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index 10325b83..b597207a 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -5,6 +5,7 @@
 use common\models\BulkAction;
 use common\models\EcommerceIntegration;
 use common\models\EcommercePlatform;
+use common\models\EcommerceWebhook;
 use common\models\FulfillmentMeta;
 use common\models\Order;
 use common\models\ScheduledOrder;
@@ -73,11 +74,14 @@ public function actionFrequent(): int
         $this->runScheduledOrders();
 
         $this->runEcommerceIntegrations();
+        $this->runEcommerceWebhooks();
 
         return ExitCode::OK;
     }
 
     /**
+     * This method is used for pulling first (initial) raw orders from E-commerce platforms like Shopify.
+     * For working with webhooks, the method `runEcommerceWebhooks()` is used.
      * @throws InvalidConfigException
      */
     protected function runEcommerceIntegrations(): void
@@ -85,6 +89,7 @@ protected function runEcommerceIntegrations(): void
         $ecommerceIntegrations = EcommerceIntegration::find()
             ->active()
             ->orderById()
+            ->limit(100)
             ->all();
 
         foreach ($ecommerceIntegrations as $ecommerceIntegration) {
@@ -112,6 +117,25 @@ protected function runEcommerceIntegrations(): void
         }
     }
 
+    /**
+     * This method is used for creating Jobs for webhooks with the status "received".
+     */
+    protected function runEcommerceWebhooks(): void
+    {
+        $ecommerceWebhooks = EcommerceWebhook::find()
+            ->received()
+            ->orderById()
+            ->limit(100)
+            ->all();
+
+        /**
+         * @var $ecommerceWebhook EcommerceWebhook
+         */
+        foreach ($ecommerceWebhooks as $ecommerceWebhook) {
+            $ecommerceWebhook->createJob();
+        }
+    }
+
     public function runIntegrations($status)
     {
         /** @var Integration $integration */
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
similarity index 95%
rename from console/jobs/platforms/ParseShopifyOrderJob.php
rename to console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 35975089..da01cbd3 100644
--- a/console/jobs/platforms/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -1,14 +1,14 @@
 setEcommerceWebhook();
+
+        if ($this->isPayloadJson()) {
+            $this->arrayPayload = Json::decode($this->ecommerceWebhook->payload);
+        }
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceWebhook(): void
+    {
+        $ecommerceWebhook = EcommerceWebhook::findOne($this->ecommerceWebhookId);
+
+        if (!$ecommerceWebhook) {
+            throw new NotFoundHttpException('E-commerce webhook not found.');
+        }
+
+        $this->ecommerceWebhook = $ecommerceWebhook;
+    }
+
+    /**
+     * Tries to find our internal Order by provided external ID. External ID = uuid.
+     * Use this method for webhook events like "order update/delete/cancel".
+     * @param int $id
+     * @return Order|null
+     */
+    protected function getOrderByExternalId(int $id): Order|null
+    {
+        return Order::find()->where(['uuid' => $id])->one();
+    }
+
+    protected function isPayloadJson(): bool
+    {
+        json_decode($this->ecommerceWebhook->payload);
+        return json_last_error() === JSON_ERROR_NONE;
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
new file mode 100644
index 00000000..9b017e24
--- /dev/null
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -0,0 +1,18 @@
+
Date: Mon, 13 Mar 2023 14:38:49 +0200
Subject: [PATCH 24/81] Dummy webhook status update

---
 common/models/EcommerceWebhook.php                              | 2 --
 console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php    | 1 +
 .../platforms/webhooks/shopify/ShopifyAppUninstalledJob.php     | 1 +
 .../webhooks/shopify/ShopifyCustomerDataRequestJob.php          | 1 +
 .../platforms/webhooks/shopify/ShopifyCustomerRedactJob.php     | 1 +
 .../platforms/webhooks/shopify/ShopifyOrderCancelledJob.php     | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php  | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php  | 1 +
 .../platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php     | 1 +
 console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php | 1 +
 .../webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php      | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php  | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php    | 1 +
 13 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
index 6c4f8abc..c336d2c7 100644
--- a/common/models/EcommerceWebhook.php
+++ b/common/models/EcommerceWebhook.php
@@ -120,8 +120,6 @@ public function createJob(): bool
                     break;
             }
 
-            //$this->setProcessing();
-
             return true;
         }
 
diff --git a/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php b/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
index 81ae6747..d9edf713 100644
--- a/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
+++ b/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
@@ -24,6 +24,7 @@ abstract class BaseWebhookProcessingJob extends BaseObject implements RetryableJ
     public function execute($queue): void
     {
         $this->setEcommerceWebhook();
+        $this->ecommerceWebhook->setProcessing();
 
         if ($this->isPayloadJson()) {
             $this->arrayPayload = Json::decode($this->ecommerceWebhook->payload);
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index 9b017e24..58054b3e 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " app uninstalled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
index f768afea..ef994a7a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " customer data request ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
index d2a0eaf5..a4d17ac5 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " customer redact ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index ca8f2652..303e79b0 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " cancelled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
index b05c66c1..92bb64bc 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " created ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
index 687d7d50..55c56925 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " deleted ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
index 730d8929..6b442b8f 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " fulfilled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
index 4a529094..b73551a4 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " paid ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
index 1b881237..7939c152 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " part. fulfilled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index b540e4aa..1748465a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " updated ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
index 4aa5d504..6b8bf313 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " shop redact ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }

From da3f58095e6ec23ee37a14393c0df1e025e5f394 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 15:27:08 +0200
Subject: [PATCH 25/81] Unregister Shopify Webhook Listeners Job

---
 common/models/EcommerceIntegration.php        | 15 ++++++
 common/services/platforms/ShopifyService.php  | 17 ++++++-
 .../UnregisterShopifyWebhookListenersJob.php  | 50 +++++++++++++++++++
 3 files changed, 80 insertions(+), 2 deletions(-)
 create mode 100644 console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index 2fd148c5..ed6c5135 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -7,6 +7,7 @@
 use common\models\base\BaseEcommerceIntegration;
 use console\jobs\NotificationJob;
 use common\traits\MetaDataFieldTrait;
+use common\services\platforms\ShopifyService;
 
 /**
  * Class EcommerceIntegration
@@ -50,8 +51,22 @@ public function isUninstalled(): bool
         return $this->status === self::STATUS_INTEGRATION_UNINSTALLED;
     }
 
+    /**
+     * @throws \yii\db\StaleObjectException
+     * @throws \Throwable
+     * @throws \yii\base\InvalidConfigException
+     */
     public function disconnect(): bool|int
     {
+        switch ($this->platform->name) {
+            case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+
+                $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
+                $shopifyService->deleteWebhookListenersJob();
+                
+                break;
+        }
+
         return $this->delete();
     }
 
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 225c5315..83d38d8e 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -5,7 +5,9 @@
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\{Json, Url};
-use console\jobs\platforms\shopify\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use console\jobs\platforms\shopify\{ParseShopifyOrderJob,
+    RegisterShopifyWebhookListenersJob,
+    UnregisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
 use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
@@ -84,7 +86,8 @@ class ShopifyService
         'shop/redact',
     ];
 
-    protected const API_VERSION = '2023-01';
+    public const API_VERSION = '2023-01';
+
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
     protected string $redirectUrl = '/ecommerce-integration/shopify';
@@ -282,6 +285,16 @@ public function addWebhookListenersJob(): void
         );
     }
 
+    public function deleteWebhookListenersJob(): void
+    {
+        Yii::$app->queue->push(
+            new UnregisterShopifyWebhookListenersJob([
+                'shopUrl' => $this->shopUrl,
+                'accessToken' => $this->ecommerceIntegration->array_meta_data['access_token']
+            ])
+        );
+    }
+
     #############
     # Webhooks: #
     #############
diff --git a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
new file mode 100644
index 00000000..f0c998de
--- /dev/null
+++ b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
@@ -0,0 +1,50 @@
+ ShopifyService::API_VERSION,
+            'ShopUrl' => $this->shopUrl,
+            'AccessToken' => $this->accessToken
+        ]);
+
+        $webhooksList = $shopify->Webhook()->get();
+
+        if ($webhooksList) {
+            foreach ($webhooksList as $webhook) {
+                if (isset($webhook['id'])) {
+                    $shopify->Webhook((int)$webhook['id'])->delete();
+                }
+            }
+        }
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}

From 26e2d3eebcb6f84fdb2389152b111718a1347f9f Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 16:10:12 +0200
Subject: [PATCH 26/81] Shopify raw order parsing -> phone number + email

---
 common/models/EcommerceIntegration.php        |  2 +-
 common/models/base/BaseAddress.php            |  1 +
 .../shopify/ParseShopifyOrderJob.php          | 24 ++++++++++++-------
 3 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index ed6c5135..2c8d489c 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -63,7 +63,7 @@ public function disconnect(): bool|int
 
                 $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
                 $shopifyService->deleteWebhookListenersJob();
-                
+
                 break;
         }
 
diff --git a/common/models/base/BaseAddress.php b/common/models/base/BaseAddress.php
index 02788085..35036940 100755
--- a/common/models/base/BaseAddress.php
+++ b/common/models/base/BaseAddress.php
@@ -43,6 +43,7 @@ public function rules()
             [['company', 'name', 'address1', 'address2', 'city'], 'string', 'max' => 64],
             [['zip'], 'string', 'max' => 16],
             [['phone'], 'string', 'max' => 32],
+            [['email'], 'string', 'max' => 255],
             [['notes'], 'string', 'max' => 600],
             [['country'], 'string', 'max' => 2],
             [['country'], 'default', 'value' => 'US'],
diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index da01cbd3..79da77e5 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -51,7 +51,9 @@ public function execute($queue): void
                 $this->parseItemsData();
                 $order = $this->saveOrder();
 
-                EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+                if ($order) {
+                    EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+                }
             } else {
                 EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
             }
@@ -105,14 +107,8 @@ protected function parseOrderData(): void
     protected function parseAddressData(): void
     {
         $notProvided = 'Not provided';
-        $name = null;
-        $address1 = null;
-        $address2 = null;
-        $company = null;
-        $city = null;
-        $phone = null;
+        $name = $address1 = $address2 = $company = $city = $phone = $email = $zip = null;
         $stateId = 0;
-        $zip = null;
         $countryCode = State::DEFAULT_COUNTRY_ABBR;
 
         if (isset($this->rawOrder['shipping_address']['name'])) {
@@ -135,8 +131,17 @@ protected function parseAddressData(): void
             $city = trim($this->rawOrder['shipping_address']['city']);
         }
 
+        if (isset($this->rawOrder['contact_email'])) {
+            $email = trim($this->rawOrder['contact_email']);
+        }
+
+        // Trying to find the phone number:
         if (isset($this->rawOrder['shipping_address']['phone'])) {
             $phone = trim($this->rawOrder['shipping_address']['phone']);
+        } elseif (isset($this->rawOrder['phone']) && !empty($this->rawOrder['phone'])) {
+            $phone = trim($this->rawOrder['phone']);
+        } elseif (isset($this->rawOrder['customer']) && isset($this->rawOrder['customer']['phone']) && !empty($this->rawOrder['customer']['phone'])) {
+            $phone = trim($this->rawOrder['customer']['phone']);
         }
 
         if (isset($this->rawOrder['shipping_address']['zip'])) {
@@ -169,6 +174,7 @@ protected function parseAddressData(): void
             'address2' => $address2,
             'company' => $company,
             'city' => ($city) ?: $notProvided,
+            'email' => $email,
             'phone' => ($phone) ?: $notProvided,
             'state_id' => $stateId,
             'zip' => ($zip) ?: $notProvided,
@@ -199,6 +205,8 @@ protected function saveOrder(): Order|bool
         if ($createOrderService->isValid()) {
             return $createOrderService->create();
         }
+
+        return false;
     }
 
     public function canRetry($attempt, $error): bool

From ab813ca6ced7ad8c868dd605e2210cde61e95aa4 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:05:42 +0200
Subject: [PATCH 27/81] Shopify API -> Fixed issues with its exceptions

---
 common/services/platforms/ShopifyService.php | 67 +++++++++++++++-----
 console/controllers/CronController.php       |  6 +-
 2 files changed, 54 insertions(+), 19 deletions(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 83d38d8e..5619ba22 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -9,7 +9,7 @@
     RegisterShopifyWebhookListenersJob,
     UnregisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
-use PHPShopify\{ShopifySDK, AuthHelper};
+use PHPShopify\{Exception\ApiException, ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
 use PHPShopify\Exception\SdkException;
 
@@ -179,7 +179,7 @@ public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegra
     /**
      * @throws InvalidConfigException
      */
-    protected function isTokenValid()
+    protected function isTokenValid(bool $withException = false): bool
     {
         try {
             $this->getProductsList();
@@ -188,47 +188,80 @@ protected function isTokenValid()
                 $this->ecommerceIntegration->uninstall(true);
             }
 
-            throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+            if ($withException) {
+                throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+            }
+            return false;
         }
+
+        return true;
     }
 
     #####################
     # Get data via API: #
     #####################
 
-    public function getShop(): array
+    public function getShop(): array|bool
     {
-        return $this->shopify->Shop->get();
+        try {
+            return $this->shopify->Shop->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getProductsList(): array
+    public function getProductsList(): array|bool
     {
-        return $this->shopify->Product->get();
+        try {
+            return $this->shopify->Product->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getProductById(int $id): array
+    public function getProductById(int $id): array|bool
     {
-        return $this->shopify->Product($id)->get();
+        try {
+            return $this->shopify->Product($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getOrdersList(): array
+    public function getOrdersList(): array|bool
     {
-        return $this->shopify->Order->get($this->getRequestParamsForOrders());
+        try {
+            return $this->shopify->Order->get($this->getRequestParamsForOrders());
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getOrderById(int $id): array
+    public function getOrderById(int $id): array|bool
     {
-        return $this->shopify->Order($id)->get();
+        try {
+            return $this->shopify->Order($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getCustomerById(int $id): array
+    public function getCustomerById(int $id): array|bool
     {
-        return $this->shopify->Customer($id)->get();
+        try {
+            return $this->shopify->Customer($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getCustomerAddressById(int $customerId, int $addressId): array
+    public function getCustomerAddressById(int $customerId, int $addressId): array|bool
     {
-        return $this->shopify->Customer($customerId)->Address($addressId)->get();
+        try {
+            return $this->shopify->Customer($customerId)->Address($addressId)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
     /**
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index b597207a..337ec5ee 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -106,8 +106,10 @@ protected function runEcommerceIntegrations(): void
                         $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
                         $orders = $shopifyService->getOrdersList();
 
-                        foreach ($orders as $order) {
-                            $shopifyService->parseRawOrderJob($order);
+                        if ($orders) {
+                            foreach ($orders as $order) {
+                                $shopifyService->parseRawOrderJob($order);
+                            }
                         }
                     }
 

From 382a3439dd50905db60228ac28e524ed671f1f51 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:16:55 +0200
Subject: [PATCH 28/81] Shop disconnect fix + Find e-commerce integration by
 meta key

---
 common/models/EcommerceIntegration.php            | 2 +-
 common/models/query/EcommerceIntegrationQuery.php | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index 2c8d489c..5f0a0609 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -58,7 +58,7 @@ public function isUninstalled(): bool
      */
     public function disconnect(): bool|int
     {
-        switch ($this->platform->name) {
+        switch ($this->ecommercePlatform->name) {
             case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
 
                 $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php
index ed04d0aa..ef86af10 100644
--- a/common/models/query/EcommerceIntegrationQuery.php
+++ b/common/models/query/EcommerceIntegrationQuery.php
@@ -4,6 +4,7 @@
 
 use common\models\EcommerceIntegration;
 use yii\db\ActiveQuery;
+use yii\db\Expression;
 
 /**
  * Class EcommerceIntegrationQuery
@@ -16,6 +17,11 @@ public function active(): EcommerceIntegrationQuery
         return $this->andWhere(['status' => EcommerceIntegration::STATUS_INTEGRATION_CONNECTED]);
     }
 
+    public function byMetaKey(string $key, string $value): EcommerceIntegrationQuery
+    {
+        return $this->andWhere(new Expression('`meta` LIKE :find', [':find' => '%"' . $key . '": "'. $value .'"%']));
+    }
+
     public function for(?int $userId = null, ?int $customerId = null): EcommerceIntegrationQuery
     {
         if ($userId) {

From 20c1d8b8681e0548a81d1e0467a5901f8405d95b Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:46:14 +0200
Subject: [PATCH 29/81] UnregisterShopifyWebhookListenersJob

---
 .../UnregisterShopifyWebhookListenersJob.php  |  6 ++-
 .../shopify/ShopifyAppUninstalledJob.php      | 38 ++++++++++++++++++-
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
index f0c998de..6318c5db 100644
--- a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
+++ b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
@@ -27,7 +27,11 @@ public function execute($queue): void
             'AccessToken' => $this->accessToken
         ]);
 
-        $webhooksList = $shopify->Webhook()->get();
+        try {
+            $webhooksList = $shopify->Webhook()->get();
+        } catch (\Exception $e) {
+            $webhooksList = [];
+        }
 
         if ($webhooksList) {
             foreach ($webhooksList as $webhook) {
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index 58054b3e..f2673908 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -2,7 +2,9 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyAppUninstalledJob
@@ -13,7 +15,39 @@ class ShopifyAppUninstalledJob extends BaseWebhookProcessingJob
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " app uninstalled ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->uninstall()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function uninstall(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+        $shopUrl = null;
+
+        if (isset($payload['domain'])) {
+            $shopUrl = trim($payload['domain']);
+        }
+
+        if ($shopUrl) {
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->active()
+                ->byMetaKey('shop_url', $shopUrl)
+                ->one();
+
+            if ($ecommerceIntegration && !$ecommerceIntegration->isUninstalled()) {
+                $ecommerceIntegration->uninstall(true);
+
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 639934a983931c6fd42ffb3305133c70ea7bc569 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:54:25 +0200
Subject: [PATCH 30/81] ShopifyShopRedactJob

---
 .../webhooks/shopify/ShopifyShopRedactJob.php | 33 +++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
index 6b8bf313..557e9d78 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
@@ -2,18 +2,47 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyShopRedactJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#shop-redact
  */
 class ShopifyShopRedactJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " shop redact ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->deleteEcommerceIntegration()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function deleteEcommerceIntegration(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['shop_domain'])) {
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->byMetaKey('shop_url', trim($payload['shop_domain']))
+                ->one();
+
+            if ($ecommerceIntegration) {
+                $ecommerceIntegration->disconnect();
+
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 637300760c21a1bd4a4a52bad1e4f547551f4f3a Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:59:31 +0200
Subject: [PATCH 31/81] ShopifyCustomerRedactJob

---
 .../platforms/webhooks/shopify/ShopifyAppUninstalledJob.php  | 1 +
 .../platforms/webhooks/shopify/ShopifyCustomerRedactJob.php  | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index f2673908..e898b9f7 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -9,6 +9,7 @@
 /**
  * Class ShopifyAppUninstalledJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-app-uninstalled
  */
 class ShopifyAppUninstalledJob extends BaseWebhookProcessingJob
 {
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
index a4d17ac5..b789e836 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
@@ -7,13 +7,16 @@
 /**
  * Class ShopifyCustomerRedactJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#customers-redact
  */
 class ShopifyCustomerRedactJob extends BaseWebhookProcessingJob
 {
+    /**
+     * Since we don't save information (data) about Shopify customers, we skip this mandatory webhook
+     */
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " customer redact ";
         $this->ecommerceWebhook->setSuccess();
     }
 }

From 682e02a347f1ade7fbf2e2e9962e5774ec5987a6 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 18:03:25 +0200
Subject: [PATCH 32/81] ShopifyCustomerDataRequestJob

---
 .../shopify/ShopifyCustomerDataRequestJob.php         | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
index ef994a7a..d36462eb 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
@@ -7,13 +7,22 @@
 /**
  * Class ShopifyCustomerDataRequestJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#customers-data_request
  */
 class ShopifyCustomerDataRequestJob extends BaseWebhookProcessingJob
 {
+    /**
+     * Quote:
+     *
+     * The webhook contains the resource IDs of the customer data that you need to provide to the store owner.
+     * It's your responsibility to provide this data to the store owner directly.
+     * In some cases, a customer record contains only the customer's email address.
+     *
+     * It means that it's enough just to mark the webhook as `success` since the `payload` field consists of the needed resource IDs.
+     */
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " customer data request ";
         $this->ecommerceWebhook->setSuccess();
     }
 }

From 370bb611ef6fe19c11211068df33bb128a01866f Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 19:02:28 +0200
Subject: [PATCH 33/81] ShopifyOrderCancelledJob

---
 .../shopify/ShopifyOrderCancelledJob.php      | 29 +++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index 303e79b0..ceb8ed3a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -2,18 +2,43 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\Status;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderCancelledJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-cancelled
  */
 class ShopifyOrderCancelledJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " cancelled ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->cancel()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function cancel(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $externalOrderId = (int)$payload['id'];
+            $internalOrder = $this->getOrderByExternalId($externalOrderId);
+
+            if ($internalOrder) {
+                $internalOrder->status_id = Status::CANCELLED;
+                $internalOrder->save();
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 38f752e77c10a949cbb27d7e36b51a5c5917b79e Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 19:11:03 +0200
Subject: [PATCH 34/81] ShopifyOrderDeletedJob

---
 .../shopify/ShopifyOrderCancelledJob.php      |  3 +-
 .../shopify/ShopifyOrderDeletedJob.php        | 30 +++++++++++++++++--
 2 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index ceb8ed3a..19e42926 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -29,8 +29,7 @@ protected function cancel(): bool
         $payload = Json::decode($this->ecommerceWebhook->payload);
 
         if (isset($payload['id'])) {
-            $externalOrderId = (int)$payload['id'];
-            $internalOrder = $this->getOrderByExternalId($externalOrderId);
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
 
             if ($internalOrder) {
                 $internalOrder->status_id = Status::CANCELLED;
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
index 55c56925..59438502 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
@@ -3,17 +3,43 @@
 namespace console\jobs\platforms\webhooks\shopify;
 
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderDeletedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-delete
  */
 class ShopifyOrderDeletedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " deleted ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->delete()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    /**
+     * @throws \yii\db\StaleObjectException
+     * @throws \Throwable
+     */
+    protected function delete(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+            if ($internalOrder) {
+                $internalOrder->delete();
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 52babfb2859af27c221013523a9ae2bad4871a65 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 14:56:22 +0200
Subject: [PATCH 35/81] ShopifyOrderUpdatedJob

---
 .../shopify/ParseShopifyOrderJob.php          |   2 +-
 .../shopify/ShopifyOrderUpdatedJob.php        | 160 +++++++++++++++++-
 2 files changed, 159 insertions(+), 3 deletions(-)

diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 79da77e5..861f9c20 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -99,7 +99,7 @@ protected function parseOrderData(): void
             'uuid' => (string)$this->rawOrder['id'],
             'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
             'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'notes' => $this->rawOrder['tags'],
+            'notes' => trim($this->rawOrder['tags']),
             'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
         ];
     }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index 1748465a..086e982c 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -2,18 +2,174 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\{Address, Country, Item, Order, State};
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderUpdatedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-updated
  */
 class ShopifyOrderUpdatedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " updated ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->update()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function update(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+            if ($internalOrder) {
+                $this->updateOrder($internalOrder, $payload);
+                $this->updateAddress($internalOrder->address, $payload);
+                $this->updateItems($internalOrder, $payload);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    protected function updateOrder(Order $internalOrder, array $payload): void
+    {
+        $internalOrder->notes = trim($payload['tags']);
+        $internalOrder->save();
+    }
+
+    protected function updateAddress(Address $internalAddress, array $payload): void
+    {
+        $notProvided = 'Not provided';
+        $name = trim($payload['shipping_address']['name']);
+        $address1 = trim($payload['shipping_address']['address1']);
+        $address2 = trim($payload['shipping_address']['address2']);
+        $company = trim($payload['shipping_address']['company']);
+        $city = trim($payload['shipping_address']['city']);
+        $email = trim($payload['contact_email']);
+        $zip = trim($payload['shipping_address']['zip']);
+
+        $internalAddress->name = ($name) ?: $notProvided;
+        $internalAddress->address1 = ($address1) ?: $notProvided;
+        $internalAddress->address2 = ($address2) ?: null;
+        $internalAddress->company = ($company) ?: null;
+        $internalAddress->city = ($city) ?: $notProvided;
+        $internalAddress->email = ($email) ?: null;
+        $internalAddress->phone = ($this->getPhoneNumberFromPayload($payload)) ?: $notProvided;
+        $internalAddress->zip = ($zip) ?: $notProvided;
+        $internalAddress->state_id = $this->getStateIdFromPayload($payload);
+        $internalAddress->country = $this->getCountryCodeFromPayload($payload);
+        $internalAddress->save();
+    }
+
+    protected function updateItems(Order $internalOrder, array $payload): void
+    {
+        $internalItems = $internalOrder->items;
+        $internalUuids = $externalUuids = [];
+
+        // Collect external item UUIDs:
+        if (isset($payload['line_items'])) {
+            foreach ($payload['line_items'] as $externalItem) {
+                $externalUuids[] = (string)$externalItem['id'];
+            }
+        }
+
+        // Collect internal item UUIDs:
+        foreach ($internalItems as $internalItem) {
+            $internalUuids[] = (string)$internalItem->uuid;
+        }
+
+        // Remove internal Items that aren't presented in the `line_items`:
+        foreach ($internalItems as $internalItem) {
+            if (!in_array($internalItem->uuid, $externalUuids)) {
+                $internalItem->delete();
+            }
+        }
+
+        // Add or update:
+        foreach ($payload['line_items'] as $externalItem) {
+            $externalItemUuid = (string)$externalItem['id'];
+
+            $attributes = [
+                'order_id' => $internalOrder->id,
+                'quantity' => $externalItem['fulfillable_quantity'],
+                'sku' => ($externalItem['sku']) ?: 'Not provided',
+                'name' => $externalItem['name'],
+                'uuid' => $externalItemUuid,
+            ];
+
+            if (!in_array($externalItemUuid, $internalUuids)) { // Add:
+                $orderItem = new Item();
+                $orderItem->setAttributes($attributes);
+                $orderItem->save();
+            } else { // Update:
+                foreach ($internalItems as $internalItem) {
+                    if ($internalItem->uuid == $externalItemUuid) {
+                        $internalItem->setAttributes($attributes);
+                        $internalItem->save();
+                    }
+                }
+            }
+        }
+    }
+
+    protected function getPhoneNumberFromPayload(array $payload): ?string
+    {
+        $phone = null;
+
+        if (isset($payload['shipping_address']['phone'])) {
+            $phone = trim($payload['shipping_address']['phone']);
+        } elseif (isset($payload['phone']) && !empty($payload['phone'])) {
+            $phone = trim($payload['phone']);
+        } elseif (isset($payload['customer']) && isset($payload['customer']['phone']) && !empty($payload['customer']['phone'])) {
+            $phone = trim($payload['customer']['phone']);
+        }
+
+        return $phone;
+    }
+
+    protected function getStateIdFromPayload(array $payload): int
+    {
+        $stateId = 0;
+
+        if (isset($payload['shipping_address']['province_code'])) {
+            $state = State::find()->where([
+                'abbreviation' => trim($payload['shipping_address']['province_code'])
+            ])->one();
+
+            if ($state) {
+                $stateId = (int)$state->id;
+            }
+        }
+
+        return $stateId;
+    }
+
+    protected function getCountryCodeFromPayload(array $payload): string
+    {
+        $countryCode = State::DEFAULT_COUNTRY_ABBR;
+
+        if (isset($payload['shipping_address']['country_code'])) {
+            $country = Country::find()->where([
+                'abbreviation' => trim($payload['shipping_address']['country_code'])
+            ])->one();
+
+            if ($country) {
+                $countryCode = $country->abbreviation;
+            }
+        }
+
+        return $countryCode;
     }
 }

From 8c296b9ed929461b6786d06a090651f441e92eb3 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 15:50:31 +0200
Subject: [PATCH 36/81] ShopifyOrderCreatedJob

---
 .../shopify/ShopifyOrderCreatedJob.php        | 56 ++++++++++++++++++-
 1 file changed, 54 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
index 92bb64bc..0b1744e1 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
@@ -2,18 +2,70 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use Yii;
+use yii\helpers\Json;
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use console\jobs\platforms\shopify\ParseShopifyOrderJob;
 
 /**
  * Class ShopifyOrderCreatedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-create
  */
 class ShopifyOrderCreatedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " created ";
-        $this->ecommerceWebhook->setSuccess();
+
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+        $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+        if ($internalOrder) {
+            // If we already have an internal order with such UUID, we just do nothing:
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $ecommerceIntegration = $this->getEcommerceIntegration($payload);
+
+            if ($ecommerceIntegration && $this->create($payload, $ecommerceIntegration)) {
+                $this->ecommerceWebhook->setSuccess();
+            } else {
+                $this->ecommerceWebhook->setFailed();
+            }
+        }
+    }
+
+    /**
+     * For parsing raw Shopify orders, we have a separate Job:
+     */
+    protected function create(array $payload, EcommerceIntegration $ecommerceIntegration): bool
+    {
+        Yii::$app->queue->push(
+            new ParseShopifyOrderJob([
+                'rawOrder' => $payload,
+                'ecommerceIntegrationId' => $ecommerceIntegration->id
+            ])
+        );
+
+        return true;
+    }
+
+    protected function getEcommerceIntegration(array $payload): EcommerceIntegration|bool
+    {
+        if (isset($payload['order_status_url']) && !empty($payload['order_status_url'])) {
+            $domain = parse_url($payload['order_status_url'], PHP_URL_HOST);
+
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->byMetaKey('shop_url', $domain)
+                ->one();
+
+            return ($ecommerceIntegration) ?: false;
+        }
+
+        return false;
     }
 }

From ba90391afd62c58fc3e2eae5c35d10525d37c076 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 15:57:14 +0200
Subject: [PATCH 37/81] Order Notes

---
 console/jobs/platforms/shopify/ParseShopifyOrderJob.php         | 2 +-
 .../jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 861f9c20..c8f341b5 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -99,7 +99,7 @@ protected function parseOrderData(): void
             'uuid' => (string)$this->rawOrder['id'],
             'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
             'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'notes' => trim($this->rawOrder['tags']),
+            'notes' => trim($this->rawOrder['note']),
             'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
         ];
     }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index 086e982c..a6740117 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -45,7 +45,7 @@ protected function update(): bool
 
     protected function updateOrder(Order $internalOrder, array $payload): void
     {
-        $internalOrder->notes = trim($payload['tags']);
+        $internalOrder->notes = trim($payload['note']);
         $internalOrder->save();
     }
 

From b0b98ff51a66622ab720106e6728ec0e6405dfa3 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 16 Mar 2023 18:34:47 +0200
Subject: [PATCH 38/81] Updated docs + removed test code

---
 .../EcommerceWebhookController.php            | 20 -------------------
 intro-docs/ecommerce-platfroms.md             | 11 +++++++---
 2 files changed, 8 insertions(+), 23 deletions(-)

diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 4b870b3c..b12e93ab 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -31,9 +31,6 @@ public function beforeAction($action): bool
      */
     public function actionShopify(): array
     {
-        // file_put_contents('shopify.txt', "\r\n\r\n\r\n" . Yii::$app->request->rawBody, FILE_APPEND);
-
-
         $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
@@ -88,21 +85,4 @@ protected function getNewEcommerceWebhookObject(int $ecommercePlatformId): Ecomm
 
         return $ecommerceWebhook;
     }
-
-    public function actionTest()
-    {
-        $ecommerceIntegrations = EcommerceIntegration::find()
-            ->active()
-            ->orderById()
-            ->all();
-
-        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
-            $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
-
-            if ($accessToken) {
-                $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
-                return $shopifyService->getWebhooksList();
-            }
-        }
-    }
 }
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index bbcb0676..428464c0 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -9,20 +9,24 @@
 3. Add implementation to:
 - `common\models\EcommercePlatform`
 - `common\models\EcommerceIntegration`
+- `common\models\EcommerceWebhook`
 - `frontend\controllers\EcommercePlatformController`
 - `frontend\controllers\EcommerceIntegrationController`
 - `frontend\controllers\EcommerceWebhookController`
 
 4. Create a Service similar to `common\services\platforms\ShopifyService`.
 
-5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
+5. Create Jobs similar to `console\jobs\platforms\*`.
 
 > php yii queue/listen --verbose
 
 ### Cron:
 
 1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
-We need this method to pull existing orders from a needed E-commerce platform.
+We need this method to pull initial existing orders from a needed E-commerce platform.
+   
+2. See `console\controllers\CronController.php` -> `runEcommerceWebhooks()`.
+We need this method to process received webhooks (`status=received`).
    
 > php yii cron/frequent
 
@@ -74,7 +78,8 @@ If you're going to **test it locally**, specify:
 4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain`
 to `https://shipwise.ngrok.io`.
    
-5. You need to request `Protected customer data access`. Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` ->
+5. You need to request `Protected customer data access` for data like shipping address. 
+Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` ->
 Press the button `Request access`. On the page, select and request access for:
    
 - `Protected customer data`

From aa84b5cead99c4d6feb79e75ab0ba374bad4ba7b Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Sat, 25 Feb 2023 21:12:00 +0200
Subject: [PATCH 39/81] Shopify OAuth implementation

---
 common/config/params.php                      |  16 +++
 .../platforms/ConnectShopifyStoreForm.php     | 100 ++++++++++++++++++
 common/services/platforms/ShopifyService.php  |  96 +++++++++++++++++
 composer.json                                 |   3 +-
 composer.lock                                 |  53 +++++++++-
 .../EcommerceIntegrationController.php        |  48 ++++++++-
 .../views/ecommerce-integration/index.php     |  17 ++-
 .../views/ecommerce-integration/shopify.php   |  58 ++++++++++
 frontend/web/css/site.css                     |   8 ++
 9 files changed, 388 insertions(+), 11 deletions(-)
 create mode 100644 common/models/forms/platforms/ConnectShopifyStoreForm.php
 create mode 100644 common/services/platforms/ShopifyService.php
 create mode 100644 frontend/views/ecommerce-integration/shopify.php

diff --git a/common/config/params.php b/common/config/params.php
index 496ad838..22038d9b 100755
--- a/common/config/params.php
+++ b/common/config/params.php
@@ -63,4 +63,20 @@
     'csvBoxS3Bucket' => '',
     'csvBoxS3Path' => '',
     'csvBoxImportKey' => '',
+
+    /**
+     * Shopify
+     */
+    'shopify' => [
+        /**
+         * Visit https://partners.shopify.com/ -> Apps -> Your App -> Overview -> Client credentials
+         */
+        'client_id' => '',
+        'client_secret' => '',
+        /**
+         * You can set `https://shipwise.ngrok.io` to test it locally.
+         * Otherwise, if leave the default value, the current server's domain name will be used automatically.
+         */
+        'override_redirect_domain' => false,
+    ],
 ];
diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
new file mode 100644
index 00000000..3a0dad45
--- /dev/null
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -0,0 +1,100 @@
+ ['name', 'url'],
+            self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'],
+        ];
+    }
+
+    public function rules(): array
+    {
+        return [
+            [['name', 'url', 'code'], 'filter', 'filter' => 'trim'],
+            [['name', 'url', 'code'], 'string', 'max' => 128],
+            /** @see https://www.regextester.com/104785 */
+            ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'],
+
+            [['name', 'url'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST],
+            [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST],
+            [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST],
+
+            [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN],
+        ];
+    }
+
+    public function attributeLabels(): array
+    {
+        return [
+            'name' => 'Shop Name',
+            'url' => 'Shop URL',
+            'code' => 'Code',
+        ];
+    }
+
+    public function validateShopName(): void
+    {
+        if (EcommerceIntegration::find()
+            ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%']))
+            ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id])
+            ->exists())
+        {
+            $this->addError('name', 'Shop name already exists.');
+        }
+    }
+
+    public function validateShopUrl(): void
+    {
+        if (EcommerceIntegration::find()
+            ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%']))
+            ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id])
+            ->exists())
+        {
+            $this->addError('url', 'Shop URL already exists.');
+        }
+    }
+
+    public function auth(): void
+    {
+        $this->saveShopName();
+
+        // Step 1 - Send request to receive access token
+        $shopifyService = new ShopifyService($this->url);
+        $shopifyService->auth();
+    }
+
+    /**
+     * @throws ServerErrorHttpException
+     */
+    public function saveAccessToken(): void
+    {
+        // Step 2 - Receive and save access token:
+        $shopifyService = new ShopifyService($this->url);
+        $shopifyService->accessToken(Yii::$app->session->get('shop_name', 'Shop Name'));
+    }
+
+    protected function saveShopName()
+    {
+        Yii::$app->session->set('shop_name', $this->name);
+    }
+}
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
new file mode 100644
index 00000000..7d944889
--- /dev/null
+++ b/common/services/platforms/ShopifyService.php
@@ -0,0 +1,96 @@
+shopUrl = $shopUrl;
+        $this->token = $token;
+
+        if (!$this->token) { // Authorize user's shop
+            $config = [
+                'ShopUrl' => $this->shopUrl,
+                'ApiKey' => Yii::$app->params['shopify']['client_id'],
+                'SharedSecret' => Yii::$app->params['shopify']['client_secret'],
+            ];
+        } else { // Use existing user's shop
+            $config = [
+                'ShopUrl' => $this->shopUrl,
+                'AccessToken' => $this->token,
+            ];
+        }
+
+        $this->shopify = new ShopifySDK($config);
+    }
+
+    public function auth(): void
+    {
+        $redirectDomain = trim(Url::to(['/'], true), '/');
+
+        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
+            $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain'];
+        }
+
+        // Step 1 - Send request to receive access token:
+        AuthHelper::createAuthRequest($this->scopes, $redirectDomain . $this->redirectUrl);
+        exit;
+    }
+
+    public function accessToken(string $shopName)
+    {
+        $accessToken = AuthHelper::createAuthRequest($this->scopes);
+
+        $meta = [
+            'platform' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'shop_url' => $this->shopUrl,
+            'shop_name' => $shopName,
+            'access_token' => $accessToken,
+        ];
+
+        $ecommerceIntegration = new EcommerceIntegration();
+        $ecommerceIntegration->user_id = Yii::$app->user->id;
+        $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id;
+        $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED;
+        $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT);
+
+        if (!$ecommerceIntegration->save()) {
+            throw new ServerErrorHttpException('Shopify integration is not added. Something went wrong.');
+        }
+    }
+
+    public function makeReq()
+    {
+        echo '
';
+        print_r($this->shopify->Product->get());
+        exit;
+    }
+}
diff --git a/composer.json b/composer.json
index 99e72293..fe11121a 100755
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,8 @@
         "2amigos/2fa-library": "^2.0",
         "2amigos/qrcode-library": "^2.0",
         "frostealth/yii2-aws-s3": "~2.0",
-        "league/csv": "^9.8"
+        "league/csv": "^9.8",
+        "phpclassic/php-shopify": "^1.2"
     },
     "require-dev": {
         "yiisoft/yii2-debug": "~2.1.0",
diff --git a/composer.lock b/composer.lock
index 58928c02..c4484637 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "1367a46f8214849610c956ec18d57da1",
+    "content-hash": "4a8bcf4bc3f9f7b9dce6864dd222eee9",
     "packages": [
         {
             "name": "2amigos/2fa-library",
@@ -2649,6 +2649,57 @@
             },
             "time": "2020-07-07T09:29:14+00:00"
         },
+        {
+            "name": "phpclassic/php-shopify",
+            "version": "v1.2.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpclassic/php-shopify.git",
+                "reference": "1d05bc9c662b01e804b12ab6049cf707a43d4285"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpclassic/php-shopify/zipball/1d05bc9c662b01e804b12ab6049cf707a43d4285",
+                "reference": "1d05bc9c662b01e804b12ab6049cf707a43d4285",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPShopify\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Tareq Mahmood",
+                    "email": "tareqtms@yahoo.com"
+                }
+            ],
+            "description": "PHP SDK for Shopify API",
+            "homepage": "https://github.com/phpclassic/php-shopify",
+            "keywords": [
+                "php",
+                "sdk",
+                "shopify"
+            ],
+            "support": {
+                "issues": "https://github.com/phpclassic/php-shopify/issues",
+                "source": "https://github.com/phpclassic/php-shopify/tree/v1.2.5"
+            },
+            "time": "2023-02-13T03:51:33+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "3.0.0",
diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index 3b501323..cc71544b 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -3,13 +3,13 @@
 namespace frontend\controllers;
 
 use Yii;
+use yii\helpers\Url;
 use yii\web\Response;
-use common\models\EcommercePlatform;
 use yii\filters\AccessControl;
-use yii\web\NotFoundHttpException;
 use Da\User\Filter\AccessRuleFilter;
-use common\models\EcommerceIntegration;
-use yii\web\ServerErrorHttpException;
+use common\models\{EcommercePlatform, EcommerceIntegration};
+use common\models\forms\platforms\ConnectShopifyStoreForm;
+use yii\web\{NotFoundHttpException, ServerErrorHttpException};
 
 class EcommerceIntegrationController extends Controller
 {
@@ -34,6 +34,46 @@ public function behaviors(): array
         ];
     }
 
+    /**
+     * Connects Shopify shop.
+     * @return string|Response
+     * @throws ServerErrorHttpException
+     */
+    public function actionShopify(): string|Response
+    {
+        if (Yii::$app->request->isPost) {
+            // Step 1 - Send request to receive access token:
+            $model = new ConnectShopifyStoreForm([
+                'scenario' => ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST
+            ]);
+            $model->load(Yii::$app->request->post());
+
+            if ($model->validate()) {
+                $model->auth();
+            }
+        } elseif (Yii::$app->request->get('code')) {
+            // Step 2 - Receive and save access token:
+            $model = new ConnectShopifyStoreForm([
+                'scenario' => ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN,
+                'url' => Yii::$app->request->get('shop'),
+                'code' => Yii::$app->request->get('code')
+            ]);
+
+            if ($model->validate()) {
+                $model->saveAccessToken();
+
+                Yii::$app->session->setFlash('success', 'Shopify shop has been connected.');
+                return $this->redirect(['index']);
+            }
+        } else {
+            $model = new ConnectShopifyStoreForm();
+        }
+
+        return $this->render('shopify', [
+            'model' => $model,
+        ]);
+    }
+
     /**
      * Lists all EcommercePlatform models with their EcommerceIntegration for the current user.
      * @return string
diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php
index 6ebda07b..c2886e78 100644
--- a/frontend/views/ecommerce-integration/index.php
+++ b/frontend/views/ecommerce-integration/index.php
@@ -1,6 +1,7 @@
 
     
 
-    
-        render('_platform', [
-            'model' => $model,
-        ]) ?>
-    
+    
+
+    
+ + render('_platform', [ + 'model' => $model, + ]) ?> + +
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php new file mode 100644 index 00000000..e5293e29 --- /dev/null +++ b/frontend/views/ecommerce-integration/shopify.php @@ -0,0 +1,58 @@ +title = $title . ' - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Integrations', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $title; +?> + +
+

+ +

+ +
+
+ Please input your Shopify shop name and its URL without http(s). + Example: myshop.myshopify.com. +
+ +
+
+ field($model, 'name') + ->textInput([ + 'class' => 'form-control', + 'placeholder' => $model->getAttributeLabel('name') . '...', + 'required' => true, + 'autofocus' => true, + 'maxlength' => true + ]) ?> +
+
+ field($model, 'url') + ->textInput([ + 'class' => 'form-control', + 'placeholder' => $model->getAttributeLabel('url') . '...', + 'required' => true, + 'maxlength' => true + ]) ?> +
+
+ 'btn btn-success', + ]) ?> +
+
+ +
+
diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 48a16aa3..499177ee 100755 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -383,3 +383,11 @@ pre { .form-actions { margin: 0 0 15px 0 !important; } + +.mb-2 { + margin-bottom: 20px; +} + +.mt-2 { + margin-top: 20px; +} From 44db9c9a31430dd38f24addbdb8e5b62cf2937c7 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 17:15:35 +0200 Subject: [PATCH 40/81] Some changes in the logic + actions {disconnect/pause/resume} --- common/models/EcommerceIntegration.php | 40 +++++- common/models/EcommercePlatform.php | 5 + .../models/base/BaseEcommerceIntegration.php | 11 +- common/models/base/BaseEcommercePlatform.php | 9 -- .../platforms/ConnectShopifyStoreForm.php | 15 ++- .../query/EcommerceIntegrationQuery.php | 31 +++++ .../models/query/EcommercePlatformQuery.php | 42 ------ common/services/platforms/ShopifyService.php | 11 +- .../EcommerceIntegrationController.php | 125 ++++++++---------- .../views/ecommerce-integration/_platform.php | 124 ++--------------- .../views/ecommerce-integration/_shopify.php | 107 +++++++++++++++ .../views/ecommerce-integration/index.php | 23 +++- .../views/ecommerce-integration/shopify.php | 3 +- frontend/views/ecommerce-platform/index.php | 2 +- frontend/views/ecommerce-platform/update.php | 4 +- frontend/views/ecommerce-platform/view.php | 4 +- frontend/views/layouts/main.php | 2 +- 17 files changed, 295 insertions(+), 263 deletions(-) create mode 100644 common/models/query/EcommerceIntegrationQuery.php delete mode 100644 common/models/query/EcommercePlatformQuery.php create mode 100644 frontend/views/ecommerce-integration/_shopify.php diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index bb38d63d..a066ffc4 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -2,6 +2,7 @@ namespace common\models; +use yii\helpers\Json; use common\models\base\BaseEcommerceIntegration; /** @@ -10,14 +11,20 @@ */ class EcommerceIntegration extends BaseEcommerceIntegration { - public const STATUS_INTEGRATION_DISCONNECTED = 0; public const STATUS_INTEGRATION_CONNECTED = 1; - public const STATUS_INTEGRATION_PAUSED = -1; + public const STATUS_INTEGRATION_PAUSED = 0; + + public array $array_meta_data = []; + + public function init(): void + { + parent::init(); + $this->on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']); + } public static function getStatuses(): array { return [ - self::STATUS_INTEGRATION_DISCONNECTED => 'Disconnected', self::STATUS_INTEGRATION_CONNECTED => 'Connected', self::STATUS_INTEGRATION_PAUSED => 'Paused', ]; @@ -28,13 +35,32 @@ public function isConnected(): bool return $this->status === self::STATUS_INTEGRATION_CONNECTED; } - public function isDisconnected(): bool + public function isPaused(): bool { - return $this->status === self::STATUS_INTEGRATION_DISCONNECTED; + return $this->status === self::STATUS_INTEGRATION_PAUSED; } - public function isPaused(): bool + public function disconnect(): bool|int { - return $this->status === self::STATUS_INTEGRATION_PAUSED; + return $this->delete(); + } + + public function pause(): bool + { + $this->status = self::STATUS_INTEGRATION_PAUSED; + return $this->save(); + } + + public function resume(): bool + { + $this->status = self::STATUS_INTEGRATION_CONNECTED; + return $this->save(); + } + + protected function convertMetaData(): void + { + if ($this->meta) { + $this->array_meta_data = Json::decode($this->meta); + } } } diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php index d1b0056f..ec5ba303 100644 --- a/common/models/EcommercePlatform.php +++ b/common/models/EcommercePlatform.php @@ -47,4 +47,9 @@ public function getConnectedUsersCounter(): int { return 0; } + + public static function getShopifyObject(): ?EcommercePlatform + { + return self::findOne(['name' => self::SHOPIFY_PLATFORM_NAME]); + } } diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php index 7087fa75..6e5ad507 100644 --- a/common/models/base/BaseEcommerceIntegration.php +++ b/common/models/base/BaseEcommerceIntegration.php @@ -4,6 +4,7 @@ use yii\db\ActiveRecord; use yii\db\ActiveQuery; +use common\models\query\EcommerceIntegrationQuery; use common\models\EcommercePlatform; use common\models\Customer; use frontend\models\User; @@ -26,6 +27,14 @@ */ class BaseEcommerceIntegration extends ActiveRecord { + /** + * @return EcommerceIntegrationQuery + */ + public static function find(): EcommerceIntegrationQuery + { + return new EcommerceIntegrationQuery(get_called_class()); + } + /** * {@inheritdoc} */ @@ -79,7 +88,7 @@ public function getCustomer(): ActiveQuery /** * @return ActiveQuery */ - public function getPlatform(): ActiveQuery + public function getEcommercePlatform(): ActiveQuery { return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']); } diff --git a/common/models/base/BaseEcommercePlatform.php b/common/models/base/BaseEcommercePlatform.php index 10bce9cf..559753df 100644 --- a/common/models/base/BaseEcommercePlatform.php +++ b/common/models/base/BaseEcommercePlatform.php @@ -3,7 +3,6 @@ namespace common\models\base; use yii\db\ActiveRecord; -use common\models\query\EcommercePlatformQuery; /** * This is the model class for table "ecommerce_platform". @@ -17,14 +16,6 @@ */ class BaseEcommercePlatform extends ActiveRecord { - /** - * @return EcommercePlatformQuery - */ - public static function find(): EcommercePlatformQuery - { - return new EcommercePlatformQuery(get_called_class()); - } - /** * {@inheritdoc} */ diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index 3a0dad45..efc8b81c 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -5,11 +5,16 @@ use Yii; use yii\base\Model; use yii\db\Expression; +use PHPShopify\Exception\SdkException; +use yii\web\ServerErrorHttpException; use common\services\platforms\ShopifyService; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; -use yii\web\ServerErrorHttpException; +/** + * Class ConnectShopifyStoreForm + * @package common\models\forms\platforms + */ class ConnectShopifyStoreForm extends Model { public const SCENARIO_AUTH_REQUEST = 'scenarioAuthRequest'; @@ -56,7 +61,7 @@ public function validateShopName(): void { if (EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) ->exists()) { $this->addError('name', 'Shop name already exists.'); @@ -67,13 +72,16 @@ public function validateShopUrl(): void { if (EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id]) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) ->exists()) { $this->addError('url', 'Shop URL already exists.'); } } + /** + * @throws SdkException + */ public function auth(): void { $this->saveShopName(); @@ -85,6 +93,7 @@ public function auth(): void /** * @throws ServerErrorHttpException + * @throws SdkException */ public function saveAccessToken(): void { diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php new file mode 100644 index 00000000..8b905414 --- /dev/null +++ b/common/models/query/EcommerceIntegrationQuery.php @@ -0,0 +1,31 @@ +andWhere(['user_id' => $userId]); + } + + if ($customerId) { + $this->andWhere(['customer_id' => $customerId]); + } + + return $this; + } + + public function orderById(int $sort = SORT_ASC): EcommerceIntegrationQuery + { + return $this + ->orderBy(['id' => $sort]); + } +} diff --git a/common/models/query/EcommercePlatformQuery.php b/common/models/query/EcommercePlatformQuery.php deleted file mode 100644 index db7d97fb..00000000 --- a/common/models/query/EcommercePlatformQuery.php +++ /dev/null @@ -1,42 +0,0 @@ -andWhere(['status' => EcommercePlatform::STATUS_PLATFORM_ACTIVE]); - } - - public function for(?int $userId = null, ?int $customerId = null): EcommercePlatformQuery - { - $this->joinWith([ - 'ecommerceIntegration' => function ($query) use ($userId, $customerId) { - if ($userId) { - $query->onCondition(['ecommerce_integration.user_id' => $userId]); - } - - if ($customerId) { - $query->onCondition(['ecommerce_integration.customer_id' => $customerId]); - } - } - ]); - - return $this; - } - - public function orderById(int $sort = SORT_ASC): EcommercePlatformQuery - { - return $this - ->orderBy(['id' => $sort]); - } -} diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 7d944889..181e78d8 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -10,6 +10,7 @@ use yii\helpers\Json; use yii\helpers\Url; use yii\web\ServerErrorHttpException; +use PHPShopify\Exception\SdkException; /** * Class ShopifyService @@ -52,6 +53,9 @@ public function __construct(string $shopUrl, string $token = null) $this->shopify = new ShopifySDK($config); } + /** + * @throws SdkException + */ public function auth(): void { $redirectDomain = trim(Url::to(['/'], true), '/'); @@ -65,8 +69,13 @@ public function auth(): void exit; } - public function accessToken(string $shopName) + /** + * @throws SdkException + * @throws ServerErrorHttpException + */ + public function accessToken(string $shopName): void { + // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); $meta = [ diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index cc71544b..a92250a1 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -3,10 +3,10 @@ namespace frontend\controllers; use Yii; -use yii\helpers\Url; use yii\web\Response; use yii\filters\AccessControl; use Da\User\Filter\AccessRuleFilter; +use PHPShopify\Exception\SdkException; use common\models\{EcommercePlatform, EcommerceIntegration}; use common\models\forms\platforms\ConnectShopifyStoreForm; use yii\web\{NotFoundHttpException, ServerErrorHttpException}; @@ -35,12 +35,32 @@ public function behaviors(): array } /** - * Connects Shopify shop. - * @return string|Response + * Lists all EcommerceIntegration models for the current user. + * @return string + */ + public function actionIndex(): string + { + $ecommerceIntegrations = EcommerceIntegration::find() + ->with(['ecommercePlatform']) + ->for(Yii::$app->user->id) + ->orderById() + ->all(); + + return $this->render('index', [ + 'models' => $ecommerceIntegrations, + ]); + } + + /** + * Connects a new Shopify shop. + * @throws SdkException + * @throws NotFoundHttpException * @throws ServerErrorHttpException */ public function actionShopify(): string|Response { + $this->checkEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME); + if (Yii::$app->request->isPost) { // Step 1 - Send request to receive access token: $model = new ConnectShopifyStoreForm([ @@ -75,118 +95,79 @@ public function actionShopify(): string|Response } /** - * Lists all EcommercePlatform models with their EcommerceIntegration for the current user. - * @return string - */ - public function actionIndex(): string - { - $ecommercePlatforms = EcommercePlatform::find() - ->with(['ecommerceIntegration']) - ->for(Yii::$app->user->id) - ->orderById() - ->all(); - - return $this->render('index', [ - 'models' => $ecommercePlatforms, - ]); - } - - /** + * Disconnects a needed EcommerceIntegration model. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionConnect(): Response + public function actionDisconnect(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - - if (!$ecommercePlatform->ecommerceIntegration) { - $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = Yii::$app->user->id; - /** - * TODO: implement adding `customer_id` - */ - //$ecommerceIntegration->customer_id = 1; - $ecommerceIntegration->platform_id = $ecommercePlatform->id; - $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; - - if (!$ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); - } - } else { - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->disconnect(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not connected. Something went wrong.'); - } - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been disconnected.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been connected.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Makes a needed EcommerceIntegration model paused. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionDisconnect(): Response + public function actionPause(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->pause(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not disconnected. Something went wrong.'); - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been paused.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been disconnected.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Makes a needed EcommerceIntegration model active again. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionPause(): Response + public function actionResume(int $id): Response { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_PAUSED; + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $ecommerceIntegration->resume(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not paused. Something went wrong.'); - } + Yii::$app->session->setFlash('success', 'Ecommerce platform has been resumed.'); - Yii::$app->session->setFlash('success', 'Ecommerce platform has been paused.'); return $this->redirect(Yii::$app->request->referrer); } /** + * Returns a needed EcommerceIntegration model by its ID. * @throws NotFoundHttpException - * @throws ServerErrorHttpException */ - public function actionResume(): Response + protected function getEcommerceIntegrationById(int $id): EcommerceIntegration { - $ecommercePlatform = $this->getEcommercePlatformByName(Yii::$app->request->get('platform')); - $ecommercePlatform->ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; + $model = EcommerceIntegration::find() + ->for(Yii::$app->user->id) + ->where(['id' => $id]) + ->one(); - if (!$ecommercePlatform->ecommerceIntegration->save()) { - throw new ServerErrorHttpException('Ecommerce platform is not resumed. Something went wrong.'); + if (!$model) { + throw new NotFoundHttpException('Ecommerce integration does not exist.'); } - Yii::$app->session->setFlash('success', 'Ecommerce platform has been resumed.'); - return $this->redirect(Yii::$app->request->referrer); + /** + * @var $model EcommerceIntegration + */ + return $model; } /** + * Checks a needed EcommercePlatform model by the provided name. It must exist and be active. * @throws NotFoundHttpException * @throws ServerErrorHttpException */ - protected function getEcommercePlatformByName(string $name): EcommercePlatform + protected function checkEcommercePlatformByName(string $name): void { /** * @var EcommercePlatform $model */ $model = EcommercePlatform::find() - ->with(['ecommerceIntegration']) - ->for(Yii::$app->user->id) ->where(['name' => $name]) ->one(); @@ -197,7 +178,5 @@ protected function getEcommercePlatformByName(string $name): EcommercePlatform if (!$model->isActive()) { throw new ServerErrorHttpException('Ecommerce platform is not active.'); } - - return $model; } } diff --git a/frontend/views/ecommerce-integration/_platform.php b/frontend/views/ecommerce-integration/_platform.php index 04ee4d7b..997573bd 100644 --- a/frontend/views/ecommerce-integration/_platform.php +++ b/frontend/views/ecommerce-integration/_platform.php @@ -1,119 +1,19 @@ - $model, - 'options' => [ - 'id' => $model->id, - 'class' => 'table table-striped table-bordered detail-view' - ], - 'attributes' => [ - [ - 'label' => 'Platform:', - 'attribute' => 'name', - 'format' => 'raw', - 'value' => function($model) { - if ($model->isActive()) { - $string = ''; - } else { - $string = ''; - } - - return Html::encode($model->name) . ' ' . $string; - }, - ], - [ - 'label' => 'Status:', - 'format' => 'raw', - 'value' => function($model) { - /* @var $ecommerceIntegration EcommerceIntegration */ - $ecommerceIntegration = $model->ecommerceIntegration; - - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED] . ' ' . $icon . ''; - - if ($ecommerceIntegration && $ecommerceIntegration->status != EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED) { - if ($ecommerceIntegration->isConnected()) { - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; - } elseif ($ecommerceIntegration->isPaused()) { - $icon = ''; - $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; - } - } - - return $string; - }, - ], - [ - 'label' => 'Processed orders:', - 'format' => 'raw', - 'value' => function($model) { - return 0; - }, - ], - [ - 'label' => 'Pending orders:', - 'format' => 'raw', - 'value' => function($model) { - return 0; - }, - ], - [ - 'label' => 'Actions:', - 'format' => 'raw', - 'visible' => $model->isActive(), - 'value' => function($model) use ($pauseConfirm, $disconnectConfirm) { - /* @var $ecommerceIntegration EcommerceIntegration */ - $ecommerceIntegration = $model->ecommerceIntegration; - - if (!$ecommerceIntegration) { - $status = EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED; - } else { - $status = $ecommerceIntegration->status; - } - - switch ($status) { - case EcommerceIntegration::STATUS_INTEGRATION_DISCONNECTED: - $url = Url::to(['/ecommerce-integration/connect', 'platform' => $model->name]); - $buttons = 'Connect'; - break; - case EcommerceIntegration::STATUS_INTEGRATION_CONNECTED: - $url = Url::to(['/ecommerce-integration/pause', 'platform' => $model->name]); - $buttons = 'Pause'; - - $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); - $buttons .= ' Disconnect'; - break; - case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: - $url = Url::to(['/ecommerce-integration/resume', 'platform' => $model->name]); - $buttons = 'Resume'; - - $url = Url::to(['/ecommerce-integration/disconnect', 'platform' => $model->name]); - $buttons .= ' Disconnect'; - break; - default: - $buttons = ''; - } - - return $buttons; - }, - ], - ], -]) ?> +ecommercePlatform->name) { + case EcommercePlatform::SHOPIFY_PLATFORM_NAME: + echo $this->render('_shopify', [ + 'model' => $model, + ]); + break; + default: + } +?> diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php new file mode 100644 index 00000000..51519016 --- /dev/null +++ b/frontend/views/ecommerce-integration/_shopify.php @@ -0,0 +1,107 @@ + + + $model, + 'options' => [ + 'id' => $model->id, + 'class' => 'table table-striped table-bordered detail-view' + ], + 'attributes' => [ + [ + 'label' => 'Platform:', + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + if ($model->ecommercePlatform->isActive()) { + $string = ''; + } else { + $string = ''; + } + + return Html::encode($model->ecommercePlatform->name) . ' ' . $string; + }, + ], + [ + 'label' => 'Status:', + 'format' => 'raw', + 'value' => function($model) { + if ($model->isConnected()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; + } elseif ($model->isPaused()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; + } else { + $string = null; + } + + return $string; + }, + ], + [ + 'label' => 'Shop name:', + 'format' => 'raw', + 'value' => function($model) { + $name = isset($model->array_meta_data['shop_name']) ? Html::encode($model->array_meta_data['shop_name']) : null; + return $name; + }, + ], + [ + 'label' => 'Shop URL:', + 'format' => 'raw', + 'value' => function($model) { + return '' . $model->array_meta_data['shop_url'] . ' '; + }, + ], + [ + 'label' => 'Connected:', + 'attribute' => 'created_date', + 'format' => 'datetime', + ], + [ + 'label' => 'Actions:', + 'format' => 'raw', + 'visible' => $model->ecommercePlatform->isActive(), + 'value' => function($model) use ($pauseConfirm, $disconnectConfirm) { + $status = $model->status; + + switch ($status) { + case EcommerceIntegration::STATUS_INTEGRATION_CONNECTED: + $url = Url::to(['/ecommerce-integration/pause', 'id' => $model->id]); + $buttons = 'Pause'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Disconnect'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: + $url = Url::to(['/ecommerce-integration/resume', 'id' => $model->id]); + $buttons = 'Resume'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Disconnect'; + break; + default: + $buttons = ''; + } + + return $buttons; + }, + ], + ], +]) ?> diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php index c2886e78..0de06906 100644 --- a/frontend/views/ecommerce-integration/index.php +++ b/frontend/views/ecommerce-integration/index.php @@ -2,12 +2,12 @@ use yii\web\View; use yii\helpers\Html; use yii\helpers\Url; -use common\models\EcommercePlatform; +use common\models\EcommerceIntegration; /* @var $this View */ -/* @var $models EcommercePlatform[] */ +/* @var $models EcommerceIntegration[] */ -$title = 'Ecommerce Integrations'; +$title = 'E-commerce Integrations'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = $title; ?> @@ -22,10 +22,19 @@
- - render('_platform', [ - 'model' => $model, - ]) ?> + + + render('_platform', [ + 'model' => $model, + ]) ?> + + +
+

+ No connected shops yet. + Please connect your first shop. +

+
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index e5293e29..998a0943 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -1,7 +1,6 @@ title = $title . ' - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Integrations', 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php index bb07649d..3e6a09c1 100644 --- a/frontend/views/ecommerce-platform/index.php +++ b/frontend/views/ecommerce-platform/index.php @@ -11,7 +11,7 @@ /* @var $searchModel EcommercePlatformSearch */ /* @var $dataProvider ActiveDataProvider */ -$title = 'Ecommerce Platforms'; +$title = 'E-commerce Platforms'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/ecommerce-platform/update.php b/frontend/views/ecommerce-platform/update.php index 5a1f231e..cf245d99 100644 --- a/frontend/views/ecommerce-platform/update.php +++ b/frontend/views/ecommerce-platform/update.php @@ -7,8 +7,8 @@ /* @var $model EcommercePlatform */ $title = 'Update ' . $model->name; -$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->title = $title . ' - E-commerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Platforms', 'url' => ['index']]; $this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; $this->params['breadcrumbs'][] = 'Update'; ?> diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php index 522fadb6..2d50506b 100644 --- a/frontend/views/ecommerce-platform/view.php +++ b/frontend/views/ecommerce-platform/view.php @@ -10,8 +10,8 @@ /* @var $model EcommercePlatform */ $title = $model->name; -$this->title = $title . ' - Ecommerce Platforms - ' . Yii::$app->name; -$this->params['breadcrumbs'][] = ['label' => 'Ecommerce Platforms', 'url' => ['index']]; +$this->title = $title . ' - E-commerce Platforms - ' . Yii::$app->name; +$this->params['breadcrumbs'][] = ['label' => 'E-commerce Platforms', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; ?> diff --git a/frontend/views/layouts/main.php b/frontend/views/layouts/main.php index f4ff30f9..799c256c 100755 --- a/frontend/views/layouts/main.php +++ b/frontend/views/layouts/main.php @@ -158,7 +158,7 @@ ['label' => 'Subscriptions', 'url' => ['/subscription']], ['label' => 'One-Time Charges', 'url' => ['/one-time-charge']], ['label' => 'Invoices', 'url' => ['/invoice']], - ['label' => 'Ecommerce Platforms', 'url' => ['/ecommerce-platform']], + ['label' => 'E-commerce Platforms', 'url' => ['/ecommerce-platform']], ['label' => 'Integrations', 'url' => ['/integration']], ['label' => 'Behaviors', 'url' => ['/behavior']], ['label' => 'Jobs', 'url' => ['/monitor/jobs']], From e938c68542fdbda8954bf1d5c3b6aa4c5849da2e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 19:35:14 +0200 Subject: [PATCH 41/81] Shopify documentation --- intro-docs/ecommerce-platfroms.md | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 intro-docs/ecommerce-platfroms.md diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md new file mode 100644 index 00000000..2366593c --- /dev/null +++ b/intro-docs/ecommerce-platfroms.md @@ -0,0 +1,54 @@ +# E-commerce Platforms + +### How to connect a new platform: + +1. Create a new `migration`. + +2. Implement a new `insert` query for the table `ecommerce_platform`. Specify the needed platform. Example - `console\migrations\m230221_134343_add_shopify_mock_ecommerce_platform.php`. + +3. Add implementation to: +- `common\models\EcommercePlatform.php` +- `common\models\EcommerceIntegration.php` +- `frontend\controllers\EcommercePlatformController.php` +- `frontend\controllers\EcommerceIntegrationController.php` + +### How to manage existing platforms: + +1. Visit our website - `/ecommerce-platform` (you must be an `Admin`). + +# Shopify + +### App: + +1. Register a new partner account - `https://www.shopify.com/partners`. + +2. Go to `Apps` -> `Create app` -> `Create app manually`. Copy `Client ID` and `Client secret`. +Insert them in `common\config\params-local.php` (`Shopify section`). + +3. Go to `Apps` -> `App setup`. In the `URLs` section specify the parameters for `App URL` and `Allowed redirection URL(s)`. +If you're going to **test it locally**, specify: + +- `App URL`: `https://shipwise.ngrok.io/` +- `Allowed redirection URL(s)`: `https://shipwise.ngrok.io/ecommerce-integration/shopify` + +4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain` +to `https://shipwise.ngrok.io`. + +### Test shop(s): + +1. Go to `https://partners.shopify.com/` -> `Stores`. + +2. Press `Add store` -> `Create developemrnt store`. + +3. Choose `Development store use`, specify `Store name`, specify `Store URL`. +In `Data and configurations`, choose `Start with test data`. + +4. You can create several test shops if needed. + +### Connect a test shop: + +1. Visit our website - `/ecommerce-integration/index`. Press the button `Connect Shopify shop`. + +2. In the form, specify your test shop's details (`name` and `URL`). + +3. If you want to remove the Shopify app from your test shop, visit `https://admin.shopify.com/` -> `Apps` -> `Apps and sales channels` -> and press `Uninstall`. From 0fd3bcc7782c720d233aa1929c2c27cda370d985 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 27 Feb 2023 20:03:02 +0200 Subject: [PATCH 42/81] Connected shops --- common/models/EcommercePlatform.php | 9 ++++----- frontend/views/ecommerce-platform/index.php | 2 +- frontend/views/ecommerce-platform/view.php | 4 ++-- intro-docs/ecommerce-platfroms.md | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php index ec5ba303..6895fa9c 100644 --- a/common/models/EcommercePlatform.php +++ b/common/models/EcommercePlatform.php @@ -40,12 +40,11 @@ public function switchStatus(): void $this->save(); } - /** - * TODO: implement this later - */ - public function getConnectedUsersCounter(): int + public function getConnectedShopsCounter(): int { - return 0; + return EcommerceIntegration::find() + ->where(['platform_id' => $this->id]) + ->count(); } public static function getShopifyObject(): ?EcommercePlatform diff --git a/frontend/views/ecommerce-platform/index.php b/frontend/views/ecommerce-platform/index.php index 3e6a09c1..76471650 100644 --- a/frontend/views/ecommerce-platform/index.php +++ b/frontend/views/ecommerce-platform/index.php @@ -34,7 +34,7 @@ 'format' => 'raw', 'value' => function($model) { $string = Html::encode($model->name); - $string .= '
Connected users: ' . $model->getConnectedUsersCounter() . ''; + $string .= '
Connected shops: ' . $model->getConnectedShopsCounter() . ''; if ($model->updated_date) { $string .= '
Last update: ' . Yii::$app->formatter->asDatetime($model->updated_date) . ''; diff --git a/frontend/views/ecommerce-platform/view.php b/frontend/views/ecommerce-platform/view.php index 2d50506b..ef5de95e 100644 --- a/frontend/views/ecommerce-platform/view.php +++ b/frontend/views/ecommerce-platform/view.php @@ -47,10 +47,10 @@ }, ], [ - 'label' => 'Connected users:', + 'label' => 'Connected shops:', 'format' => 'raw', 'value' => function($model) { - return $model->getConnectedUsersCounter(); + return $model->getConnectedShopsCounter(); }, ], [ diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 2366593c..0fb6888f 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -38,7 +38,7 @@ to `https://shipwise.ngrok.io`. 1. Go to `https://partners.shopify.com/` -> `Stores`. -2. Press `Add store` -> `Create developemrnt store`. +2. Press `Add store` -> `Create development store`. 3. Choose `Development store use`, specify `Store name`, specify `Store URL`. In `Data and configurations`, choose `Start with test data`. From fc9557f0f72f41af0a326c1d3e3e2429cae3710e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Thu, 2 Mar 2023 20:14:12 +0200 Subject: [PATCH 43/81] Update --- .../models/base/BaseEcommerceIntegration.php | 2 +- .../platforms/ConnectShopifyStoreForm.php | 22 ++- .../query/EcommerceIntegrationQuery.php | 6 + .../services/platforms/CreateOrderService.php | 128 +++++++++++++ common/services/platforms/ShopifyService.php | 131 ++++++++++--- .../jobs/platforms/ParseShopifyOrderJob.php | 180 ++++++++++++++++++ .../EcommerceIntegrationController.php | 29 +++ .../{_platform.php => _integration.php} | 0 .../views/ecommerce-integration/_shopify.php | 7 + .../views/ecommerce-integration/index.php | 7 +- .../views/ecommerce-integration/shopify.php | 18 ++ intro-docs/ecommerce-platfroms.md | 7 +- 12 files changed, 507 insertions(+), 30 deletions(-) create mode 100644 common/services/platforms/CreateOrderService.php create mode 100644 console/jobs/platforms/ParseShopifyOrderJob.php rename frontend/views/ecommerce-integration/{_platform.php => _integration.php} (100%) diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php index 6e5ad507..c4de3a64 100644 --- a/common/models/base/BaseEcommerceIntegration.php +++ b/common/models/base/BaseEcommerceIntegration.php @@ -50,7 +50,7 @@ public function rules(): array { return [ [['meta'], 'default', 'value' => null], - [['user_id', 'platform_id'], 'required'], + [['user_id', 'customer_id', 'platform_id'], 'required'], [['user_id', 'customer_id', 'platform_id', 'status'], 'integer'], [['meta'], 'string'], [['created_date', 'updated_date'], 'safe'], diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index efc8b81c..45cf9498 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -3,6 +3,7 @@ namespace common\models\forms\platforms; use Yii; +use yii\base\InvalidConfigException; use yii\base\Model; use yii\db\Expression; use PHPShopify\Exception\SdkException; @@ -10,6 +11,7 @@ use common\services\platforms\ShopifyService; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; +use common\models\Customer; /** * Class ConnectShopifyStoreForm @@ -22,12 +24,13 @@ class ConnectShopifyStoreForm extends Model public ?string $name = null; public ?string $url = null; + public ?int $customer_id = null; public ?string $code = null; public function scenarios(): array { return [ - self::SCENARIO_AUTH_REQUEST => ['name', 'url'], + self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'customer_id'], self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'], ]; } @@ -40,9 +43,10 @@ public function rules(): array /** @see https://www.regextester.com/104785 */ ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'], - [['name', 'url'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], + [['name', 'url', 'customer_id'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST], [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST], + ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], 'on' => self::SCENARIO_AUTH_REQUEST], [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], ]; @@ -53,6 +57,7 @@ public function attributeLabels(): array return [ 'name' => 'Shop Name', 'url' => 'Shop URL', + 'customer_id' => 'Customer', 'code' => 'Code', ]; } @@ -81,10 +86,12 @@ public function validateShopUrl(): void /** * @throws SdkException + * @throws InvalidConfigException */ public function auth(): void { $this->saveShopName(); + $this->saveCustomerId(); // Step 1 - Send request to receive access token $shopifyService = new ShopifyService($this->url); @@ -94,16 +101,25 @@ public function auth(): void /** * @throws ServerErrorHttpException * @throws SdkException + * @throws InvalidConfigException */ public function saveAccessToken(): void { // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken(Yii::$app->session->get('shop_name', 'Shop Name')); + $shopifyService->accessToken( + Yii::$app->session->get('shop_name', 'Shop Name'), + Yii::$app->user->id, + Yii::$app->session->get('customer_id')); } protected function saveShopName() { Yii::$app->session->set('shop_name', $this->name); } + + protected function saveCustomerId() + { + Yii::$app->session->set('customer_id', $this->customer_id); + } } diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php index 8b905414..ed04d0aa 100644 --- a/common/models/query/EcommerceIntegrationQuery.php +++ b/common/models/query/EcommerceIntegrationQuery.php @@ -2,6 +2,7 @@ namespace common\models\query; +use common\models\EcommerceIntegration; use yii\db\ActiveQuery; /** @@ -10,6 +11,11 @@ */ class EcommerceIntegrationQuery extends ActiveQuery { + public function active(): EcommerceIntegrationQuery + { + return $this->andWhere(['status' => EcommerceIntegration::STATUS_INTEGRATION_CONNECTED]); + } + public function for(?int $userId = null, ?int $customerId = null): EcommerceIntegrationQuery { if ($userId) { diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php new file mode 100644 index 00000000..a1b5ad8f --- /dev/null +++ b/common/services/platforms/CreateOrderService.php @@ -0,0 +1,128 @@ +customerId = $customerId; + $this->order = new Order(); + $this->address = new Address(); + } + + public function setOrder(array $attributes): void + { + $this->order->setAttributes($attributes); + // Skip validation by `address_id` for the moment (will be set later): + $this->order->address_id = 0; + } + + public function setCarrier() + { + + } + + public function setAddress(array $attributes): void + { + $this->address->setAttributes($attributes); + } + + public function setItems(array $items): void + { + $this->items = $items; + + foreach ($this->items as $k => $item) { + // Skip validation by `order_id` for the moment (will be set later): + $this->items[$k]['order_id'] = 0; + } + + $excluded = $this->getExcludedItems(); + + foreach ($this->items as $k => $item) { + if (in_array($item['sku'], $excluded)) { + unset($this->items[$k]); + } + } + } + + public function getOrderErrors(): array + { + return $this->order->getErrors(); + } + + public function getAddressErrors(): array + { + return $this->address->getErrors(); + } + + public function getItemsErrors(): array + { + return $this->itemsErrors; + } + + public function isValid(): bool + { + $orderIsValid = $this->order->validate(); + $addressIsValid = $this->address->validate(); + $itemsAreValid = true; + + foreach ($this->items as $key => $item) { + $orderItem = new Item(); + $orderItem->setAttributes($item); + + if (!$orderItem->validate()) { + $this->itemsErrors[$key] = $orderItem->getErrors(); + $itemsAreValid = false; + } + } + + return ($orderIsValid && $addressIsValid && $itemsAreValid); + } + + public function create(): bool + { + if (!$this->isValid()) { + return false; + } + + $this->order->save(); + $this->address->save(); + + $this->order->address_id = $this->address->id; + $this->order->save(); + + foreach ($this->items as $item) { + $orderItem = new Item(); + $orderItem->setAttributes($item); + $orderItem->order_id = $this->order->id; + $orderItem->save(); + } + + return true; + } + + protected function getExcludedItems(): array + { + return ArrayHelper::map( + Sku::find() + ->where(['customer_id' => $this->customerId, 'excluded' => 1]) + ->all(), 'id','sku'); + } +} diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 181e78d8..4513abaa 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -3,12 +3,15 @@ namespace common\services\platforms; use Yii; +use common\models\Order; +use yii\base\InvalidConfigException; +use yii\helpers\Json; +use yii\helpers\Url; +use console\jobs\platforms\ParseShopifyOrderJob; use common\models\EcommerceIntegration; use common\models\EcommercePlatform; use PHPShopify\ShopifySDK; use PHPShopify\AuthHelper; -use yii\helpers\Json; -use yii\helpers\Url; use yii\web\ServerErrorHttpException; use PHPShopify\Exception\SdkException; @@ -26,33 +29,44 @@ */ class ShopifyService { - protected ?string $token = null; + protected const API_VERSION = '2023-01'; protected string $shopUrl; protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; protected string $redirectUrl = '/ecommerce-integration/shopify'; protected ShopifySDK $shopify; + protected ?EcommerceIntegration $ecommerceIntegration = null; - public function __construct(string $shopUrl, string $token = null) + /** + * @throws InvalidConfigException + */ + public function __construct(string $shopUrl, EcommerceIntegration $ecommerceIntegration = null) { $this->shopUrl = $shopUrl; - $this->token = $token; - - if (!$this->token) { // Authorize user's shop - $config = [ - 'ShopUrl' => $this->shopUrl, - 'ApiKey' => Yii::$app->params['shopify']['client_id'], - 'SharedSecret' => Yii::$app->params['shopify']['client_secret'], - ]; - } else { // Use existing user's shop - $config = [ - 'ShopUrl' => $this->shopUrl, - 'AccessToken' => $this->token, - ]; + $this->ecommerceIntegration = $ecommerceIntegration; + $config = [ + 'ApiVersion' => self::API_VERSION, + 'ShopUrl' => $this->shopUrl, + ]; + + if (!$this->ecommerceIntegration) { // Authorize user's shop: + $config['ApiKey'] = Yii::$app->params['shopify']['client_id']; + $config['SharedSecret'] = Yii::$app->params['shopify']['client_secret']; + } else { // Use existing user's shop: + $config['AccessToken'] = $this->ecommerceIntegration->array_meta_data['access_token']; } $this->shopify = new ShopifySDK($config); + + // Check if the provided token is valid: + if ($this->ecommerceIntegration) { + $this->isTokenValid(); + } } + ######### + # Auth: # + ######### + /** * @throws SdkException */ @@ -73,7 +87,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(string $shopName): void + public function accessToken(string $shopName, int $userId, int $customerId): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -86,7 +100,8 @@ public function accessToken(string $shopName): void ]; $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = Yii::$app->user->id; + $ecommerceIntegration->user_id = $userId; + $ecommerceIntegration->customer_id = $customerId; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT); @@ -96,10 +111,80 @@ public function accessToken(string $shopName): void } } - public function makeReq() + /** + * @throws InvalidConfigException + */ + protected function isTokenValid() { - echo '
';
-        print_r($this->shopify->Product->get());
-        exit;
+        try {
+            $this->getProductsList();
+        } catch (\Exception $e) {
+            throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+        }
+    }
+
+    #####################
+    # Get data via API: #
+    #####################
+
+    public function getProductsList(): array
+    {
+        return $this->shopify->Product->get();
+    }
+
+    public function getProductById(int $id): array
+    {
+        return $this->shopify->Product($id)->get();
+    }
+
+    public function getOrdersList(array $params = []): array
+    {
+        return $this->shopify->Order->get($params);
+    }
+
+    public function getOrderById(int $id): array
+    {
+        return $this->shopify->Order($id)->get();
+    }
+
+    public function getCustomerById(int $id): array
+    {
+        return $this->shopify->Customer($id)->get();
+    }
+
+    public function getCustomerAddressById(int $customerId, int $addressId): array
+    {
+        return $this->shopify->Customer($customerId)->Address($addressId)->get();
+    }
+
+    ##################
+    # Order parsing: #
+    ##################
+
+    public function parseRawOrderJob(array $order): void
+    {
+        if ($this->canBeParsed($order) && $this->isNotDuplicate($order)) {
+            Yii::$app->queue->push(
+                new ParseShopifyOrderJob([
+                    'rawOrder' => $order,
+                    'ecommerceIntegrationId' => $this->ecommerceIntegration->id
+                ])
+            );
+        }
+    }
+
+    protected function canBeParsed(array $order): bool
+    {
+        return (isset($order['shipping_address']) && isset($order['customer']));
+    }
+
+    protected function isNotDuplicate(array $order): bool
+    {
+        return !Order::find()->where([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'order_reference' => $order['name'],
+            'customer_reference' => $order['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])->exists();
     }
 }
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php
new file mode 100644
index 00000000..09e97f5d
--- /dev/null
+++ b/console/jobs/platforms/ParseShopifyOrderJob.php
@@ -0,0 +1,180 @@
+setEcommerceIntegration();
+        $this->parseOrderData();
+        $this->parseAddressData();
+        $this->parseItemsData();
+        $this->saveOrder();
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceIntegration(): void
+    {
+        $ecommerceIntegration = EcommerceIntegration::findOne($this->ecommerceIntegrationId);
+
+        if (!$ecommerceIntegration) {
+            throw new NotFoundHttpException('E-commerce integration not found.');
+        }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
+    }
+
+    protected function parseOrderData(): void
+    {
+        $this->parsedOrderAttributes = [
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+            'customer_reference' => (string)$this->rawOrder['id'],
+            'order_reference' => $this->rawOrder['name'],
+            'status_id' => Status::OPEN,
+            'uuid' => (string)$this->rawOrder['id'],
+            'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'notes' => $this->rawOrder['tags'],
+            'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
+        ];
+    }
+
+    protected function parseAddressData(): void
+    {
+        $notProvided = 'Not provided.';
+        $name = null;
+        $address1 = null;
+        $address2 = null;
+        $company = null;
+        $city = null;
+        $phone = null;
+        $stateId = 0;
+        $zip = null;
+        $countryCode = State::DEFAULT_COUNTRY_ABBR;
+
+        if (isset($this->rawOrder['shipping_address']['name'])) {
+            $name = trim($this->rawOrder['shipping_address']['name']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['address1'])) {
+            $address1 = trim($this->rawOrder['shipping_address']['address1']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['address2'])) {
+            $address2 = trim($this->rawOrder['shipping_address']['address2']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['company'])) {
+            $company = trim($this->rawOrder['shipping_address']['company']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['city'])) {
+            $city = trim($this->rawOrder['shipping_address']['city']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['phone'])) {
+            $phone = trim($this->rawOrder['shipping_address']['phone']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['zip'])) {
+            $zip = trim($this->rawOrder['shipping_address']['zip']);
+        }
+
+        if (isset($this->rawOrder['shipping_address']['province_code'])) {
+            $state = State::find()->where([
+                'abbreviation' => trim($this->rawOrder['shipping_address']['province_code'])
+            ])->one();
+
+            if ($state) {
+                $stateId = $state->id;
+            }
+        }
+
+        if (isset($this->rawOrder['shipping_address']['country_code'])) {
+            $country = Country::find()->where([
+                'abbreviation' => trim($this->rawOrder['shipping_address']['country_code'])
+            ])->one();
+
+            if ($country) {
+                $countryCode = $country->abbreviation;
+            }
+        }
+
+        $this->parsedAddressAttributes = [
+            'name' => ($name) ?: $notProvided,
+            'address1' => ($address1) ?: $notProvided,
+            'address2' => $address2,
+            'company' => $company,
+            'city' => ($city) ?: $notProvided,
+            'phone' => ($phone) ?: $notProvided,
+            'state_id' => $stateId,
+            'zip' => ($zip) ?: $notProvided,
+            'country' => $countryCode,
+        ];
+    }
+
+    protected function parseItemsData(): void
+    {
+        foreach ($this->rawOrder['line_items'] as $item) {
+            $this->parsedItemsAttributes[] = [
+                'quantity' => $item['fulfillable_quantity'],
+                'sku' => ($item['sku']) ?: 'Not provided.',
+                'name' => $item['name'],
+                'uuid' => (string)$item['id'],
+            ];
+        }
+    }
+
+    protected function saveOrder(): void
+    {
+        $createOrderService = new CreateOrderService($this->ecommerceIntegration->customer_id);
+        $createOrderService->setOrder($this->parsedOrderAttributes);
+        $createOrderService->setCarrier();
+        $createOrderService->setAddress($this->parsedAddressAttributes);
+        $createOrderService->setItems($this->parsedItemsAttributes);
+
+        if ($createOrderService->isValid()) {
+            $createOrderService->create();
+        }
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index a92250a1..b9fbfed3 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -2,6 +2,7 @@
 
 namespace frontend\controllers;
 
+use common\services\platforms\ShopifyService;
 use Yii;
 use yii\web\Response;
 use yii\filters\AccessControl;
@@ -34,6 +35,34 @@ public function behaviors(): array
         ];
     }
 
+    public function actionTest()
+    {
+        $integrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($integrations as $integration) {
+            $accessToken = $integration->array_meta_data['access_token'];
+
+            if ($accessToken) {
+                $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration);
+
+                $params = [
+                    'status' => 'open',
+                    'fufillment_status' => 'unfulfilled',
+                    'limit' => 250,
+                ];
+
+                $orders = $shopifyService->getOrdersList($params);
+
+                foreach ($orders as $order) {
+                    $shopifyService->parseRawOrderJob($order);
+                }
+            }
+        }
+    }
+
     /**
      * Lists all EcommerceIntegration models for the current user.
      * @return string
diff --git a/frontend/views/ecommerce-integration/_platform.php b/frontend/views/ecommerce-integration/_integration.php
similarity index 100%
rename from frontend/views/ecommerce-integration/_platform.php
rename to frontend/views/ecommerce-integration/_integration.php
diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php
index 51519016..e21dc489 100644
--- a/frontend/views/ecommerce-integration/_shopify.php
+++ b/frontend/views/ecommerce-integration/_shopify.php
@@ -69,6 +69,13 @@
                 return '' . $model->array_meta_data['shop_url'] . ' ';
             },
         ],
+        [
+            'label' => 'Customer',
+            'format' => 'raw',
+            'value' => function($model) {
+                return Html::encode($model->customer->name);
+            },
+        ],
         [
             'label' => 'Connected:',
             'attribute' => 'created_date',
diff --git a/frontend/views/ecommerce-integration/index.php b/frontend/views/ecommerce-integration/index.php
index 0de06906..fee504d6 100644
--- a/frontend/views/ecommerce-integration/index.php
+++ b/frontend/views/ecommerce-integration/index.php
@@ -3,6 +3,7 @@
 use yii\helpers\Html;
 use yii\helpers\Url;
 use common\models\EcommerceIntegration;
+use common\models\EcommercePlatform;
 
 /* @var $this View */
 /* @var $models EcommerceIntegration[] */
@@ -18,13 +19,15 @@
     
 
     
- Connect Shopify shop + isActive()) { ?> + Connect Shopify shop +
- render('_platform', [ + render('_integration', [ 'model' => $model, ]) ?> diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index 998a0943..ef35a748 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -1,8 +1,10 @@ title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; + +$customersList = ArrayHelper::map( + Customer::find() + ->orderBy(['name' => SORT_ASC]) + ->all(), 'id','name'); ?>
@@ -46,6 +53,17 @@ 'maxlength' => true ]) ?>
+
+ field($model, 'customer_id') + ->dropdownList($customersList, [ + 'class' => 'form-control', + 'prompt' => $model->getAttributeLabel('customer_id') . '...', + 'placeholder' => $model->getAttributeLabel('customer_id') . '...', + 'required' => true, + 'maxlength' => true + ]) ?> +
'btn btn-success', diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 0fb6888f..1167f39a 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -16,7 +16,12 @@ 1. Visit our website - `/ecommerce-platform` (you must be an `Admin`). -# Shopify +# Constraints + +1. A new integration can be added by a user only if the needed e-commerce platform +has the status `Active`. + +# E-commerce Integrations - Shopify ### App: From 19f187c852e92aba608b74327e268fb7112730db Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 3 Mar 2023 16:47:52 +0200 Subject: [PATCH 44/81] Added: Order Statuses, Financial Statuses, Fulfillment Statuses --- common/models/EcommerceIntegration.php | 5 ++ .../platforms/ConnectShopifyStoreForm.php | 57 ++++++++++++------- common/services/platforms/ShopifyService.php | 44 ++++++++++++-- .../EcommerceIntegrationController.php | 21 ++++++- .../views/ecommerce-integration/_shopify.php | 42 +++++++++++++- .../views/ecommerce-integration/shopify.php | 48 ++++++++++++++++ frontend/web/css/site.css | 12 ++++ 7 files changed, 203 insertions(+), 26 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index a066ffc4..60b30598 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -57,6 +57,11 @@ public function resume(): bool return $this->save(); } + public function isMetaKeyExistsAndNotEmpty(string $key): bool + { + return (isset($this->array_meta_data[$key]) && !empty($this->array_meta_data[$key])); + } + protected function convertMetaData(): void { if ($this->meta) { diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index 45cf9498..d370754f 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -24,13 +24,16 @@ class ConnectShopifyStoreForm extends Model public ?string $name = null; public ?string $url = null; + public string|array|null $order_statuses = null; + public string|array|null $financial_statuses = null; + public string|array|null $fulfillment_statuses = null; public ?int $customer_id = null; public ?string $code = null; public function scenarios(): array { return [ - self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'customer_id'], + self::SCENARIO_AUTH_REQUEST => ['name', 'url', 'order_statuses', 'financial_statuses', 'fulfillment_statuses', 'customer_id'], self::SCENARIO_SAVE_ACCESS_TOKEN => ['code'], ]; } @@ -43,12 +46,23 @@ public function rules(): array /** @see https://www.regextester.com/104785 */ ['url', 'match', 'pattern' => '/[^.\s]+\.myshopify\.com$/', 'message' => 'Invalid shop URL.'], - [['name', 'url', 'customer_id'], 'required', 'on' => self::SCENARIO_AUTH_REQUEST], - [['name'], 'validateShopName', 'on' => self::SCENARIO_AUTH_REQUEST], - [['url'], 'validateShopUrl', 'on' => self::SCENARIO_AUTH_REQUEST], - ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], 'on' => self::SCENARIO_AUTH_REQUEST], - - [['url', 'code'], 'required', 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], + [['name', 'url', 'customer_id'], 'required', + 'on' => self::SCENARIO_AUTH_REQUEST], + [['name'], 'validateShopName', + 'on' => self::SCENARIO_AUTH_REQUEST], + [['url'], 'validateShopUrl', + 'on' => self::SCENARIO_AUTH_REQUEST], + ['order_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$orderStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['financial_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$financialStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['fulfillment_statuses', 'in', 'allowArray' => true, 'range' => array_keys(ShopifyService::$fulfillmentStatuses), + 'on' => self::SCENARIO_AUTH_REQUEST], + ['customer_id', 'exist', 'skipOnError' => true, 'targetClass' => Customer::class, 'targetAttribute' => ['customer_id' => 'id'], + 'on' => self::SCENARIO_AUTH_REQUEST], + + [['url', 'code'], 'required', + 'on' => self::SCENARIO_SAVE_ACCESS_TOKEN], ]; } @@ -57,6 +71,9 @@ public function attributeLabels(): array return [ 'name' => 'Shop Name', 'url' => 'Shop URL', + 'order_statuses' => 'Order Statuses', + 'financial_statuses' => 'Financial Statuses', + 'fulfillment_statuses' => 'Fulfillment Statuses', 'customer_id' => 'Customer', 'code' => 'Code', ]; @@ -90,8 +107,7 @@ public function validateShopUrl(): void */ public function auth(): void { - $this->saveShopName(); - $this->saveCustomerId(); + $this->saveDataForSecondStep(); // Step 1 - Send request to receive access token $shopifyService = new ShopifyService($this->url); @@ -105,21 +121,24 @@ public function auth(): void */ public function saveAccessToken(): void { + $data = unserialize(Yii::$app->session->get('shopify_connection_second_step')); + // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken( - Yii::$app->session->get('shop_name', 'Shop Name'), - Yii::$app->user->id, - Yii::$app->session->get('customer_id')); + $shopifyService->accessToken($data); } - protected function saveShopName() + protected function saveDataForSecondStep() { - Yii::$app->session->set('shop_name', $this->name); - } + $data = [ + 'shop_name' => $this->name, + 'customer_id' => $this->customer_id, + 'user_id' => Yii::$app->user->id, + 'order_statuses' => $this->order_statuses, + 'financial_statuses' => $this->financial_statuses, + 'fulfillment_statuses' => $this->fulfillment_statuses, + ]; - protected function saveCustomerId() - { - Yii::$app->session->set('customer_id', $this->customer_id); + Yii::$app->session->set('shopify_connection_second_step', serialize($data)); } } diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 4513abaa..4ec55d1a 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -29,6 +29,39 @@ */ class ShopifyService { + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any-examples + */ + public static array $orderStatuses = [ + 'open' => 'Open', + 'closed' => 'Closed', + 'cancelled' => 'Cancelled', + ]; + + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any + */ + public static array $financialStatuses = [ + 'authorized' => 'Authorized', + 'pending' => 'Pending', + 'paid' => 'Paid', + 'partially_paid' => 'Partially paid', + 'refunded' => 'Refunded', + 'voided' => 'Voided', + 'partially_refunded' => 'Partially refunded', + 'unpaid' => 'Unpaid', + ]; + + /** + * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/order#get-orders?status=any + */ + public static array $fulfillmentStatuses = [ + 'shipped' => 'Shipped', // Show orders that have been shipped. Returns orders with `fulfillment_status` of `fulfilled` + 'partial' => 'Partial', // Show partially shipped orders + 'unshipped' => 'Unshipped', // Show orders that have not yet been shipped. Returns orders with `fulfillment_status` of `null` + 'unfulfilled' => 'Unfulfilled', // Returns orders with `fulfillment_status` of `null` or `partial` + ]; + protected const API_VERSION = '2023-01'; protected string $shopUrl; protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; @@ -87,7 +120,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(string $shopName, int $userId, int $customerId): void + public function accessToken(array $data): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -95,13 +128,16 @@ public function accessToken(string $shopName, int $userId, int $customerId): voi $meta = [ 'platform' => EcommercePlatform::SHOPIFY_PLATFORM_NAME, 'shop_url' => $this->shopUrl, - 'shop_name' => $shopName, + 'shop_name' => $data['shop_name'], + 'order_statuses' => $data['order_statuses'], + 'financial_statuses' => $data['financial_statuses'], + 'fulfillment_statuses' => $data['fulfillment_statuses'], 'access_token' => $accessToken, ]; $ecommerceIntegration = new EcommerceIntegration(); - $ecommerceIntegration->user_id = $userId; - $ecommerceIntegration->customer_id = $customerId; + $ecommerceIntegration->user_id = $data['user_id']; + $ecommerceIntegration->customer_id = $data['customer_id']; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; $ecommerceIntegration->status = EcommerceIntegration::STATUS_INTEGRATION_CONNECTED; $ecommerceIntegration->meta = Json::encode($meta, JSON_PRETTY_PRINT); diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index b9fbfed3..64bd0c2a 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -48,14 +48,31 @@ public function actionTest() if ($accessToken) { $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration); + /** + * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any + */ $params = [ - 'status' => 'open', - 'fufillment_status' => 'unfulfilled', 'limit' => 250, ]; + if ($integration->isMetaKeyExistsAndNotEmpty('order_statuses')) { + $params['status'] = implode(',', $integration->array_meta_data['order_statuses']); + } + + if ($integration->isMetaKeyExistsAndNotEmpty('financial_statuses')) { + $params['financial_status'] = implode(',', $integration->array_meta_data['financial_statuses']); + } + + if ($integration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) { + $params['fulfillment_status'] = implode(',', $integration->array_meta_data['fulfillment_statuses']); + } + $orders = $shopifyService->getOrdersList($params); + echo '
';
+                print_r($orders);
+                exit;
+
                 foreach ($orders as $order) {
                     $shopifyService->parseRawOrderJob($order);
                 }
diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php
index e21dc489..85fe3926 100644
--- a/frontend/views/ecommerce-integration/_shopify.php
+++ b/frontend/views/ecommerce-integration/_shopify.php
@@ -4,6 +4,7 @@
 use yii\helpers\Url;
 use yii\widgets\DetailView;
 use common\models\EcommerceIntegration;
+use common\services\platforms\ShopifyService;
 
 /* @var $this View */
 /* @var $model EcommerceIntegration */
@@ -70,7 +71,46 @@
             },
         ],
         [
-            'label' => 'Customer',
+            'label' => 'Order Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('order_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$orderStatuses[$el];
+                    }, $model->array_meta_data['order_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Financial Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$financialStatuses[$el];
+                    }, $model->array_meta_data['financial_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Fulfillment Statuses:',
+            'format' => 'raw',
+            'value' => function($model) {
+                if ($model->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
+                    return implode(', ', array_map(function ($el) {
+                        return ShopifyService::$fulfillmentStatuses[$el];
+                    }, $model->array_meta_data['fulfillment_statuses']));
+                }
+
+                return 'Any';
+            },
+        ],
+        [
+            'label' => 'Customer:',
             'format' => 'raw',
             'value' => function($model) {
                 return Html::encode($model->customer->name);
diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php
index ef35a748..c89ec3f5 100644
--- a/frontend/views/ecommerce-integration/shopify.php
+++ b/frontend/views/ecommerce-integration/shopify.php
@@ -4,6 +4,7 @@
 use yii\helpers\ArrayHelper;
 use yii\widgets\ActiveForm;
 use common\models\forms\platforms\ConnectShopifyStoreForm;
+use common\services\platforms\ShopifyService;
 use common\models\Customer;
 
 /* @var $this View */
@@ -18,6 +19,10 @@
     Customer::find()
         ->orderBy(['name' => SORT_ASC])
         ->all(), 'id','name');
+
+$orderStatuses = ' Order Statuses';
+$financialStatusesLabel = ' Financial Statuses';
+$fulfillmentStatusesLabel = ' Fulfillment Statuses';
 ?>
 
 
@@ -53,6 +58,49 @@ 'maxlength' => true ]) ?>
+ +
+ field($model, 'order_statuses') + ->checkboxList(ShopifyService::$orderStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('order_statuses') . '...', + ]) + ->label($orderStatuses) ?> +
+ +
+ field($model, 'financial_statuses') + ->checkboxList(ShopifyService::$financialStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('financial_statuses') . '...', + ]) + ->label($financialStatusesLabel) ?> +
+ +
+ field($model, 'fulfillment_statuses') + ->checkboxList(ShopifyService::$fulfillmentStatuses, [ + 'itemOptions' => [ + 'labelOptions' => [ + 'class' => 'font-weight-normal mr-1', + ], + ], + 'placeholder' => $model->getAttributeLabel('fulfillment_statuses') . '...', + ]) + ->label($fulfillmentStatusesLabel) ?> +
+
field($model, 'customer_id') diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 499177ee..fee2a6c9 100755 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -384,6 +384,10 @@ pre { margin: 0 0 15px 0 !important; } +.font-weight-normal { + font-weight: normal; +} + .mb-2 { margin-bottom: 20px; } @@ -391,3 +395,11 @@ pre { .mt-2 { margin-top: 20px; } + +.ml-1 { + margin-left: 10px; +} + +.mr-1 { + margin-right: 10px; +} From 709d00c344187ce4f5db0db44db8611b67417558 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 3 Mar 2023 18:09:26 +0200 Subject: [PATCH 45/81] Customers drop-down only for the current user --- .../EcommerceIntegrationController.php | 21 +++++++++++++++++++ .../views/ecommerce-integration/shopify.php | 6 +----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index 64bd0c2a..5c02dfa4 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -3,7 +3,9 @@ namespace frontend\controllers; use common\services\platforms\ShopifyService; +use frontend\models\Customer; use Yii; +use yii\helpers\ArrayHelper; use yii\web\Response; use yii\filters\AccessControl; use Da\User\Filter\AccessRuleFilter; @@ -137,6 +139,7 @@ public function actionShopify(): string|Response return $this->render('shopify', [ 'model' => $model, + 'customersList' => $this->getCustomersList(), ]); } @@ -203,6 +206,24 @@ protected function getEcommerceIntegrationById(int $id): EcommerceIntegration return $model; } + protected function getCustomersList(): array + { + if (Yii::$app->user->identity->isAdmin) { + $data = Customer::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + } else { + $data = Customer::find() + ->where("`id` IN(SELECT DISTINCT(`customer_id`) FROM `user_customer` WHERE `user_id` = :user_id)", [ + 'user_id' => Yii::$app->user->id + ]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + } + + return ArrayHelper::map($data, 'id','name'); + } + /** * Checks a needed EcommercePlatform model by the provided name. It must exist and be active. * @throws NotFoundHttpException diff --git a/frontend/views/ecommerce-integration/shopify.php b/frontend/views/ecommerce-integration/shopify.php index c89ec3f5..d377682f 100644 --- a/frontend/views/ecommerce-integration/shopify.php +++ b/frontend/views/ecommerce-integration/shopify.php @@ -9,17 +9,13 @@ /* @var $this View */ /* @var $model ConnectShopifyStoreForm */ +/* @var $customersList array */ $title = 'Shopify Integration'; $this->title = $title . ' - ' . Yii::$app->name; $this->params['breadcrumbs'][] = ['label' => 'E-commerce Integrations', 'url' => ['index']]; $this->params['breadcrumbs'][] = $title; -$customersList = ArrayHelper::map( - Customer::find() - ->orderBy(['name' => SORT_ASC]) - ->all(), 'id','name'); - $orderStatuses = ' Order Statuses'; $financialStatusesLabel = ' Financial Statuses'; $fulfillmentStatusesLabel = ' Fulfillment Statuses'; From 2f19d27ed6f0e2e6dba245429ad83dc4c24b12ff Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 15:26:33 +0200 Subject: [PATCH 46/81] Reconnect uninstalled shop --- common/models/EcommerceIntegration.php | 13 +++++ .../platforms/ConnectShopifyStoreForm.php | 34 +++++++++--- common/services/platforms/ShopifyService.php | 12 +++-- .../EcommerceIntegrationController.php | 52 ++++++++++++++----- .../views/ecommerce-integration/_shopify.php | 26 +++++++--- intro-docs/ecommerce-platfroms.md | 6 +++ 6 files changed, 113 insertions(+), 30 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index 60b30598..2112a852 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -13,6 +13,7 @@ class EcommerceIntegration extends BaseEcommerceIntegration { public const STATUS_INTEGRATION_CONNECTED = 1; public const STATUS_INTEGRATION_PAUSED = 0; + public const STATUS_INTEGRATION_UNINSTALLED = -1; public array $array_meta_data = []; @@ -27,6 +28,7 @@ public static function getStatuses(): array return [ self::STATUS_INTEGRATION_CONNECTED => 'Connected', self::STATUS_INTEGRATION_PAUSED => 'Paused', + self::STATUS_INTEGRATION_UNINSTALLED => 'Uninstalled', ]; } @@ -40,11 +42,22 @@ public function isPaused(): bool return $this->status === self::STATUS_INTEGRATION_PAUSED; } + public function isUninstalled(): bool + { + return $this->status === self::STATUS_INTEGRATION_UNINSTALLED; + } + public function disconnect(): bool|int { return $this->delete(); } + public function uninstall(): void + { + $this->status = self::STATUS_INTEGRATION_UNINSTALLED; + $this->save(); + } + public function pause(): bool { $this->status = self::STATUS_INTEGRATION_PAUSED; diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php index d370754f..916f3788 100644 --- a/common/models/forms/platforms/ConnectShopifyStoreForm.php +++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php @@ -22,6 +22,8 @@ class ConnectShopifyStoreForm extends Model public const SCENARIO_AUTH_REQUEST = 'scenarioAuthRequest'; public const SCENARIO_SAVE_ACCESS_TOKEN = 'scenarioSaveAccessToken'; + public ?EcommerceIntegration $ecommerceIntegration = null; + public ?string $name = null; public ?string $url = null; public string|array|null $order_statuses = null; @@ -81,10 +83,15 @@ public function attributeLabels(): array public function validateShopName(): void { - if (EcommerceIntegration::find() + $query = EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :name', [':name' => '%"' . $this->name . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) - ->exists()) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]); + + if ($this->ecommerceIntegration) { + $query->andWhere('id != :id', ['id' => $this->ecommerceIntegration->id]); + } + + if ($query->exists()) { $this->addError('name', 'Shop name already exists.'); } @@ -92,10 +99,15 @@ public function validateShopName(): void public function validateShopUrl(): void { - if (EcommerceIntegration::find() + $query = EcommerceIntegration::find() ->andWhere(new Expression('`meta` LIKE :url', [':url' => '%"' . $this->url . '"%'])) - ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]) - ->exists()) + ->andWhere(['platform_id' => EcommercePlatform::getShopifyObject()->id]); + + if ($this->ecommerceIntegration) { + $query->andWhere('id != :id', ['id' => $this->ecommerceIntegration->id]); + } + + if ($query->exists()) { $this->addError('url', 'Shop URL already exists.'); } @@ -123,9 +135,13 @@ public function saveAccessToken(): void { $data = unserialize(Yii::$app->session->get('shopify_connection_second_step')); + if (isset($data['integration_id'])) { + $this->ecommerceIntegration = EcommerceIntegration::findOne($data['integration_id']); + } + // Step 2 - Receive and save access token: $shopifyService = new ShopifyService($this->url); - $shopifyService->accessToken($data); + $shopifyService->accessToken($data, $this->ecommerceIntegration); } protected function saveDataForSecondStep() @@ -139,6 +155,10 @@ protected function saveDataForSecondStep() 'fulfillment_statuses' => $this->fulfillment_statuses, ]; + if ($this->ecommerceIntegration) { + $data['integration_id'] = $this->ecommerceIntegration->id; + } + Yii::$app->session->set('shopify_connection_second_step', serialize($data)); } } diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 4ec55d1a..9cbb55a2 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -64,7 +64,7 @@ class ShopifyService protected const API_VERSION = '2023-01'; protected string $shopUrl; - protected string $scopes = 'read_products,read_customers,read_fulfillments,read_orders,read_shipping,read_returns'; + protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,'; protected string $redirectUrl = '/ecommerce-integration/shopify'; protected ShopifySDK $shopify; protected ?EcommerceIntegration $ecommerceIntegration = null; @@ -120,7 +120,7 @@ public function auth(): void * @throws SdkException * @throws ServerErrorHttpException */ - public function accessToken(array $data): void + public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegration = null): void { // Step 2 - Receive and save access token: $accessToken = AuthHelper::createAuthRequest($this->scopes); @@ -135,7 +135,9 @@ public function accessToken(array $data): void 'access_token' => $accessToken, ]; - $ecommerceIntegration = new EcommerceIntegration(); + if (!$ecommerceIntegration) { + $ecommerceIntegration = new EcommerceIntegration(); + } $ecommerceIntegration->user_id = $data['user_id']; $ecommerceIntegration->customer_id = $data['customer_id']; $ecommerceIntegration->platform_id = EcommercePlatform::findOne(['name' => EcommercePlatform::SHOPIFY_PLATFORM_NAME])->id; @@ -155,6 +157,10 @@ protected function isTokenValid() try { $this->getProductsList(); } catch (\Exception $e) { + if (!$this->ecommerceIntegration->isUninstalled()) { + $this->ecommerceIntegration->uninstall(); + } + throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.'); } } diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index 5c02dfa4..fca638a3 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -5,6 +5,7 @@ use common\services\platforms\ShopifyService; use frontend\models\Customer; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\ArrayHelper; use yii\web\Response; use yii\filters\AccessControl; @@ -101,19 +102,37 @@ public function actionIndex(): string /** * Connects a new Shopify shop. - * @throws SdkException + * @param int|null $id Reconnect a shop by ID. + * @return string|Response + * @throws InvalidConfigException * @throws NotFoundHttpException + * @throws SdkException * @throws ServerErrorHttpException */ - public function actionShopify(): string|Response + public function actionShopify(?int $id = null): string|Response { $this->checkEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME); + $model = new ConnectShopifyStoreForm(); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST; + + if ($id) { // Edit existing model (Reconnect action): + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + $model->ecommerceIntegration = $ecommerceIntegration; + + $model->setAttributes([ + 'name' => $ecommerceIntegration->array_meta_data['shop_name'], + 'url' => $ecommerceIntegration->array_meta_data['shop_url'], + 'customer_id' => $ecommerceIntegration->customer_id, + 'order_statuses' => $ecommerceIntegration->array_meta_data['order_statuses'], + 'financial_statuses' => $ecommerceIntegration->array_meta_data['financial_statuses'], + 'fulfillment_statuses' => $ecommerceIntegration->array_meta_data['fulfillment_statuses'], + ]); + } + if (Yii::$app->request->isPost) { // Step 1 - Send request to receive access token: - $model = new ConnectShopifyStoreForm([ - 'scenario' => ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST - ]); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_AUTH_REQUEST; $model->load(Yii::$app->request->post()); if ($model->validate()) { @@ -121,11 +140,9 @@ public function actionShopify(): string|Response } } elseif (Yii::$app->request->get('code')) { // Step 2 - Receive and save access token: - $model = new ConnectShopifyStoreForm([ - 'scenario' => ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN, - 'url' => Yii::$app->request->get('shop'), - 'code' => Yii::$app->request->get('code') - ]); + $model->scenario = ConnectShopifyStoreForm::SCENARIO_SAVE_ACCESS_TOKEN; + $model->url = Yii::$app->request->get('shop'); + $model->code = Yii::$app->request->get('code'); if ($model->validate()) { $model->saveAccessToken(); @@ -133,8 +150,6 @@ public function actionShopify(): string|Response Yii::$app->session->setFlash('success', 'Shopify shop has been connected.'); return $this->redirect(['index']); } - } else { - $model = new ConnectShopifyStoreForm(); } return $this->render('shopify', [ @@ -143,6 +158,19 @@ public function actionShopify(): string|Response ]); } + /** + * @throws NotFoundHttpException + */ + public function actionReconnect(int $id): Response + { + $ecommerceIntegration = $this->getEcommerceIntegrationById($id); + + return match ($ecommerceIntegration->ecommercePlatform->name) { + EcommercePlatform::SHOPIFY_PLATFORM_NAME => $this->redirect(['shopify', 'id' => $ecommerceIntegration->id]), + default => throw new NotFoundHttpException('Ecommerce platform not found.'), + }; + } + /** * Disconnects a needed EcommerceIntegration model. * @throws NotFoundHttpException diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php index 85fe3926..e92e0dd7 100644 --- a/frontend/views/ecommerce-integration/_shopify.php +++ b/frontend/views/ecommerce-integration/_shopify.php @@ -9,12 +9,12 @@ /* @var $this View */ /* @var $model EcommerceIntegration */ -$disconnectConfirm = 'Are you sure you want to disconnect this platform?'; -$disconnectConfirm .= ' In this case, all orders related to the platform will not be processed.'; -$disconnectConfirm .= ' Also, you will lose all your current credentials and will need to reconnect this platform again.'; +$disconnectConfirm = 'Are you sure you want to remove this shop?'; +$disconnectConfirm .= ' In this case, all orders related to the shop will not be processed.'; +$disconnectConfirm .= ' Also, you will lose all your current credentials and will need to reconnect the shop again.'; -$pauseConfirm = 'Are you sure you want to pause this platform?'; -$pauseConfirm .= ' In this case, all orders with the platform will not be processed.'; +$pauseConfirm = 'Are you sure you want to pause this shop?'; +$pauseConfirm .= ' In this case, all orders related to the shop will not be processed.'; ?> '; $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_CONNECTED] . ' ' . $icon . ''; } elseif ($model->isPaused()) { - $icon = ''; + $icon = ''; $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_PAUSED] . ' ' . $icon . ''; + } elseif ($model->isUninstalled()) { + $icon = ''; + $string = '' . EcommerceIntegration::getStatuses()[EcommerceIntegration::STATUS_INTEGRATION_UNINSTALLED] . ' ' . $icon . ''; } else { $string = null; } @@ -134,14 +137,21 @@ $buttons = 'Pause'; $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); - $buttons .= ' Disconnect'; + $buttons .= ' Remove'; break; case EcommerceIntegration::STATUS_INTEGRATION_PAUSED: $url = Url::to(['/ecommerce-integration/resume', 'id' => $model->id]); $buttons = 'Resume'; $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); - $buttons .= ' Disconnect'; + $buttons .= ' Remove'; + break; + case EcommerceIntegration::STATUS_INTEGRATION_UNINSTALLED: + $url = Url::to(['/ecommerce-integration/reconnect', 'id' => $model->id]); + $buttons = 'Reconnect'; + + $url = Url::to(['/ecommerce-integration/disconnect', 'id' => $model->id]); + $buttons .= ' Remove'; break; default: $buttons = ''; diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md index 1167f39a..b0dc92a8 100644 --- a/intro-docs/ecommerce-platfroms.md +++ b/intro-docs/ecommerce-platfroms.md @@ -39,6 +39,12 @@ If you're going to **test it locally**, specify: 4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain` to `https://shipwise.ngrok.io`. +5. You need to request `Protected customer data access`. Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` -> +Press the button `Request access`. On the page, select and request access for: + +- `Protected customer data` +- `Protected customer fields (optional)` -- all the fields + ### Test shop(s): 1. Go to `https://partners.shopify.com/` -> `Stores`. From 379132b89750ebf946a11e5650dab9090d8878d5 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 16:20:29 +0200 Subject: [PATCH 47/81] Send notification once a shop is uninstalled --- common/models/EcommerceIntegration.php | 23 +++++++++++++++++++- common/services/platforms/ShopifyService.php | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php index 2112a852..fe590942 100644 --- a/common/models/EcommerceIntegration.php +++ b/common/models/EcommerceIntegration.php @@ -2,8 +2,11 @@ namespace common\models; +use Yii; +use yii\helpers\Html; use yii\helpers\Json; use common\models\base\BaseEcommerceIntegration; +use console\jobs\NotificationJob; /** * Class EcommerceIntegration @@ -52,10 +55,28 @@ public function disconnect(): bool|int return $this->delete(); } - public function uninstall(): void + public function uninstall(bool $withNotification = false): void { $this->status = self::STATUS_INTEGRATION_UNINSTALLED; $this->save(); + + if ($withNotification) { + $shopUrl = Html::encode($this->array_meta_data['shop_url']); + $subject = '⚠️ Problem pulling data from ' . $shopUrl; + $message = 'We were not able to pull data from the Shopify shop ' . $shopUrl .'.'; + $message .= ' The status of the shop is changed to `Uninstalled`.'; + $message .= ' Please click the link below and try to reconnect the shop.'; + + Yii::$app->queue->push( + new NotificationJob([ + 'customer_id' => $this->customer_id, + 'subject' => $subject, + 'message' => $message, + 'url' => ['/ecommerce-integration/index'], + 'urlText' => 'Reconnect the shop', + ]) + ); + } } public function pause(): bool diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index 9cbb55a2..e08b3174 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -158,7 +158,7 @@ protected function isTokenValid() $this->getProductsList(); } catch (\Exception $e) { if (!$this->ecommerceIntegration->isUninstalled()) { - $this->ecommerceIntegration->uninstall(); + $this->ecommerceIntegration->uninstall(true); } throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.'); From 9e261fbce620db40a654e5ae0c94623c116a21d7 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 6 Mar 2023 20:58:50 +0200 Subject: [PATCH 48/81] E-commerce orders logs --- common/models/EcommerceOrderLog.php | 48 +++++++++ common/models/base/BaseEcommerceOrderLog.php | 101 ++++++++++++++++++ .../services/platforms/CreateOrderService.php | 4 +- common/services/platforms/ShopifyService.php | 11 +- .../jobs/platforms/ParseShopifyOrderJob.php | 39 +++++-- ...44546_create_ecommerce_order_log_table.php | 66 ++++++++++++ .../EcommerceIntegrationController.php | 6 +- 7 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 common/models/EcommerceOrderLog.php create mode 100644 common/models/base/BaseEcommerceOrderLog.php create mode 100644 console/migrations/m230306_144546_create_ecommerce_order_log_table.php diff --git a/common/models/EcommerceOrderLog.php b/common/models/EcommerceOrderLog.php new file mode 100644 index 00000000..5ef65f44 --- /dev/null +++ b/common/models/EcommerceOrderLog.php @@ -0,0 +1,48 @@ + 'Success', + self::STATUS_FAILED => 'Failed', + ]; + } + + public static function success(EcommerceIntegration $ecommerceIntegration, array $payload, Order $order): void + { + $ecommerceOrderLog = new EcommerceOrderLog(); + $ecommerceOrderLog->platform_id = $ecommerceIntegration->ecommercePlatform->id; + $ecommerceOrderLog->integration_id = $ecommerceIntegration->id; + $ecommerceOrderLog->original_order_id = (string)$payload['id']; + $ecommerceOrderLog->internal_order_id = $order->id; + $ecommerceOrderLog->status = self::STATUS_SUCCESS; + $ecommerceOrderLog->payload = Json::encode($payload, JSON_PRETTY_PRINT); + $ecommerceOrderLog->save(); + } + + public static function failed(EcommerceIntegration $ecommerceIntegration, array $payload, ?array $meta = null): void + { + $ecommerceOrderLog = new EcommerceOrderLog(); + $ecommerceOrderLog->platform_id = $ecommerceIntegration->ecommercePlatform->id; + $ecommerceOrderLog->integration_id = $ecommerceIntegration->id; + $ecommerceOrderLog->original_order_id = (string)$payload['id']; + $ecommerceOrderLog->status = self::STATUS_FAILED; + $ecommerceOrderLog->payload = Json::encode($payload, JSON_PRETTY_PRINT); + $ecommerceOrderLog->meta = ($meta) ? Json::encode($meta, JSON_PRETTY_PRINT) : null; + $ecommerceOrderLog->save(); + } +} diff --git a/common/models/base/BaseEcommerceOrderLog.php b/common/models/base/BaseEcommerceOrderLog.php new file mode 100644 index 00000000..d6a49bdb --- /dev/null +++ b/common/models/base/BaseEcommerceOrderLog.php @@ -0,0 +1,101 @@ + null], + [['platform_id', 'integration_id', 'original_order_id', 'status', 'payload'], 'required'], + [['platform_id', 'integration_id', 'internal_order_id'], 'integer'], + [['payload', 'meta'], 'string'], + [['created_date', 'updated_date'], 'safe'], + [['status'], 'string', 'max' => 64], + [['original_order_id'], 'string', 'max' => 256], + [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::className(), 'targetAttribute' => ['integration_id' => 'id']], + [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::className(), 'targetAttribute' => ['internal_order_id' => 'id']], + [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels(): array + { + return [ + 'id' => 'ID', + 'platform_id' => 'Platform ID', + 'integration_id' => 'Integration ID', + 'original_order_id' => 'Original Order ID', + 'internal_order_id' => 'Internal Order ID', + 'status' => 'Status', + 'payload' => 'Payload', + 'meta' => 'Meta Data', + 'created_date' => 'Created Date', + 'updated_date' => 'Updated Date', + ]; + } + + /** + * @return ActiveQuery + */ + public function getIntegration(): ActiveQuery + { + return $this->hasOne(EcommerceIntegration::className(), ['id' => 'integration_id']); + } + + /** + * @return ActiveQuery + */ + public function getInternalOrder(): ActiveQuery + { + return $this->hasOne(Order::className(), ['id' => 'internal_order_id']); + } + + /** + * @return ActiveQuery + */ + public function getPlatform(): ActiveQuery + { + return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']); + } +} diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php index a1b5ad8f..dd59dba4 100644 --- a/common/services/platforms/CreateOrderService.php +++ b/common/services/platforms/CreateOrderService.php @@ -96,7 +96,7 @@ public function isValid(): bool return ($orderIsValid && $addressIsValid && $itemsAreValid); } - public function create(): bool + public function create(): bool|Order { if (!$this->isValid()) { return false; @@ -115,7 +115,7 @@ public function create(): bool $orderItem->save(); } - return true; + return $this->order; } protected function getExcludedItems(): array diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php index e08b3174..a01c6dd8 100644 --- a/common/services/platforms/ShopifyService.php +++ b/common/services/platforms/ShopifyService.php @@ -2,6 +2,7 @@ namespace common\services\platforms; +use common\models\EcommerceOrderLog; use Yii; use common\models\Order; use yii\base\InvalidConfigException; @@ -205,7 +206,7 @@ public function getCustomerAddressById(int $customerId, int $addressId): array public function parseRawOrderJob(array $order): void { - if ($this->canBeParsed($order) && $this->isNotDuplicate($order)) { + if ($this->isNotDuplicate($order)) { Yii::$app->queue->push( new ParseShopifyOrderJob([ 'rawOrder' => $order, @@ -215,17 +216,11 @@ public function parseRawOrderJob(array $order): void } } - protected function canBeParsed(array $order): bool - { - return (isset($order['shipping_address']) && isset($order['customer'])); - } - protected function isNotDuplicate(array $order): bool { return !Order::find()->where([ 'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME, - 'order_reference' => $order['name'], - 'customer_reference' => $order['id'], + 'uuid' => (string)$order['id'], 'customer_id' => $this->ecommerceIntegration->customer_id, ])->exists(); } diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php index 09e97f5d..ce4b65bd 100644 --- a/console/jobs/platforms/ParseShopifyOrderJob.php +++ b/console/jobs/platforms/ParseShopifyOrderJob.php @@ -4,7 +4,9 @@ use common\models\Country; use common\models\EcommerceIntegration; +use common\models\EcommerceOrderLog; use common\models\EcommercePlatform; +use common\models\Order; use common\models\State; use common\models\Status; use common\services\platforms\CreateOrderService; @@ -34,10 +36,18 @@ class ParseShopifyOrderJob extends BaseObject implements RetryableJobInterface public function execute($queue): void { $this->setEcommerceIntegration(); - $this->parseOrderData(); - $this->parseAddressData(); - $this->parseItemsData(); - $this->saveOrder(); + $parsingErrors = $this->getParsingErrors(); + + if (!$parsingErrors) { + $this->parseOrderData(); + $this->parseAddressData(); + $this->parseItemsData(); + $order = $this->saveOrder(); + + EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order); + } else { + EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]); + } } /** @@ -54,6 +64,21 @@ protected function setEcommerceIntegration(): void $this->ecommerceIntegration = $ecommerceIntegration; } + protected function getParsingErrors(): array + { + $errors = []; + + if (!isset($this->rawOrder['shipping_address'])) { + $errors[] = 'Shipping address is missed.'; + } + + if (!isset($this->rawOrder['customer'])) { + $errors[] = 'Customer is missed.'; + } + + return $errors; + } + protected function parseOrderData(): void { $this->parsedOrderAttributes = [ @@ -71,7 +96,7 @@ protected function parseOrderData(): void protected function parseAddressData(): void { - $notProvided = 'Not provided.'; + $notProvided = 'Not provided'; $name = null; $address1 = null; $address2 = null; @@ -155,7 +180,7 @@ protected function parseItemsData(): void } } - protected function saveOrder(): void + protected function saveOrder(): Order|bool { $createOrderService = new CreateOrderService($this->ecommerceIntegration->customer_id); $createOrderService->setOrder($this->parsedOrderAttributes); @@ -164,7 +189,7 @@ protected function saveOrder(): void $createOrderService->setItems($this->parsedItemsAttributes); if ($createOrderService->isValid()) { - $createOrderService->create(); + return $createOrderService->create(); } } diff --git a/console/migrations/m230306_144546_create_ecommerce_order_log_table.php b/console/migrations/m230306_144546_create_ecommerce_order_log_table.php new file mode 100644 index 00000000..5d3f240a --- /dev/null +++ b/console/migrations/m230306_144546_create_ecommerce_order_log_table.php @@ -0,0 +1,66 @@ +execute(" + CREATE TABLE `ecommerce_order_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `platform_id` int NOT NULL, + `integration_id` int NOT NULL, + `original_order_id` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, + `internal_order_id` int DEFAULT NULL, + `status` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `meta` MEDIUMTEXT COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL , + `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_date` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + + $this->addForeignKey( + '{{%fk-ecommerce_order-platform_id}}', + '{{%ecommerce_order_log}}', + 'platform_id', + '{{%ecommerce_platform}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_order-integration_id}}', + '{{%ecommerce_order_log}}', + 'integration_id', + '{{%ecommerce_integration}}', + 'id', + 'CASCADE' + ); + + $this->addForeignKey( + '{{%fk-ecommerce_order-internal_order_id}}', + '{{%ecommerce_order_log}}', + 'internal_order_id', + '{{%orders}}', + 'id', + 'CASCADE' + ); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable('{{%ecommerce_order_log}}'); + } +} diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php index fca638a3..2e9e9f44 100644 --- a/frontend/controllers/EcommerceIntegrationController.php +++ b/frontend/controllers/EcommerceIntegrationController.php @@ -72,9 +72,9 @@ public function actionTest() $orders = $shopifyService->getOrdersList($params); - echo '
';
-                print_r($orders);
-                exit;
+//                echo '
';
+//                print_r($orders);
+//                exit;
 
                 foreach ($orders as $order) {
                     $shopifyService->parseRawOrderJob($order);

From 27c4c3a2fe9062ef760ec8de88cdf1aa238a92cc Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 18:05:24 +0200
Subject: [PATCH 49/81] Added Shopify orders parsing to Cron

---
 common/models/OrderHistory.php                |  9 +++-
 .../services/platforms/CreateOrderService.php | 19 ++++++++
 common/services/platforms/ShopifyService.php  | 44 +++++++++++++-----
 console/controllers/CronController.php        | 46 ++++++++++++++++++-
 .../jobs/platforms/ParseShopifyOrderJob.php   | 30 +++++++-----
 5 files changed, 121 insertions(+), 27 deletions(-)

diff --git a/common/models/OrderHistory.php b/common/models/OrderHistory.php
index 58aac294..f9c3edd8 100755
--- a/common/models/OrderHistory.php
+++ b/common/models/OrderHistory.php
@@ -77,8 +77,13 @@ protected function orderHistoryPopulate(): void
         }
 
         if (!$this->user_id) {
-            if (!Yii::$app->user->isGuest) {
-                $this->user_id = Yii::$app->user->id;
+            /**
+             * TODO: Replace with something like `system`
+             */
+            if (Yii::$app->request->isConsoleRequest) {
+                $this->user_id = 1;
+            } else {
+                $this->user_id = (!Yii::$app->user->isGuest) ? Yii::$app->user->id : 1;
             }
         }
 
diff --git a/common/services/platforms/CreateOrderService.php b/common/services/platforms/CreateOrderService.php
index dd59dba4..5e8814e7 100644
--- a/common/services/platforms/CreateOrderService.php
+++ b/common/services/platforms/CreateOrderService.php
@@ -125,4 +125,23 @@ protected function getExcludedItems(): array
                 ->where(['customer_id' => $this->customerId, 'excluded' => 1])
                 ->all(), 'id','sku');
     }
+
+    public static function isOrderExists(array $params): bool
+    {
+        $exists = Order::find();
+
+        if (isset($params['origin'])) {
+            $exists->andWhere(['origin' => $params['origin']]);
+        }
+
+        if (isset($params['uuid'])) {
+            $exists->andWhere(['uuid' => (string)$params['uuid']]);
+        }
+
+        if (isset($params['customer_id'])) {
+            $exists->andWhere(['customer_id' => $params['customer_id']]);
+        }
+
+        return $exists->exists();
+    }
 }
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index a01c6dd8..854817c2 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -180,9 +180,9 @@ public function getProductById(int $id): array
         return $this->shopify->Product($id)->get();
     }
 
-    public function getOrdersList(array $params = []): array
+    public function getOrdersList(): array
     {
-        return $this->shopify->Order->get($params);
+        return $this->shopify->Order->get($this->getRequestParamsForOrders());
     }
 
     public function getOrderById(int $id): array
@@ -200,13 +200,42 @@ public function getCustomerAddressById(int $customerId, int $addressId): array
         return $this->shopify->Customer($customerId)->Address($addressId)->get();
     }
 
+    /**
+     * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any
+     * @return array
+     */
+    protected function getRequestParamsForOrders(): array
+    {
+        $params = [
+            'limit' => 250,
+        ];
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('order_statuses')) {
+            $params['status'] = implode(',', $this->ecommerceIntegration->array_meta_data['order_statuses']);
+        }
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
+            $params['financial_status'] = implode(',', $this->ecommerceIntegration->array_meta_data['financial_statuses']);
+        }
+
+        if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
+            $params['fulfillment_status'] = implode(',', $this->ecommerceIntegration->array_meta_data['fulfillment_statuses']);
+        }
+
+        return $params;
+    }
+
     ##################
     # Order parsing: #
     ##################
 
     public function parseRawOrderJob(array $order): void
     {
-        if ($this->isNotDuplicate($order)) {
+        if (!CreateOrderService::isOrderExists([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'uuid' => (string)$order['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])) {
             Yii::$app->queue->push(
                 new ParseShopifyOrderJob([
                     'rawOrder' => $order,
@@ -215,13 +244,4 @@ public function parseRawOrderJob(array $order): void
             );
         }
     }
-
-    protected function isNotDuplicate(array $order): bool
-    {
-        return !Order::find()->where([
-            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'uuid' => (string)$order['id'],
-            'customer_id' => $this->ecommerceIntegration->customer_id,
-        ])->exists();
-    }
 }
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index c0d8dfc6..10325b83 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -3,13 +3,17 @@
 namespace console\controllers;
 
 use common\models\BulkAction;
+use common\models\EcommerceIntegration;
+use common\models\EcommercePlatform;
 use common\models\FulfillmentMeta;
 use common\models\Order;
 use common\models\ScheduledOrder;
+use common\services\platforms\ShopifyService;
 use console\jobs\orders\FetchJob;
 use console\jobs\orders\SendTo3PLJob;
 use yii\console\{Controller, ExitCode};
 use common\models\Integration;
+use yii\base\InvalidConfigException;
 use yii\db\Exception;
 
 // To create/edit crontab file: crontab -e
@@ -39,7 +43,7 @@ class CronController extends Controller
      * Action Index
      * @return int exit code
      */
-    public function actionIndex()
+    public function actionIndex(): int
     {
         $this->stdout('Yes, service cron is running');
         return ExitCode::OK;
@@ -49,8 +53,9 @@ public function actionIndex()
      * Action Frequent
      * Called every five minutes
      * @return int exit code
+     * @throws InvalidConfigException
      */
-    public function actionFrequent()
+    public function actionFrequent(): int
     {
         /**
          * 1. Loop through customers and customer meta data to find ecommerce site
@@ -67,9 +72,46 @@ public function actionFrequent()
         $this->runIntegrations(Integration::ACTIVE);
         $this->runScheduledOrders();
 
+        $this->runEcommerceIntegrations();
+
         return ExitCode::OK;
     }
 
+    /**
+     * @throws InvalidConfigException
+     */
+    protected function runEcommerceIntegrations(): void
+    {
+        $ecommerceIntegrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
+            switch ($ecommerceIntegration->ecommercePlatform->name) {
+
+                /**
+                 * Shopify:
+                 */
+                case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+
+                    $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
+
+                    if ($accessToken) {
+                        $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
+                        $orders = $shopifyService->getOrdersList();
+
+                        foreach ($orders as $order) {
+                            $shopifyService->parseRawOrderJob($order);
+                        }
+                    }
+
+                    break;
+
+            }
+        }
+    }
+
     public function runIntegrations($status)
     {
         /** @var Integration $integration */
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/ParseShopifyOrderJob.php
index ce4b65bd..35975089 100644
--- a/console/jobs/platforms/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/ParseShopifyOrderJob.php
@@ -36,17 +36,25 @@ class ParseShopifyOrderJob extends BaseObject implements RetryableJobInterface
     public function execute($queue): void
     {
         $this->setEcommerceIntegration();
-        $parsingErrors = $this->getParsingErrors();
 
-        if (!$parsingErrors) {
-            $this->parseOrderData();
-            $this->parseAddressData();
-            $this->parseItemsData();
-            $order = $this->saveOrder();
-
-            EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
-        } else {
-            EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
+        // Parse the order only if it doesn't exist in our table:
+        if (!CreateOrderService::isOrderExists([
+            'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
+            'uuid' => (string)$this->rawOrder['id'],
+            'customer_id' => $this->ecommerceIntegration->customer_id,
+        ])) {
+            $parsingErrors = $this->getParsingErrors();
+
+            if (!$parsingErrors) {
+                $this->parseOrderData();
+                $this->parseAddressData();
+                $this->parseItemsData();
+                $order = $this->saveOrder();
+
+                EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+            } else {
+                EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
+            }
         }
     }
 
@@ -173,7 +181,7 @@ protected function parseItemsData(): void
         foreach ($this->rawOrder['line_items'] as $item) {
             $this->parsedItemsAttributes[] = [
                 'quantity' => $item['fulfillable_quantity'],
-                'sku' => ($item['sku']) ?: 'Not provided.',
+                'sku' => ($item['sku']) ?: 'Not provided',
                 'name' => $item['name'],
                 'uuid' => (string)$item['id'],
             ];

From 39f776b6802df656c97927591f877681ee9a5008 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:25:36 +0200
Subject: [PATCH 50/81] Updated docs

---
 common/services/platforms/ShopifyService.php |  2 --
 intro-docs/ecommerce-platfroms.md            | 30 ++++++++++++++++----
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 854817c2..c5ebcd94 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,9 +2,7 @@
 
 namespace common\services\platforms;
 
-use common\models\EcommerceOrderLog;
 use Yii;
-use common\models\Order;
 use yii\base\InvalidConfigException;
 use yii\helpers\Json;
 use yii\helpers\Url;
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index b0dc92a8..4be6b583 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -7,19 +7,37 @@
 2. Implement a new `insert` query for the table `ecommerce_platform`. Specify the needed platform. Example - `console\migrations\m230221_134343_add_shopify_mock_ecommerce_platform.php`.
 
 3. Add implementation to:
-- `common\models\EcommercePlatform.php`
-- `common\models\EcommerceIntegration.php`
-- `frontend\controllers\EcommercePlatformController.php`
-- `frontend\controllers\EcommerceIntegrationController.php`
+- `common\models\EcommercePlatform`
+- `common\models\EcommerceIntegration`
+- `frontend\controllers\EcommercePlatformController`
+- `frontend\controllers\EcommerceIntegrationController`
+
+4. Create a Service similar to `common\services\platforms\ShopifyService`.
+
+5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
+
+### Cron:
+
+1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
+We need this method to pull existing orders from a needed E-commerce platform.
 
 ### How to manage existing platforms:
 
-1. Visit our website - `/ecommerce-platform` (you must be an `Admin`).
+1. Visit our website - URL: `/ecommerce-platform` (you must be an `Admin`).
 
 # Constraints
 
-1. A new integration can be added by a user only if the needed e-commerce platform
+1. A new integration (URL: `/ecommerce-integration`) can be added by a user only if the needed e-commerce platform
 has the status `Active`.
+   
+2. Try to use `common\services\platforms\CreateOrderService` when you parse raw orders from E-commerce platforms.
+
+3. In the cron method `runEcommerceIntegrations()`, only active (`status=connected`) E-commerce integrations are used for
+order pulling.
+   
+4. Once we cannot pull orders from an E-commerce platform like Shopify, the E-commerce integration must become `uninstalled` automatically.
+See `common\services\platforms\ShopifyService` -> `isTokenValid()` as an example. So we will not make any requests for the
+E-commerce integration until the user reconnects the shop (URL: `/ecommerce-integration`).
 
 # E-commerce Integrations - Shopify
 

From 1be29ba05bb55953c4c03ffb41828edf0180989e Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:29:33 +0200
Subject: [PATCH 51/81] Updated docs

---
 intro-docs/ecommerce-platfroms.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 4be6b583..80b46c35 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -38,6 +38,9 @@ order pulling.
 4. Once we cannot pull orders from an E-commerce platform like Shopify, the E-commerce integration must become `uninstalled` automatically.
 See `common\services\platforms\ShopifyService` -> `isTokenValid()` as an example. So we will not make any requests for the
 E-commerce integration until the user reconnects the shop (URL: `/ecommerce-integration`).
+   
+5. Each E-commerce platform-integration can have specific user-based settings (access token, specific order statuses, etc.). 
+For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceIntegration`.
 
 # E-commerce Integrations - Shopify
 

From 0cd8f3e96ad6658189f0089a163469c49e3800f5 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 7 Mar 2023 19:37:18 +0200
Subject: [PATCH 52/81] Removed test action

---
 .../EcommerceIntegrationController.php        | 45 -------------------
 intro-docs/ecommerce-platfroms.md             |  4 ++
 2 files changed, 4 insertions(+), 45 deletions(-)

diff --git a/frontend/controllers/EcommerceIntegrationController.php b/frontend/controllers/EcommerceIntegrationController.php
index 2e9e9f44..2355e0e1 100644
--- a/frontend/controllers/EcommerceIntegrationController.php
+++ b/frontend/controllers/EcommerceIntegrationController.php
@@ -38,51 +38,6 @@ public function behaviors(): array
         ];
     }
 
-    public function actionTest()
-    {
-        $integrations = EcommerceIntegration::find()
-            ->active()
-            ->orderById()
-            ->all();
-
-        foreach ($integrations as $integration) {
-            $accessToken = $integration->array_meta_data['access_token'];
-
-            if ($accessToken) {
-                $shopifyService = new ShopifyService($integration->array_meta_data['shop_url'], $integration);
-
-                /**
-                 * @see https://shopify.dev/docs/api/admin-rest/2022-10/resources/order#get-orders?status=any
-                 */
-                $params = [
-                    'limit' => 250,
-                ];
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('order_statuses')) {
-                    $params['status'] = implode(',', $integration->array_meta_data['order_statuses']);
-                }
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('financial_statuses')) {
-                    $params['financial_status'] = implode(',', $integration->array_meta_data['financial_statuses']);
-                }
-
-                if ($integration->isMetaKeyExistsAndNotEmpty('fulfillment_statuses')) {
-                    $params['fulfillment_status'] = implode(',', $integration->array_meta_data['fulfillment_statuses']);
-                }
-
-                $orders = $shopifyService->getOrdersList($params);
-
-//                echo '
';
-//                print_r($orders);
-//                exit;
-
-                foreach ($orders as $order) {
-                    $shopifyService->parseRawOrderJob($order);
-                }
-            }
-        }
-    }
-
     /**
      * Lists all EcommerceIntegration models for the current user.
      * @return string
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 80b46c35..377b072e 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -16,10 +16,14 @@
 
 5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
 
+> php yii queue/listen --verbose
+
 ### Cron:
 
 1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
 We need this method to pull existing orders from a needed E-commerce platform.
+   
+> php yii cron/frequent
 
 ### How to manage existing platforms:
 

From 0d529d79313df4eadcf303ac08923f35ea312ec8 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 9 Mar 2023 16:01:21 +0200
Subject: [PATCH 53/81] Register Shopify webhook listeners logic

---
 .../platforms/ConnectShopifyStoreForm.php     |   7 +-
 common/services/platforms/ShopifyService.php  |  91 ++++++++++++++-
 .../RegisterShopifyWebhookListenersJob.php    | 108 ++++++++++++++++++
 .../EcommerceWebhookController.php            |  67 +++++++++++
 intro-docs/ecommerce-platfroms.md             |   5 +
 5 files changed, 274 insertions(+), 4 deletions(-)
 create mode 100644 console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
 create mode 100644 frontend/controllers/EcommerceWebhookController.php

diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
index 916f3788..6384bd9d 100644
--- a/common/models/forms/platforms/ConnectShopifyStoreForm.php
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -131,7 +131,7 @@ public function auth(): void
      * @throws SdkException
      * @throws InvalidConfigException
      */
-    public function saveAccessToken(): void
+    public function saveAccessToken(bool $addWebHookListeners = true): void
     {
         $data = unserialize(Yii::$app->session->get('shopify_connection_second_step'));
 
@@ -142,6 +142,11 @@ public function saveAccessToken(): void
         // Step 2 - Receive and save access token:
         $shopifyService = new ShopifyService($this->url);
         $shopifyService->accessToken($data, $this->ecommerceIntegration);
+
+        // Add Webhook listeners:
+        if ($addWebHookListeners) {
+            $shopifyService->addWebhookListenersJob();
+        }
     }
 
     protected function saveDataForSecondStep()
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index c5ebcd94..39a2054e 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,6 +2,7 @@
 
 namespace common\services\platforms;
 
+use console\jobs\platforms\RegisterShopifyWebhookListenersJob;
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\Json;
@@ -23,6 +24,7 @@
  * @see https://shopify.dev/docs/api/usage/access-scopes
  * @see https://shopify.dev/docs/apps/webhooks
  * @see https://shopify.dev/docs/apps/webhooks/configuration
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook
  * @see https://github.com/phpclassic/php-shopify
  * @see https://community.shopify.com/c/shopify-apis-and-sdks/will-access-token-expired/td-p/559870
  */
@@ -61,6 +63,22 @@ class ShopifyService
         'unfulfilled' => 'Unfulfilled', // Returns orders with `fulfillment_status` of `null` or `partial`
     ];
 
+    public static string $webhooksUrl = '/ecommerce-webhook/shopify';
+
+    /**
+     * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics
+     */
+    public static array $webhookListeners = [
+        'orders/create',
+        'orders/cancelled',
+        'orders/updated',
+        'orders/delete',
+        'orders/fulfilled',
+        'orders/partially_fulfilled',
+        'orders/paid',
+        'app/uninstalled'
+    ];
+
     protected const API_VERSION = '2023-01';
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
@@ -146,6 +164,8 @@ public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegra
         if (!$ecommerceIntegration->save()) {
             throw new ServerErrorHttpException('Shopify integration is not added. Something went wrong.');
         }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
     }
 
     /**
@@ -168,6 +188,11 @@ protected function isTokenValid()
     # Get data via API: #
     #####################
 
+    public function getShop(): array
+    {
+        return $this->shopify->Shop->get();
+    }
+
     public function getProductsList(): array
     {
         return $this->shopify->Product->get();
@@ -223,9 +248,9 @@ protected function getRequestParamsForOrders(): array
         return $params;
     }
 
-    ##################
-    # Order parsing: #
-    ##################
+    #########
+    # Jobs: #
+    #########
 
     public function parseRawOrderJob(array $order): void
     {
@@ -242,4 +267,64 @@ public function parseRawOrderJob(array $order): void
             );
         }
     }
+
+    public function addWebhookListenersJob(): void
+    {
+        Yii::$app->queue->push(
+            new RegisterShopifyWebhookListenersJob([
+                'ecommerceIntegrationId' => $this->ecommerceIntegration->id
+            ])
+        );
+    }
+
+    #############
+    # Webhooks: #
+    #############
+
+    public function getWebhooksList(): array
+    {
+        return $this->shopify->Webhook()->get();
+    }
+
+    public function createWebhook($params): array
+    {
+        return $this->shopify->Webhook()->post($params);
+    }
+
+    public function getWebhookById(int $id): array
+    {
+        return $this->shopify->Webhook($id)->get();
+    }
+
+    public function deleteWebhookById(int $id): array
+    {
+        return $this->shopify->Webhook($id)->delete();
+    }
+
+//
+//    public function createWebhookOrderCreated()
+//    {
+//        $redirectDomain = trim(Url::to(['/'], true), '/');
+//
+//        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
+//            $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain'];
+//        }
+//
+////        $res = $this->shopify->Webhook()->post([
+////            'topic' => 'orders/updated',
+////            'address' => $redirectDomain . '/ecommerce-webhook/shopify?event=order_updated',
+////            'format' => 'json',
+////        ]);
+//
+//
+//
+////        $res = $this->shopify->Webhook(1266747506984)->get();
+// //       $res = $this->shopify->Webhook(1266742624552)->delete();
+//
+//        $res = $this->getWebhooksList();
+//
+//        echo '
';
+//        print_r($res);
+//        exit;
+//    }
 }
diff --git a/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php b/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
new file mode 100644
index 00000000..8e31f73b
--- /dev/null
+++ b/console/jobs/platforms/RegisterShopifyWebhookListenersJob.php
@@ -0,0 +1,108 @@
+setEcommerceIntegration();
+        $this->setShopifyService();
+        $this->setDomain();
+
+        // Register listeners:
+        foreach (ShopifyService::$webhookListeners as $listener) {
+            $this->sendRequest($listener);
+        }
+
+        // Update integration:
+        $this->updateIntegrationMetaData();
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceIntegration(): void
+    {
+        $ecommerceIntegration = EcommerceIntegration::findOne($this->ecommerceIntegrationId);
+
+        if (!$ecommerceIntegration) {
+            throw new NotFoundHttpException('E-commerce integration not found.');
+        }
+
+        $this->ecommerceIntegration = $ecommerceIntegration;
+    }
+
+    /**
+     * @throws InvalidConfigException
+     */
+    protected function setShopifyService(): void
+    {
+        $this->shopifyService = new ShopifyService($this->ecommerceIntegration->array_meta_data['shop_url'], $this->ecommerceIntegration);
+    }
+
+    protected function setDomain(): void
+    {
+        $this->domain = trim(Url::to(['/'], true), '/');
+
+        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
+            $this->domain = Yii::$app->params['shopify']['override_redirect_domain'];
+        }
+    }
+
+    protected function updateIntegrationMetaData(): void
+    {
+        $this->ecommerceIntegration->array_meta_data['connected_webhook_listeners'] = ShopifyService::$webhookListeners;
+        $this->ecommerceIntegration->meta = Json::encode($this->ecommerceIntegration->array_meta_data, JSON_PRETTY_PRINT);
+        $this->ecommerceIntegration->save();
+    }
+
+    protected function sendRequest(string $event): void
+    {
+        $res = $this->shopifyService->createWebhook([
+            'topic' => $event,
+            'address' => $this->domain . ShopifyService::$webhooksUrl . '?event=' . $event,
+            'format' => 'json',
+        ]);
+
+//        echo '
' . $event . ': ';
+//        print_r($res);
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
new file mode 100644
index 00000000..7b7be857
--- /dev/null
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -0,0 +1,67 @@
+enableCsrfValidation = false;
+        Yii::$app->response->format = Response::FORMAT_JSON;
+        return parent::beforeAction($action);
+    }
+
+    /**
+     * @return array
+     * @throws NotFoundHttpException
+     */
+    public function actionShopify(): array
+    {
+        $event = Yii::$app->request->get('event');
+
+        if (!in_array($event, ShopifyService::$webhookListeners)) {
+            throw new NotFoundHttpException('Event not found.');
+        }
+
+        return [
+            'result' => 'success',
+        ];
+    }
+
+    public function actionTest()
+    {
+        $ecommerceIntegrations = EcommerceIntegration::find()
+            ->active()
+            ->orderById()
+            ->all();
+
+        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
+            $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
+
+            if ($accessToken) {
+                $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
+                return $shopifyService->getWebhooksList();
+            }
+        }
+    }
+}
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 377b072e..1ebca6e5 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -46,6 +46,11 @@ E-commerce integration until the user reconnects the shop (URL: `/ecommerce-inte
 5. Each E-commerce platform-integration can have specific user-based settings (access token, specific order statuses, etc.). 
 For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceIntegration`.
 
+6. For storing webhook listeners list, also use the `meta` field.
+
+7. When you save orders to the table `orders`, use the field `uuid` for storing the Order ID of the original E-commerce platform.
+
+
 # E-commerce Integrations - Shopify
 
 ### App:

From 0ef08ef4c0eb7694c2a742c04d23479aabfce886 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 9 Mar 2023 18:12:27 +0200
Subject: [PATCH 54/81] E-commerce webhooks + MetaDataFieldTrait + updated docs

---
 common/models/EcommerceIntegration.php        | 18 +----
 common/models/EcommerceOrderLog.php           |  3 +
 common/models/EcommercePlatform.php           |  5 +-
 common/models/EcommerceWebhook.php            | 56 +++++++++++++++
 common/models/base/BaseEcommerceWebhook.php   | 71 +++++++++++++++++++
 .../platforms/ConnectShopifyStoreForm.php     |  9 +--
 common/services/platforms/ShopifyService.php  | 39 ++--------
 common/traits/MetaDataFieldTrait.php          | 26 +++++++
 ..._141820_create_table_ecommerce_webhook.php | 55 ++++++++++++++
 .../EcommerceWebhookController.php            | 61 +++++++++++++---
 intro-docs/ecommerce-platfroms.md             |  5 ++
 11 files changed, 280 insertions(+), 68 deletions(-)
 create mode 100644 common/models/EcommerceWebhook.php
 create mode 100644 common/models/base/BaseEcommerceWebhook.php
 create mode 100644 common/traits/MetaDataFieldTrait.php
 create mode 100644 console/migrations/m230309_141820_create_table_ecommerce_webhook.php

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index fe590942..2fd148c5 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -4,9 +4,9 @@
 
 use Yii;
 use yii\helpers\Html;
-use yii\helpers\Json;
 use common\models\base\BaseEcommerceIntegration;
 use console\jobs\NotificationJob;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommerceIntegration
@@ -14,12 +14,12 @@
  */
 class EcommerceIntegration extends BaseEcommerceIntegration
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_INTEGRATION_CONNECTED = 1;
     public const STATUS_INTEGRATION_PAUSED = 0;
     public const STATUS_INTEGRATION_UNINSTALLED = -1;
 
-    public array $array_meta_data = [];
-
     public function init(): void
     {
         parent::init();
@@ -90,16 +90,4 @@ public function resume(): bool
         $this->status = self::STATUS_INTEGRATION_CONNECTED;
         return $this->save();
     }
-
-    public function isMetaKeyExistsAndNotEmpty(string $key): bool
-    {
-        return (isset($this->array_meta_data[$key]) && !empty($this->array_meta_data[$key]));
-    }
-
-    protected function convertMetaData(): void
-    {
-        if ($this->meta) {
-            $this->array_meta_data = Json::decode($this->meta);
-        }
-    }
 }
diff --git a/common/models/EcommerceOrderLog.php b/common/models/EcommerceOrderLog.php
index 5ef65f44..131659a0 100644
--- a/common/models/EcommerceOrderLog.php
+++ b/common/models/EcommerceOrderLog.php
@@ -4,6 +4,7 @@
 
 use yii\helpers\Json;
 use common\models\base\BaseEcommerceOrderLog;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommerceOrderLog
@@ -11,6 +12,8 @@
  */
 class EcommerceOrderLog extends BaseEcommerceOrderLog
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_SUCCESS = 'success';
     public const STATUS_FAILED = 'failed';
 
diff --git a/common/models/EcommercePlatform.php b/common/models/EcommercePlatform.php
index 6895fa9c..7854a88e 100644
--- a/common/models/EcommercePlatform.php
+++ b/common/models/EcommercePlatform.php
@@ -2,8 +2,9 @@
 
 namespace common\models;
 
-use common\models\base\BaseEcommercePlatform;
 use yii\db\ActiveQuery;
+use common\models\base\BaseEcommercePlatform;
+use common\traits\MetaDataFieldTrait;
 
 /**
  * Class EcommercePlatform
@@ -11,6 +12,8 @@
  */
 class EcommercePlatform extends BaseEcommercePlatform
 {
+    use MetaDataFieldTrait;
+
     public const STATUS_PLATFORM_ACTIVE = 1;
     public const STATUS_PLATFORM_INACTIVE = 0;
 
diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
new file mode 100644
index 00000000..53dbbf8d
--- /dev/null
+++ b/common/models/EcommerceWebhook.php
@@ -0,0 +1,56 @@
+on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']);
+    }
+
+    public static function getStatuses(): array
+    {
+        return [
+            self::STATUS_RECEIVED => 'Received',
+            self::STATUS_PROCESSING => 'Processing',
+            self::STATUS_SUCCESS => 'Success',
+            self::STATUS_FAILED => 'Failed',
+        ];
+    }
+
+    public function isReceived(): bool
+    {
+        return $this->status === self::STATUS_RECEIVED;
+    }
+
+    public function isProcessing(): bool
+    {
+        return $this->status === self::STATUS_PROCESSING;
+    }
+
+    public function isSuccess(): bool
+    {
+        return $this->status === self::STATUS_SUCCESS;
+    }
+
+    public function isFailed(): bool
+    {
+        return $this->status === self::STATUS_FAILED;
+    }
+}
diff --git a/common/models/base/BaseEcommerceWebhook.php b/common/models/base/BaseEcommerceWebhook.php
new file mode 100644
index 00000000..eba91ed1
--- /dev/null
+++ b/common/models/base/BaseEcommerceWebhook.php
@@ -0,0 +1,71 @@
+ 64],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels(): array
+    {
+        return [
+            'id' => 'ID',
+            'platform_id' => 'Platform ID',
+            'status' => 'Status',
+            'event' => 'Event',
+            'payload' => 'Payload',
+            'meta' => 'Meta',
+            'created_date' => 'Created Date',
+            'updated_date' => 'Updated Date',
+        ];
+    }
+
+    /**
+     * @return ActiveQuery
+     */
+    public function getPlatform(): ActiveQuery
+    {
+        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+    }
+}
diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
index 6384bd9d..07ed3670 100644
--- a/common/models/forms/platforms/ConnectShopifyStoreForm.php
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -3,15 +3,12 @@
 namespace common\models\forms\platforms;
 
 use Yii;
-use yii\base\InvalidConfigException;
-use yii\base\Model;
+use yii\base\{InvalidConfigException, Model};
 use yii\db\Expression;
 use PHPShopify\Exception\SdkException;
 use yii\web\ServerErrorHttpException;
 use common\services\platforms\ShopifyService;
-use common\models\EcommerceIntegration;
-use common\models\EcommercePlatform;
-use common\models\Customer;
+use common\models\{EcommerceIntegration, EcommercePlatform, Customer};
 
 /**
  * Class ConnectShopifyStoreForm
@@ -149,7 +146,7 @@ public function saveAccessToken(bool $addWebHookListeners = true): void
         }
     }
 
-    protected function saveDataForSecondStep()
+    protected function saveDataForSecondStep(): void
     {
         $data = [
             'shop_name' => $this->name,
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 39a2054e..c2e064af 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -2,16 +2,12 @@
 
 namespace common\services\platforms;
 
-use console\jobs\platforms\RegisterShopifyWebhookListenersJob;
 use Yii;
 use yii\base\InvalidConfigException;
-use yii\helpers\Json;
-use yii\helpers\Url;
-use console\jobs\platforms\ParseShopifyOrderJob;
-use common\models\EcommerceIntegration;
-use common\models\EcommercePlatform;
-use PHPShopify\ShopifySDK;
-use PHPShopify\AuthHelper;
+use yii\helpers\{Json, Url};
+use console\jobs\platforms\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use common\models\{EcommerceIntegration, EcommercePlatform};
+use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
 use PHPShopify\Exception\SdkException;
 
@@ -300,31 +296,4 @@ public function deleteWebhookById(int $id): array
     {
         return $this->shopify->Webhook($id)->delete();
     }
-
-//
-//    public function createWebhookOrderCreated()
-//    {
-//        $redirectDomain = trim(Url::to(['/'], true), '/');
-//
-//        if (Yii::$app->params['shopify']['override_redirect_domain'] != false) {
-//            $redirectDomain = Yii::$app->params['shopify']['override_redirect_domain'];
-//        }
-//
-////        $res = $this->shopify->Webhook()->post([
-////            'topic' => 'orders/updated',
-////            'address' => $redirectDomain . '/ecommerce-webhook/shopify?event=order_updated',
-////            'format' => 'json',
-////        ]);
-//
-//
-//
-////        $res = $this->shopify->Webhook(1266747506984)->get();
-// //       $res = $this->shopify->Webhook(1266742624552)->delete();
-//
-//        $res = $this->getWebhooksList();
-//
-//        echo '
';
-//        print_r($res);
-//        exit;
-//    }
 }
diff --git a/common/traits/MetaDataFieldTrait.php b/common/traits/MetaDataFieldTrait.php
new file mode 100644
index 00000000..bc74a932
--- /dev/null
+++ b/common/traits/MetaDataFieldTrait.php
@@ -0,0 +1,26 @@
+array_meta_data[$key]) && !empty($this->array_meta_data[$key]));
+    }
+
+    protected function convertMetaData(): void
+    {
+        if ($this->meta) {
+            $this->array_meta_data = Json::decode($this->meta);
+        }
+    }
+}
diff --git a/console/migrations/m230309_141820_create_table_ecommerce_webhook.php b/console/migrations/m230309_141820_create_table_ecommerce_webhook.php
new file mode 100644
index 00000000..9e7fd28f
--- /dev/null
+++ b/console/migrations/m230309_141820_create_table_ecommerce_webhook.php
@@ -0,0 +1,55 @@
+execute("
+            CREATE TABLE `ecommerce_webhook` (
+              `id` int NOT NULL AUTO_INCREMENT,
+              `platform_id` int NOT NULL,
+              `status` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+              `event` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+              `payload` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+                `meta` MEDIUMTEXT NULL DEFAULT NULL, 
+                `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 
+                `updated_date` DATETIME NULL DEFAULT NULL,
+              PRIMARY KEY (`id`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+        ");
+
+        $this->execute("
+            ALTER TABLE `ecommerce_webhook` 
+                CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
+        ");
+
+        $this->execute("
+            ALTER TABLE `ecommerce_webhook` ADD INDEX(`status`);
+        ");
+
+        $this->addForeignKey(
+            '{{%fk-ecommerce_webhook-platform_id}}',
+            '{{%ecommerce_webhook}}',
+            'platform_id',
+            '{{%ecommerce_platform}}',
+            'id',
+            'CASCADE'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function safeDown()
+    {
+        $this->dropTable("{{%ecommerce_webhook}}");
+    }
+}
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 7b7be857..7a093109 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -2,18 +2,10 @@
 
 namespace frontend\controllers;
 
-use common\models\EcommerceIntegration;
-use common\services\platforms\ShopifyService;
 use Yii;
-use yii\filters\VerbFilter;
-use yii\web\BadRequestHttpException;
-use yii\web\Response;
-use Da\User\Filter\AccessRuleFilter;
-use common\models\EcommercePlatform;
-use common\models\search\EcommercePlatformSearch;
-use yii\filters\AccessControl;
-use yii\web\Controller;
-use yii\web\NotFoundHttpException;
+use yii\web\{BadRequestHttpException, Response, Controller, NotFoundHttpException, ServerErrorHttpException};
+use common\models\{EcommercePlatform, EcommerceWebhook, EcommerceIntegration};
+use common\services\platforms\ShopifyService;
 
 /**
  * Class EcommerceWebhookController
@@ -32,22 +24,69 @@ public function beforeAction($action): bool
     }
 
     /**
+     * Receives and saves webhooks from the Shopify e-commerce platform.
      * @return array
      * @throws NotFoundHttpException
+     * @throws ServerErrorHttpException
      */
     public function actionShopify(): array
     {
+        // file_put_contents('shopify.txt', "\r\n\r\n\r\n" . Yii::$app->request->rawBody, FILE_APPEND);
+
+
+        $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
         if (!in_array($event, ShopifyService::$webhookListeners)) {
             throw new NotFoundHttpException('Event not found.');
         }
 
+        $ecommerceWebhook = $this->getNewEcommerceWebhookObject($ecommercePlatform->id);
+        $ecommerceWebhook->event = $event;
+        $ecommerceWebhook->payload = Yii::$app->request->rawBody;
+
+        if (!$ecommerceWebhook->save()) {
+            throw new ServerErrorHttpException('Event not saved.');
+        }
+
         return [
             'result' => 'success',
         ];
     }
 
+    /**
+     * @throws NotFoundHttpException
+     * @throws ServerErrorHttpException
+     */
+    protected function getEcommercePlatformByName(string $name): EcommercePlatform
+    {
+        /**
+         * @var EcommercePlatform $model
+         */
+        $model = EcommercePlatform::find()
+            ->where(['name' => $name])
+            ->one();
+
+        if (!$model) {
+            throw new NotFoundHttpException('Ecommerce platform does not exist.');
+        }
+
+        if (!$model->isActive()) {
+            throw new ServerErrorHttpException('Ecommerce platform is not active.');
+        }
+
+        return $model;
+    }
+
+    protected function getNewEcommerceWebhookObject(int $ecommercePlatformId): EcommerceWebhook
+    {
+        $ecommerceWebhook = new EcommerceWebhook();
+        $ecommerceWebhook->platform_id = $ecommercePlatformId;
+        $ecommerceWebhook->status = EcommerceWebhook::STATUS_RECEIVED;
+
+        return $ecommerceWebhook;
+    }
+
     public function actionTest()
     {
         $ecommerceIntegrations = EcommerceIntegration::find()
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 1ebca6e5..0d3344b3 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -11,6 +11,7 @@
 - `common\models\EcommerceIntegration`
 - `frontend\controllers\EcommercePlatformController`
 - `frontend\controllers\EcommerceIntegrationController`
+- `frontend\controllers\EcommerceWebhookController`
 
 4. Create a Service similar to `common\services\platforms\ShopifyService`.
 
@@ -50,6 +51,10 @@ For this, use the `meta` attribute (JSON) of the model `common\models\EcommerceI
 
 7. When you save orders to the table `orders`, use the field `uuid` for storing the Order ID of the original E-commerce platform.
 
+8. Use the controller `frontend\controllers\EcommerceWebhookController` for accepting webhook requests from E-commerce platforms.
+
+9. Don't accept webhooks if our E-commerce Platform `is not active`. See `frontend\controllers\EcommerceWebhookController` -> `getEcommercePlatformByName()`. 
+
 
 # E-commerce Integrations - Shopify
 

From 912b868a810aed91dbb4cc46c7f57d16158a5e53 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Fri, 10 Mar 2023 14:50:28 +0200
Subject: [PATCH 55/81] Mandatory webhooks + docs updated

---
 common/services/platforms/ShopifyService.php  |  9 ++++++++
 .../EcommerceWebhookController.php            |  4 +++-
 intro-docs/ecommerce-platfroms.md             | 22 +++++++++++++++++++
 3 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index c2e064af..b8e2c05b 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -75,6 +75,15 @@ class ShopifyService
         'app/uninstalled'
     ];
 
+    /**
+     * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks
+     */
+    public static array $mandatoryWebhookListeners = [
+        'customers/data_request',
+        'customers/redact',
+        'shop/redact',
+    ];
+
     protected const API_VERSION = '2023-01';
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 7a093109..4b870b3c 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -37,7 +37,8 @@ public function actionShopify(): array
         $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
-        if (!in_array($event, ShopifyService::$webhookListeners)) {
+        if (!in_array($event, ShopifyService::$webhookListeners) &&
+            !in_array($event, ShopifyService::$mandatoryWebhookListeners)) {
             throw new NotFoundHttpException('Event not found.');
         }
 
@@ -50,6 +51,7 @@ public function actionShopify(): array
         }
 
         return [
+            'status' => 200,
             'result' => 'success',
         ];
     }
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 0d3344b3..bbcb0676 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -98,3 +98,25 @@ In `Data and configurations`, choose `Start with test data`.
 2. In the form, specify your test shop's details (`name` and `URL`).
 
 3. If you want to remove the Shopify app from your test shop, visit `https://admin.shopify.com/` -> `Apps` -> `Apps and sales channels` -> and press `Uninstall`.
+
+### Important URLs:
+
+*Common:*
+
+- https://partners.shopify.com/
+- https://github.com/phpclassic/php-shopify
+  
+*API:*
+
+- https://shopify.dev/docs/apps/auth/oauth/getting-started
+- https://shopify.dev/docs/api/usage/access-scopes
+- https://community.shopify.com/c/shopify-apis-and-sdks/will-access-token-expired/td-p/559870
+- https://shopify.dev/docs/apps/store/data-protection/protected-customer-data
+
+*Webhooks:*
+
+- https://shopify.dev/docs/apps/webhooks
+- https://shopify.dev/docs/apps/webhooks/configuration
+- https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook
+- https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics
+- https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks

From 565ac2a2972a8f45f32ebb957080e3f18416865f Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Fri, 10 Mar 2023 20:09:22 +0200
Subject: [PATCH 56/81] Cron -> runEcommerceWebhooks(), EcommerceWebhook ->
 statuses, BaseWebhookProcessingJob, initial Jobs for Shopify webhooks
 processing

---
 common/models/EcommerceWebhook.php            | 110 ++++++++++++++++++
 .../models/base/BaseEcommerceIntegration.php  |   6 +-
 common/models/base/BaseEcommerceOrderLog.php  |  19 ++-
 common/models/base/BaseEcommerceWebhook.php   |  13 ++-
 common/models/query/EcommerceWebhookQuery.php |  29 +++++
 common/services/platforms/ShopifyService.php  |   2 +-
 console/controllers/CronController.php        |  24 ++++
 .../{ => shopify}/ParseShopifyOrderJob.php    |  20 ++--
 .../RegisterShopifyWebhookListenersJob.php    |  10 +-
 .../webhooks/BaseWebhookProcessingJob.php     |  73 ++++++++++++
 .../shopify/ShopifyAppUninstalledJob.php      |  18 +++
 .../shopify/ShopifyCustomerDataRequestJob.php |  18 +++
 .../shopify/ShopifyCustomerRedactJob.php      |  18 +++
 .../shopify/ShopifyOrderCancelledJob.php      |  18 +++
 .../shopify/ShopifyOrderCreatedJob.php        |  18 +++
 .../shopify/ShopifyOrderDeletedJob.php        |  18 +++
 .../shopify/ShopifyOrderFulfilledJob.php      |  18 +++
 .../webhooks/shopify/ShopifyOrderPaidJob.php  |  18 +++
 .../ShopifyOrderPartiallyFulfilledJob.php     |  18 +++
 .../shopify/ShopifyOrderUpdatedJob.php        |  18 +++
 .../webhooks/shopify/ShopifyShopRedactJob.php |  18 +++
 21 files changed, 470 insertions(+), 34 deletions(-)
 create mode 100644 common/models/query/EcommerceWebhookQuery.php
 rename console/jobs/platforms/{ => shopify}/ParseShopifyOrderJob.php (95%)
 rename console/jobs/platforms/{ => shopify}/RegisterShopifyWebhookListenersJob.php (94%)
 create mode 100644 console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php

diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
index 53dbbf8d..6c4f8abc 100644
--- a/common/models/EcommerceWebhook.php
+++ b/common/models/EcommerceWebhook.php
@@ -2,8 +2,21 @@
 
 namespace common\models;
 
+use Yii;
 use common\models\base\BaseEcommerceWebhook;
 use common\traits\MetaDataFieldTrait;
+use common\services\platforms\ShopifyService;
+use console\jobs\platforms\webhooks\shopify\{ShopifyAppUninstalledJob,
+    ShopifyCustomerDataRequestJob,
+    ShopifyCustomerRedactJob,
+    ShopifyOrderCancelledJob,
+    ShopifyOrderCreatedJob,
+    ShopifyOrderDeletedJob,
+    ShopifyOrderFulfilledJob,
+    ShopifyOrderPaidJob,
+    ShopifyOrderPartiallyFulfilledJob,
+    ShopifyOrderUpdatedJob,
+    ShopifyShopRedactJob};
 
 /**
  * Class EcommerceWebhook
@@ -24,6 +37,10 @@ public function init(): void
         $this->on(self::EVENT_AFTER_FIND, [$this, 'convertMetaData']);
     }
 
+    #############
+    # Statuses: #
+    #############
+
     public static function getStatuses(): array
     {
         return [
@@ -53,4 +70,97 @@ public function isFailed(): bool
     {
         return $this->status === self::STATUS_FAILED;
     }
+
+    public function setReceived(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_RECEIVED;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setProcessing(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_PROCESSING;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setSuccess(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_SUCCESS;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    public function setFailed(bool $withSave = true): void
+    {
+        $this->status = self::STATUS_FAILED;
+
+        if ($withSave) {
+            $this->save();
+        }
+    }
+
+    #########
+    # Jobs: #
+    #########
+
+    public function createJob(): bool
+    {
+        if ($this->isReceived() && $this->isJobExecutable()) {
+            switch ($this->platform->name) {
+                case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+                    $this->createJobForShopify();
+                    break;
+            }
+
+            //$this->setProcessing();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function isJobExecutable(): bool
+    {
+        // All webhook events from all E-commerce platforms:
+        $availableEvents = array_merge(
+            ShopifyService::$webhookListeners,
+            ShopifyService::$mandatoryWebhookListeners
+        );
+
+        if ($this->platform->isActive() && in_array($this->event, $availableEvents) && $this->payload) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function createJobForShopify()
+    {
+        $job = match ($this->event) {
+            'orders/create' => new ShopifyOrderCreatedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/cancelled' => new ShopifyOrderCancelledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/updated' => new ShopifyOrderUpdatedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/delete' => new ShopifyOrderDeletedJob(['ecommerceWebhookId' => $this->id]),
+            'orders/fulfilled' => new ShopifyOrderFulfilledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/partially_fulfilled' => new ShopifyOrderPartiallyFulfilledJob(['ecommerceWebhookId' => $this->id]),
+            'orders/paid' => new ShopifyOrderPaidJob(['ecommerceWebhookId' => $this->id]),
+            'app/uninstalled' => new ShopifyAppUninstalledJob(['ecommerceWebhookId' => $this->id]),
+            'customers/data_request' => new ShopifyCustomerDataRequestJob(['ecommerceWebhookId' => $this->id]),
+            'customers/redact' => new ShopifyCustomerRedactJob(['ecommerceWebhookId' => $this->id]),
+            'shop/redact' => new ShopifyShopRedactJob(['ecommerceWebhookId' => $this->id]),
+        };
+
+        if (isset($job)) {
+            Yii::$app->queue->push($job);
+        }
+    }
 }
diff --git a/common/models/base/BaseEcommerceIntegration.php b/common/models/base/BaseEcommerceIntegration.php
index c4de3a64..cb211e84 100644
--- a/common/models/base/BaseEcommerceIntegration.php
+++ b/common/models/base/BaseEcommerceIntegration.php
@@ -2,11 +2,9 @@
 
 namespace common\models\base;
 
-use yii\db\ActiveRecord;
-use yii\db\ActiveQuery;
+use yii\db\{ActiveRecord, ActiveQuery};
+use common\models\{EcommercePlatform, Customer};
 use common\models\query\EcommerceIntegrationQuery;
-use common\models\EcommercePlatform;
-use common\models\Customer;
 use frontend\models\User;
 
 /**
diff --git a/common/models/base/BaseEcommerceOrderLog.php b/common/models/base/BaseEcommerceOrderLog.php
index d6a49bdb..a37bc58b 100644
--- a/common/models/base/BaseEcommerceOrderLog.php
+++ b/common/models/base/BaseEcommerceOrderLog.php
@@ -3,11 +3,8 @@
 namespace common\models\base;
 
 use Yii;
-use yii\db\ActiveQuery;
-use yii\db\ActiveRecord;
-use common\models\EcommercePlatform;
-use common\models\EcommerceIntegration;
-use common\models\Order;
+use yii\db\{ActiveQuery, ActiveRecord};
+use common\models\{EcommercePlatform, EcommerceIntegration, Order};
 
 /**
  * This is the model class for table "ecommerce_order_log".
@@ -50,9 +47,9 @@ public function rules(): array
             [['created_date', 'updated_date'], 'safe'],
             [['status'], 'string', 'max' => 64],
             [['original_order_id'], 'string', 'max' => 256],
-            [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::className(), 'targetAttribute' => ['integration_id' => 'id']],
-            [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::className(), 'targetAttribute' => ['internal_order_id' => 'id']],
-            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+            [['integration_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommerceIntegration::class, 'targetAttribute' => ['integration_id' => 'id']],
+            [['internal_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => Order::class, 'targetAttribute' => ['internal_order_id' => 'id']],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::class, 'targetAttribute' => ['platform_id' => 'id']],
         ];
     }
 
@@ -80,7 +77,7 @@ public function attributeLabels(): array
      */
     public function getIntegration(): ActiveQuery
     {
-        return $this->hasOne(EcommerceIntegration::className(), ['id' => 'integration_id']);
+        return $this->hasOne(EcommerceIntegration::class, ['id' => 'integration_id']);
     }
 
     /**
@@ -88,7 +85,7 @@ public function getIntegration(): ActiveQuery
      */
     public function getInternalOrder(): ActiveQuery
     {
-        return $this->hasOne(Order::className(), ['id' => 'internal_order_id']);
+        return $this->hasOne(Order::class, ['id' => 'internal_order_id']);
     }
 
     /**
@@ -96,6 +93,6 @@ public function getInternalOrder(): ActiveQuery
      */
     public function getPlatform(): ActiveQuery
     {
-        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+        return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']);
     }
 }
diff --git a/common/models/base/BaseEcommerceWebhook.php b/common/models/base/BaseEcommerceWebhook.php
index eba91ed1..c8042d77 100644
--- a/common/models/base/BaseEcommerceWebhook.php
+++ b/common/models/base/BaseEcommerceWebhook.php
@@ -4,6 +4,7 @@
 
 use yii\db\{ActiveQuery, ActiveRecord};
 use common\models\{EcommercePlatform};
+use common\models\query\EcommerceWebhookQuery;
 
 /**
  * This is the model class for table "ecommerce_webhook".
@@ -21,6 +22,14 @@
  */
 class BaseEcommerceWebhook extends ActiveRecord
 {
+    /**
+     * @return EcommerceWebhookQuery
+     */
+    public static function find(): EcommerceWebhookQuery
+    {
+        return new EcommerceWebhookQuery(get_called_class());
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -40,7 +49,7 @@ public function rules(): array
             [['payload', 'meta'], 'string'],
             [['created_date', 'updated_date'], 'safe'],
             [['status', 'event'], 'string', 'max' => 64],
-            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::className(), 'targetAttribute' => ['platform_id' => 'id']],
+            [['platform_id'], 'exist', 'skipOnError' => true, 'targetClass' => EcommercePlatform::class, 'targetAttribute' => ['platform_id' => 'id']],
         ];
     }
 
@@ -66,6 +75,6 @@ public function attributeLabels(): array
      */
     public function getPlatform(): ActiveQuery
     {
-        return $this->hasOne(EcommercePlatform::className(), ['id' => 'platform_id']);
+        return $this->hasOne(EcommercePlatform::class, ['id' => 'platform_id']);
     }
 }
diff --git a/common/models/query/EcommerceWebhookQuery.php b/common/models/query/EcommerceWebhookQuery.php
new file mode 100644
index 00000000..f1811973
--- /dev/null
+++ b/common/models/query/EcommerceWebhookQuery.php
@@ -0,0 +1,29 @@
+andWhere(['status' => EcommerceWebhook::STATUS_RECEIVED]);
+    }
+
+    public function forPlatformId(int $platformId): EcommerceWebhookQuery
+    {
+        $this->andWhere(['platform_id' => $platformId]);
+        return $this;
+    }
+
+    public function orderById(int $sort = SORT_ASC): EcommerceWebhookQuery
+    {
+        return $this->orderBy(['id' => $sort]);
+    }
+}
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index b8e2c05b..225c5315 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -5,7 +5,7 @@
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\{Json, Url};
-use console\jobs\platforms\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use console\jobs\platforms\shopify\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
 use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index 10325b83..b597207a 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -5,6 +5,7 @@
 use common\models\BulkAction;
 use common\models\EcommerceIntegration;
 use common\models\EcommercePlatform;
+use common\models\EcommerceWebhook;
 use common\models\FulfillmentMeta;
 use common\models\Order;
 use common\models\ScheduledOrder;
@@ -73,11 +74,14 @@ public function actionFrequent(): int
         $this->runScheduledOrders();
 
         $this->runEcommerceIntegrations();
+        $this->runEcommerceWebhooks();
 
         return ExitCode::OK;
     }
 
     /**
+     * This method is used for pulling first (initial) raw orders from E-commerce platforms like Shopify.
+     * For working with webhooks, the method `runEcommerceWebhooks()` is used.
      * @throws InvalidConfigException
      */
     protected function runEcommerceIntegrations(): void
@@ -85,6 +89,7 @@ protected function runEcommerceIntegrations(): void
         $ecommerceIntegrations = EcommerceIntegration::find()
             ->active()
             ->orderById()
+            ->limit(100)
             ->all();
 
         foreach ($ecommerceIntegrations as $ecommerceIntegration) {
@@ -112,6 +117,25 @@ protected function runEcommerceIntegrations(): void
         }
     }
 
+    /**
+     * This method is used for creating Jobs for webhooks with the status "received".
+     */
+    protected function runEcommerceWebhooks(): void
+    {
+        $ecommerceWebhooks = EcommerceWebhook::find()
+            ->received()
+            ->orderById()
+            ->limit(100)
+            ->all();
+
+        /**
+         * @var $ecommerceWebhook EcommerceWebhook
+         */
+        foreach ($ecommerceWebhooks as $ecommerceWebhook) {
+            $ecommerceWebhook->createJob();
+        }
+    }
+
     public function runIntegrations($status)
     {
         /** @var Integration $integration */
diff --git a/console/jobs/platforms/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
similarity index 95%
rename from console/jobs/platforms/ParseShopifyOrderJob.php
rename to console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 35975089..da01cbd3 100644
--- a/console/jobs/platforms/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -1,14 +1,14 @@
 setEcommerceWebhook();
+
+        if ($this->isPayloadJson()) {
+            $this->arrayPayload = Json::decode($this->ecommerceWebhook->payload);
+        }
+    }
+
+    /**
+     * @throws NotFoundHttpException
+     */
+    protected function setEcommerceWebhook(): void
+    {
+        $ecommerceWebhook = EcommerceWebhook::findOne($this->ecommerceWebhookId);
+
+        if (!$ecommerceWebhook) {
+            throw new NotFoundHttpException('E-commerce webhook not found.');
+        }
+
+        $this->ecommerceWebhook = $ecommerceWebhook;
+    }
+
+    /**
+     * Tries to find our internal Order by provided external ID. External ID = uuid.
+     * Use this method for webhook events like "order update/delete/cancel".
+     * @param int $id
+     * @return Order|null
+     */
+    protected function getOrderByExternalId(int $id): Order|null
+    {
+        return Order::find()->where(['uuid' => $id])->one();
+    }
+
+    protected function isPayloadJson(): bool
+    {
+        json_decode($this->ecommerceWebhook->payload);
+        return json_last_error() === JSON_ERROR_NONE;
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
new file mode 100644
index 00000000..9b017e24
--- /dev/null
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -0,0 +1,18 @@
+
Date: Mon, 13 Mar 2023 14:38:49 +0200
Subject: [PATCH 57/81] Dummy webhook status update

---
 common/models/EcommerceWebhook.php                              | 2 --
 console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php    | 1 +
 .../platforms/webhooks/shopify/ShopifyAppUninstalledJob.php     | 1 +
 .../webhooks/shopify/ShopifyCustomerDataRequestJob.php          | 1 +
 .../platforms/webhooks/shopify/ShopifyCustomerRedactJob.php     | 1 +
 .../platforms/webhooks/shopify/ShopifyOrderCancelledJob.php     | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php  | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php  | 1 +
 .../platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php     | 1 +
 console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php | 1 +
 .../webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php      | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php  | 1 +
 .../jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php    | 1 +
 13 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
index 6c4f8abc..c336d2c7 100644
--- a/common/models/EcommerceWebhook.php
+++ b/common/models/EcommerceWebhook.php
@@ -120,8 +120,6 @@ public function createJob(): bool
                     break;
             }
 
-            //$this->setProcessing();
-
             return true;
         }
 
diff --git a/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php b/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
index 81ae6747..d9edf713 100644
--- a/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
+++ b/console/jobs/platforms/webhooks/BaseWebhookProcessingJob.php
@@ -24,6 +24,7 @@ abstract class BaseWebhookProcessingJob extends BaseObject implements RetryableJ
     public function execute($queue): void
     {
         $this->setEcommerceWebhook();
+        $this->ecommerceWebhook->setProcessing();
 
         if ($this->isPayloadJson()) {
             $this->arrayPayload = Json::decode($this->ecommerceWebhook->payload);
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index 9b017e24..58054b3e 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " app uninstalled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
index f768afea..ef994a7a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " customer data request ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
index d2a0eaf5..a4d17ac5 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " customer redact ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index ca8f2652..303e79b0 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " cancelled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
index b05c66c1..92bb64bc 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " created ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
index 687d7d50..55c56925 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " deleted ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
index 730d8929..6b442b8f 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderFulfilledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " fulfilled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
index 4a529094..b73551a4 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPaidJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " paid ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
index 1b881237..7939c152 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderPartiallyFulfilledJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " part. fulfilled ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index b540e4aa..1748465a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " updated ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
index 4aa5d504..6b8bf313 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
@@ -14,5 +14,6 @@ public function execute($queue): void
     {
         parent::execute($queue);
         echo " shop redact ";
+        $this->ecommerceWebhook->setSuccess();
     }
 }

From 0e6f47a2c1fe345efeeaf0414259c9632cc6a8de Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 15:27:08 +0200
Subject: [PATCH 58/81] Unregister Shopify Webhook Listeners Job

---
 common/models/EcommerceIntegration.php        | 15 ++++++
 common/services/platforms/ShopifyService.php  | 17 ++++++-
 .../UnregisterShopifyWebhookListenersJob.php  | 50 +++++++++++++++++++
 3 files changed, 80 insertions(+), 2 deletions(-)
 create mode 100644 console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index 2fd148c5..ed6c5135 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -7,6 +7,7 @@
 use common\models\base\BaseEcommerceIntegration;
 use console\jobs\NotificationJob;
 use common\traits\MetaDataFieldTrait;
+use common\services\platforms\ShopifyService;
 
 /**
  * Class EcommerceIntegration
@@ -50,8 +51,22 @@ public function isUninstalled(): bool
         return $this->status === self::STATUS_INTEGRATION_UNINSTALLED;
     }
 
+    /**
+     * @throws \yii\db\StaleObjectException
+     * @throws \Throwable
+     * @throws \yii\base\InvalidConfigException
+     */
     public function disconnect(): bool|int
     {
+        switch ($this->platform->name) {
+            case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
+
+                $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
+                $shopifyService->deleteWebhookListenersJob();
+                
+                break;
+        }
+
         return $this->delete();
     }
 
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 225c5315..83d38d8e 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -5,7 +5,9 @@
 use Yii;
 use yii\base\InvalidConfigException;
 use yii\helpers\{Json, Url};
-use console\jobs\platforms\shopify\{ParseShopifyOrderJob, RegisterShopifyWebhookListenersJob};
+use console\jobs\platforms\shopify\{ParseShopifyOrderJob,
+    RegisterShopifyWebhookListenersJob,
+    UnregisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
 use PHPShopify\{ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
@@ -84,7 +86,8 @@ class ShopifyService
         'shop/redact',
     ];
 
-    protected const API_VERSION = '2023-01';
+    public const API_VERSION = '2023-01';
+
     protected string $shopUrl;
     protected string $scopes = 'read_products,write_products,read_customers,write_customers,read_fulfillments,write_fulfillments,read_orders,read_shipping,write_shipping,read_returns,write_orders,write_third_party_fulfillment_orders,read_third_party_fulfillment_orders,read_assigned_fulfillment_orders,write_assigned_fulfillment_orders,';
     protected string $redirectUrl = '/ecommerce-integration/shopify';
@@ -282,6 +285,16 @@ public function addWebhookListenersJob(): void
         );
     }
 
+    public function deleteWebhookListenersJob(): void
+    {
+        Yii::$app->queue->push(
+            new UnregisterShopifyWebhookListenersJob([
+                'shopUrl' => $this->shopUrl,
+                'accessToken' => $this->ecommerceIntegration->array_meta_data['access_token']
+            ])
+        );
+    }
+
     #############
     # Webhooks: #
     #############
diff --git a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
new file mode 100644
index 00000000..f0c998de
--- /dev/null
+++ b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
@@ -0,0 +1,50 @@
+ ShopifyService::API_VERSION,
+            'ShopUrl' => $this->shopUrl,
+            'AccessToken' => $this->accessToken
+        ]);
+
+        $webhooksList = $shopify->Webhook()->get();
+
+        if ($webhooksList) {
+            foreach ($webhooksList as $webhook) {
+                if (isset($webhook['id'])) {
+                    $shopify->Webhook((int)$webhook['id'])->delete();
+                }
+            }
+        }
+    }
+
+    public function canRetry($attempt, $error): bool
+    {
+        return ($attempt < 3);
+    }
+
+    public function getTtr(): int
+    {
+        return 5 * 60;
+    }
+}

From 1294781c7068c7491b58834ccd286eef9669beab Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 16:10:12 +0200
Subject: [PATCH 59/81] Shopify raw order parsing -> phone number + email

---
 common/models/EcommerceIntegration.php        |  2 +-
 common/models/base/BaseAddress.php            |  1 +
 .../shopify/ParseShopifyOrderJob.php          | 24 ++++++++++++-------
 3 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index ed6c5135..2c8d489c 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -63,7 +63,7 @@ public function disconnect(): bool|int
 
                 $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
                 $shopifyService->deleteWebhookListenersJob();
-                
+
                 break;
         }
 
diff --git a/common/models/base/BaseAddress.php b/common/models/base/BaseAddress.php
index 02788085..35036940 100755
--- a/common/models/base/BaseAddress.php
+++ b/common/models/base/BaseAddress.php
@@ -43,6 +43,7 @@ public function rules()
             [['company', 'name', 'address1', 'address2', 'city'], 'string', 'max' => 64],
             [['zip'], 'string', 'max' => 16],
             [['phone'], 'string', 'max' => 32],
+            [['email'], 'string', 'max' => 255],
             [['notes'], 'string', 'max' => 600],
             [['country'], 'string', 'max' => 2],
             [['country'], 'default', 'value' => 'US'],
diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index da01cbd3..79da77e5 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -51,7 +51,9 @@ public function execute($queue): void
                 $this->parseItemsData();
                 $order = $this->saveOrder();
 
-                EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+                if ($order) {
+                    EcommerceOrderLog::success($this->ecommerceIntegration, $this->rawOrder, $order);
+                }
             } else {
                 EcommerceOrderLog::failed($this->ecommerceIntegration, $this->rawOrder, ['errors' => $parsingErrors]);
             }
@@ -105,14 +107,8 @@ protected function parseOrderData(): void
     protected function parseAddressData(): void
     {
         $notProvided = 'Not provided';
-        $name = null;
-        $address1 = null;
-        $address2 = null;
-        $company = null;
-        $city = null;
-        $phone = null;
+        $name = $address1 = $address2 = $company = $city = $phone = $email = $zip = null;
         $stateId = 0;
-        $zip = null;
         $countryCode = State::DEFAULT_COUNTRY_ABBR;
 
         if (isset($this->rawOrder['shipping_address']['name'])) {
@@ -135,8 +131,17 @@ protected function parseAddressData(): void
             $city = trim($this->rawOrder['shipping_address']['city']);
         }
 
+        if (isset($this->rawOrder['contact_email'])) {
+            $email = trim($this->rawOrder['contact_email']);
+        }
+
+        // Trying to find the phone number:
         if (isset($this->rawOrder['shipping_address']['phone'])) {
             $phone = trim($this->rawOrder['shipping_address']['phone']);
+        } elseif (isset($this->rawOrder['phone']) && !empty($this->rawOrder['phone'])) {
+            $phone = trim($this->rawOrder['phone']);
+        } elseif (isset($this->rawOrder['customer']) && isset($this->rawOrder['customer']['phone']) && !empty($this->rawOrder['customer']['phone'])) {
+            $phone = trim($this->rawOrder['customer']['phone']);
         }
 
         if (isset($this->rawOrder['shipping_address']['zip'])) {
@@ -169,6 +174,7 @@ protected function parseAddressData(): void
             'address2' => $address2,
             'company' => $company,
             'city' => ($city) ?: $notProvided,
+            'email' => $email,
             'phone' => ($phone) ?: $notProvided,
             'state_id' => $stateId,
             'zip' => ($zip) ?: $notProvided,
@@ -199,6 +205,8 @@ protected function saveOrder(): Order|bool
         if ($createOrderService->isValid()) {
             return $createOrderService->create();
         }
+
+        return false;
     }
 
     public function canRetry($attempt, $error): bool

From f09ced3eeaaf0c79392dd5d64847f33e396b0b1e Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:05:42 +0200
Subject: [PATCH 60/81] Shopify API -> Fixed issues with its exceptions

---
 common/services/platforms/ShopifyService.php | 67 +++++++++++++++-----
 console/controllers/CronController.php       |  6 +-
 2 files changed, 54 insertions(+), 19 deletions(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 83d38d8e..5619ba22 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -9,7 +9,7 @@
     RegisterShopifyWebhookListenersJob,
     UnregisterShopifyWebhookListenersJob};
 use common\models\{EcommerceIntegration, EcommercePlatform};
-use PHPShopify\{ShopifySDK, AuthHelper};
+use PHPShopify\{Exception\ApiException, ShopifySDK, AuthHelper};
 use yii\web\ServerErrorHttpException;
 use PHPShopify\Exception\SdkException;
 
@@ -179,7 +179,7 @@ public function accessToken(array $data, ?EcommerceIntegration $ecommerceIntegra
     /**
      * @throws InvalidConfigException
      */
-    protected function isTokenValid()
+    protected function isTokenValid(bool $withException = false): bool
     {
         try {
             $this->getProductsList();
@@ -188,47 +188,80 @@ protected function isTokenValid()
                 $this->ecommerceIntegration->uninstall(true);
             }
 
-            throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+            if ($withException) {
+                throw new InvalidConfigException('Shopify token for the shop `' . $this->shopUrl . '` is invalid.');
+            }
+            return false;
         }
+
+        return true;
     }
 
     #####################
     # Get data via API: #
     #####################
 
-    public function getShop(): array
+    public function getShop(): array|bool
     {
-        return $this->shopify->Shop->get();
+        try {
+            return $this->shopify->Shop->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getProductsList(): array
+    public function getProductsList(): array|bool
     {
-        return $this->shopify->Product->get();
+        try {
+            return $this->shopify->Product->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getProductById(int $id): array
+    public function getProductById(int $id): array|bool
     {
-        return $this->shopify->Product($id)->get();
+        try {
+            return $this->shopify->Product($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getOrdersList(): array
+    public function getOrdersList(): array|bool
     {
-        return $this->shopify->Order->get($this->getRequestParamsForOrders());
+        try {
+            return $this->shopify->Order->get($this->getRequestParamsForOrders());
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getOrderById(int $id): array
+    public function getOrderById(int $id): array|bool
     {
-        return $this->shopify->Order($id)->get();
+        try {
+            return $this->shopify->Order($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getCustomerById(int $id): array
+    public function getCustomerById(int $id): array|bool
     {
-        return $this->shopify->Customer($id)->get();
+        try {
+            return $this->shopify->Customer($id)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
-    public function getCustomerAddressById(int $customerId, int $addressId): array
+    public function getCustomerAddressById(int $customerId, int $addressId): array|bool
     {
-        return $this->shopify->Customer($customerId)->Address($addressId)->get();
+        try {
+            return $this->shopify->Customer($customerId)->Address($addressId)->get();
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
     /**
diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index b597207a..337ec5ee 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -106,8 +106,10 @@ protected function runEcommerceIntegrations(): void
                         $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
                         $orders = $shopifyService->getOrdersList();
 
-                        foreach ($orders as $order) {
-                            $shopifyService->parseRawOrderJob($order);
+                        if ($orders) {
+                            foreach ($orders as $order) {
+                                $shopifyService->parseRawOrderJob($order);
+                            }
                         }
                     }
 

From 3dc6c74b314140e85fa5be5fba23101f258606ee Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:16:55 +0200
Subject: [PATCH 61/81] Shop disconnect fix + Find e-commerce integration by
 meta key

---
 common/models/EcommerceIntegration.php            | 2 +-
 common/models/query/EcommerceIntegrationQuery.php | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/common/models/EcommerceIntegration.php b/common/models/EcommerceIntegration.php
index 2c8d489c..5f0a0609 100644
--- a/common/models/EcommerceIntegration.php
+++ b/common/models/EcommerceIntegration.php
@@ -58,7 +58,7 @@ public function isUninstalled(): bool
      */
     public function disconnect(): bool|int
     {
-        switch ($this->platform->name) {
+        switch ($this->ecommercePlatform->name) {
             case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
 
                 $shopifyService = new ShopifyService($this->array_meta_data['shop_url'], $this);
diff --git a/common/models/query/EcommerceIntegrationQuery.php b/common/models/query/EcommerceIntegrationQuery.php
index ed04d0aa..ef86af10 100644
--- a/common/models/query/EcommerceIntegrationQuery.php
+++ b/common/models/query/EcommerceIntegrationQuery.php
@@ -4,6 +4,7 @@
 
 use common\models\EcommerceIntegration;
 use yii\db\ActiveQuery;
+use yii\db\Expression;
 
 /**
  * Class EcommerceIntegrationQuery
@@ -16,6 +17,11 @@ public function active(): EcommerceIntegrationQuery
         return $this->andWhere(['status' => EcommerceIntegration::STATUS_INTEGRATION_CONNECTED]);
     }
 
+    public function byMetaKey(string $key, string $value): EcommerceIntegrationQuery
+    {
+        return $this->andWhere(new Expression('`meta` LIKE :find', [':find' => '%"' . $key . '": "'. $value .'"%']));
+    }
+
     public function for(?int $userId = null, ?int $customerId = null): EcommerceIntegrationQuery
     {
         if ($userId) {

From 12b5cd5073974bf2b01a43e6646c0fd953b5451f Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:46:14 +0200
Subject: [PATCH 62/81] UnregisterShopifyWebhookListenersJob

---
 .../UnregisterShopifyWebhookListenersJob.php  |  6 ++-
 .../shopify/ShopifyAppUninstalledJob.php      | 38 ++++++++++++++++++-
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
index f0c998de..6318c5db 100644
--- a/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
+++ b/console/jobs/platforms/shopify/UnregisterShopifyWebhookListenersJob.php
@@ -27,7 +27,11 @@ public function execute($queue): void
             'AccessToken' => $this->accessToken
         ]);
 
-        $webhooksList = $shopify->Webhook()->get();
+        try {
+            $webhooksList = $shopify->Webhook()->get();
+        } catch (\Exception $e) {
+            $webhooksList = [];
+        }
 
         if ($webhooksList) {
             foreach ($webhooksList as $webhook) {
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index 58054b3e..f2673908 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -2,7 +2,9 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyAppUninstalledJob
@@ -13,7 +15,39 @@ class ShopifyAppUninstalledJob extends BaseWebhookProcessingJob
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " app uninstalled ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->uninstall()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function uninstall(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+        $shopUrl = null;
+
+        if (isset($payload['domain'])) {
+            $shopUrl = trim($payload['domain']);
+        }
+
+        if ($shopUrl) {
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->active()
+                ->byMetaKey('shop_url', $shopUrl)
+                ->one();
+
+            if ($ecommerceIntegration && !$ecommerceIntegration->isUninstalled()) {
+                $ecommerceIntegration->uninstall(true);
+
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 0f7a25abc347b3307930fd783a56e243b895a0d8 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:54:25 +0200
Subject: [PATCH 63/81] ShopifyShopRedactJob

---
 .../webhooks/shopify/ShopifyShopRedactJob.php | 33 +++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
index 6b8bf313..557e9d78 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyShopRedactJob.php
@@ -2,18 +2,47 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyShopRedactJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#shop-redact
  */
 class ShopifyShopRedactJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " shop redact ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->deleteEcommerceIntegration()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function deleteEcommerceIntegration(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['shop_domain'])) {
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->byMetaKey('shop_url', trim($payload['shop_domain']))
+                ->one();
+
+            if ($ecommerceIntegration) {
+                $ecommerceIntegration->disconnect();
+
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 492e73cdf378dc5c3b8aa03f033bbeb1cc7b7435 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 17:59:31 +0200
Subject: [PATCH 64/81] ShopifyCustomerRedactJob

---
 .../platforms/webhooks/shopify/ShopifyAppUninstalledJob.php  | 1 +
 .../platforms/webhooks/shopify/ShopifyCustomerRedactJob.php  | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
index f2673908..e898b9f7 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyAppUninstalledJob.php
@@ -9,6 +9,7 @@
 /**
  * Class ShopifyAppUninstalledJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-app-uninstalled
  */
 class ShopifyAppUninstalledJob extends BaseWebhookProcessingJob
 {
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
index a4d17ac5..b789e836 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerRedactJob.php
@@ -7,13 +7,16 @@
 /**
  * Class ShopifyCustomerRedactJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#customers-redact
  */
 class ShopifyCustomerRedactJob extends BaseWebhookProcessingJob
 {
+    /**
+     * Since we don't save information (data) about Shopify customers, we skip this mandatory webhook
+     */
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " customer redact ";
         $this->ecommerceWebhook->setSuccess();
     }
 }

From 696cf16da19630fd94e27963da0a71778756b3b5 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 18:03:25 +0200
Subject: [PATCH 65/81] ShopifyCustomerDataRequestJob

---
 .../shopify/ShopifyCustomerDataRequestJob.php         | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
index ef994a7a..d36462eb 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyCustomerDataRequestJob.php
@@ -7,13 +7,22 @@
 /**
  * Class ShopifyCustomerDataRequestJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#customers-data_request
  */
 class ShopifyCustomerDataRequestJob extends BaseWebhookProcessingJob
 {
+    /**
+     * Quote:
+     *
+     * The webhook contains the resource IDs of the customer data that you need to provide to the store owner.
+     * It's your responsibility to provide this data to the store owner directly.
+     * In some cases, a customer record contains only the customer's email address.
+     *
+     * It means that it's enough just to mark the webhook as `success` since the `payload` field consists of the needed resource IDs.
+     */
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " customer data request ";
         $this->ecommerceWebhook->setSuccess();
     }
 }

From dacf5751a7c291c8ec18f15b6c8634fcc5a3f394 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 19:02:28 +0200
Subject: [PATCH 66/81] ShopifyOrderCancelledJob

---
 .../shopify/ShopifyOrderCancelledJob.php      | 29 +++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index 303e79b0..ceb8ed3a 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -2,18 +2,43 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\Status;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderCancelledJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-cancelled
  */
 class ShopifyOrderCancelledJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " cancelled ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->cancel()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function cancel(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $externalOrderId = (int)$payload['id'];
+            $internalOrder = $this->getOrderByExternalId($externalOrderId);
+
+            if ($internalOrder) {
+                $internalOrder->status_id = Status::CANCELLED;
+                $internalOrder->save();
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 04c3c80c2aab9648a96d8951006dabb2fd989ad2 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 13 Mar 2023 19:11:03 +0200
Subject: [PATCH 67/81] ShopifyOrderDeletedJob

---
 .../shopify/ShopifyOrderCancelledJob.php      |  3 +-
 .../shopify/ShopifyOrderDeletedJob.php        | 30 +++++++++++++++++--
 2 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
index ceb8ed3a..19e42926 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCancelledJob.php
@@ -29,8 +29,7 @@ protected function cancel(): bool
         $payload = Json::decode($this->ecommerceWebhook->payload);
 
         if (isset($payload['id'])) {
-            $externalOrderId = (int)$payload['id'];
-            $internalOrder = $this->getOrderByExternalId($externalOrderId);
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
 
             if ($internalOrder) {
                 $internalOrder->status_id = Status::CANCELLED;
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
index 55c56925..59438502 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderDeletedJob.php
@@ -3,17 +3,43 @@
 namespace console\jobs\platforms\webhooks\shopify;
 
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderDeletedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-delete
  */
 class ShopifyOrderDeletedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " deleted ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->delete()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    /**
+     * @throws \yii\db\StaleObjectException
+     * @throws \Throwable
+     */
+    protected function delete(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+            if ($internalOrder) {
+                $internalOrder->delete();
+                return true;
+            }
+        }
+
+        return false;
     }
 }

From 2ff5e13f298d8f93f65091876fe9cdaec166ffaf Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 14:56:22 +0200
Subject: [PATCH 68/81] ShopifyOrderUpdatedJob

---
 .../shopify/ParseShopifyOrderJob.php          |   2 +-
 .../shopify/ShopifyOrderUpdatedJob.php        | 160 +++++++++++++++++-
 2 files changed, 159 insertions(+), 3 deletions(-)

diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 79da77e5..861f9c20 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -99,7 +99,7 @@ protected function parseOrderData(): void
             'uuid' => (string)$this->rawOrder['id'],
             'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
             'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'notes' => $this->rawOrder['tags'],
+            'notes' => trim($this->rawOrder['tags']),
             'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
         ];
     }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index 1748465a..086e982c 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -2,18 +2,174 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use common\models\{Address, Country, Item, Order, State};
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use yii\helpers\Json;
 
 /**
  * Class ShopifyOrderUpdatedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-updated
  */
 class ShopifyOrderUpdatedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " updated ";
-        $this->ecommerceWebhook->setSuccess();
+
+        if ($this->update()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function update(): bool
+    {
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+
+        if (isset($payload['id'])) {
+            $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+            if ($internalOrder) {
+                $this->updateOrder($internalOrder, $payload);
+                $this->updateAddress($internalOrder->address, $payload);
+                $this->updateItems($internalOrder, $payload);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    protected function updateOrder(Order $internalOrder, array $payload): void
+    {
+        $internalOrder->notes = trim($payload['tags']);
+        $internalOrder->save();
+    }
+
+    protected function updateAddress(Address $internalAddress, array $payload): void
+    {
+        $notProvided = 'Not provided';
+        $name = trim($payload['shipping_address']['name']);
+        $address1 = trim($payload['shipping_address']['address1']);
+        $address2 = trim($payload['shipping_address']['address2']);
+        $company = trim($payload['shipping_address']['company']);
+        $city = trim($payload['shipping_address']['city']);
+        $email = trim($payload['contact_email']);
+        $zip = trim($payload['shipping_address']['zip']);
+
+        $internalAddress->name = ($name) ?: $notProvided;
+        $internalAddress->address1 = ($address1) ?: $notProvided;
+        $internalAddress->address2 = ($address2) ?: null;
+        $internalAddress->company = ($company) ?: null;
+        $internalAddress->city = ($city) ?: $notProvided;
+        $internalAddress->email = ($email) ?: null;
+        $internalAddress->phone = ($this->getPhoneNumberFromPayload($payload)) ?: $notProvided;
+        $internalAddress->zip = ($zip) ?: $notProvided;
+        $internalAddress->state_id = $this->getStateIdFromPayload($payload);
+        $internalAddress->country = $this->getCountryCodeFromPayload($payload);
+        $internalAddress->save();
+    }
+
+    protected function updateItems(Order $internalOrder, array $payload): void
+    {
+        $internalItems = $internalOrder->items;
+        $internalUuids = $externalUuids = [];
+
+        // Collect external item UUIDs:
+        if (isset($payload['line_items'])) {
+            foreach ($payload['line_items'] as $externalItem) {
+                $externalUuids[] = (string)$externalItem['id'];
+            }
+        }
+
+        // Collect internal item UUIDs:
+        foreach ($internalItems as $internalItem) {
+            $internalUuids[] = (string)$internalItem->uuid;
+        }
+
+        // Remove internal Items that aren't presented in the `line_items`:
+        foreach ($internalItems as $internalItem) {
+            if (!in_array($internalItem->uuid, $externalUuids)) {
+                $internalItem->delete();
+            }
+        }
+
+        // Add or update:
+        foreach ($payload['line_items'] as $externalItem) {
+            $externalItemUuid = (string)$externalItem['id'];
+
+            $attributes = [
+                'order_id' => $internalOrder->id,
+                'quantity' => $externalItem['fulfillable_quantity'],
+                'sku' => ($externalItem['sku']) ?: 'Not provided',
+                'name' => $externalItem['name'],
+                'uuid' => $externalItemUuid,
+            ];
+
+            if (!in_array($externalItemUuid, $internalUuids)) { // Add:
+                $orderItem = new Item();
+                $orderItem->setAttributes($attributes);
+                $orderItem->save();
+            } else { // Update:
+                foreach ($internalItems as $internalItem) {
+                    if ($internalItem->uuid == $externalItemUuid) {
+                        $internalItem->setAttributes($attributes);
+                        $internalItem->save();
+                    }
+                }
+            }
+        }
+    }
+
+    protected function getPhoneNumberFromPayload(array $payload): ?string
+    {
+        $phone = null;
+
+        if (isset($payload['shipping_address']['phone'])) {
+            $phone = trim($payload['shipping_address']['phone']);
+        } elseif (isset($payload['phone']) && !empty($payload['phone'])) {
+            $phone = trim($payload['phone']);
+        } elseif (isset($payload['customer']) && isset($payload['customer']['phone']) && !empty($payload['customer']['phone'])) {
+            $phone = trim($payload['customer']['phone']);
+        }
+
+        return $phone;
+    }
+
+    protected function getStateIdFromPayload(array $payload): int
+    {
+        $stateId = 0;
+
+        if (isset($payload['shipping_address']['province_code'])) {
+            $state = State::find()->where([
+                'abbreviation' => trim($payload['shipping_address']['province_code'])
+            ])->one();
+
+            if ($state) {
+                $stateId = (int)$state->id;
+            }
+        }
+
+        return $stateId;
+    }
+
+    protected function getCountryCodeFromPayload(array $payload): string
+    {
+        $countryCode = State::DEFAULT_COUNTRY_ABBR;
+
+        if (isset($payload['shipping_address']['country_code'])) {
+            $country = Country::find()->where([
+                'abbreviation' => trim($payload['shipping_address']['country_code'])
+            ])->one();
+
+            if ($country) {
+                $countryCode = $country->abbreviation;
+            }
+        }
+
+        return $countryCode;
     }
 }

From 51a70dad82ea24992fd0ba34cdf2d03bf16a5a38 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 15:50:31 +0200
Subject: [PATCH 69/81] ShopifyOrderCreatedJob

---
 .../shopify/ShopifyOrderCreatedJob.php        | 56 ++++++++++++++++++-
 1 file changed, 54 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
index 92bb64bc..0b1744e1 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderCreatedJob.php
@@ -2,18 +2,70 @@
 
 namespace console\jobs\platforms\webhooks\shopify;
 
+use Yii;
+use yii\helpers\Json;
+use common\models\EcommerceIntegration;
 use console\jobs\platforms\webhooks\BaseWebhookProcessingJob;
+use console\jobs\platforms\shopify\ParseShopifyOrderJob;
 
 /**
  * Class ShopifyOrderCreatedJob
  * @package console\jobs\platforms\webhooks\shopify
+ * @see https://shopify.dev/docs/api/admin-rest/2023-01/resources/webhook#event-topics-orders-create
  */
 class ShopifyOrderCreatedJob extends BaseWebhookProcessingJob
 {
     public function execute($queue): void
     {
         parent::execute($queue);
-        echo " created ";
-        $this->ecommerceWebhook->setSuccess();
+
+        $payload = Json::decode($this->ecommerceWebhook->payload);
+        $internalOrder = $this->getOrderByExternalId((int)$payload['id']);
+
+        if ($internalOrder) {
+            // If we already have an internal order with such UUID, we just do nothing:
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $ecommerceIntegration = $this->getEcommerceIntegration($payload);
+
+            if ($ecommerceIntegration && $this->create($payload, $ecommerceIntegration)) {
+                $this->ecommerceWebhook->setSuccess();
+            } else {
+                $this->ecommerceWebhook->setFailed();
+            }
+        }
+    }
+
+    /**
+     * For parsing raw Shopify orders, we have a separate Job:
+     */
+    protected function create(array $payload, EcommerceIntegration $ecommerceIntegration): bool
+    {
+        Yii::$app->queue->push(
+            new ParseShopifyOrderJob([
+                'rawOrder' => $payload,
+                'ecommerceIntegrationId' => $ecommerceIntegration->id
+            ])
+        );
+
+        return true;
+    }
+
+    protected function getEcommerceIntegration(array $payload): EcommerceIntegration|bool
+    {
+        if (isset($payload['order_status_url']) && !empty($payload['order_status_url'])) {
+            $domain = parse_url($payload['order_status_url'], PHP_URL_HOST);
+
+            /**
+             * @var $ecommerceIntegration EcommerceIntegration
+             */
+            $ecommerceIntegration = EcommerceIntegration::find()
+                ->byMetaKey('shop_url', $domain)
+                ->one();
+
+            return ($ecommerceIntegration) ?: false;
+        }
+
+        return false;
     }
 }

From 51b15037b1aa3469fd13993b99041dd86818fddf Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 14 Mar 2023 15:57:14 +0200
Subject: [PATCH 70/81] Order Notes

---
 console/jobs/platforms/shopify/ParseShopifyOrderJob.php         | 2 +-
 .../jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index 861f9c20..c8f341b5 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -99,7 +99,7 @@ protected function parseOrderData(): void
             'uuid' => (string)$this->rawOrder['id'],
             'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),
             'origin' => EcommercePlatform::SHOPIFY_PLATFORM_NAME,
-            'notes' => trim($this->rawOrder['tags']),
+            'notes' => trim($this->rawOrder['note']),
             'address_id' => 0, // To skip validation, will be overwritten in `CreateOrderService`
         ];
     }
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
index 086e982c..a6740117 100644
--- a/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderUpdatedJob.php
@@ -45,7 +45,7 @@ protected function update(): bool
 
     protected function updateOrder(Order $internalOrder, array $payload): void
     {
-        $internalOrder->notes = trim($payload['tags']);
+        $internalOrder->notes = trim($payload['note']);
         $internalOrder->save();
     }
 

From 2f4e4d865a79bf59e507782e0abc9c93a9cc01dd Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 16 Mar 2023 18:34:47 +0200
Subject: [PATCH 71/81] Updated docs + removed test code

---
 .../EcommerceWebhookController.php            | 20 -------------------
 intro-docs/ecommerce-platfroms.md             | 11 +++++++---
 2 files changed, 8 insertions(+), 23 deletions(-)

diff --git a/frontend/controllers/EcommerceWebhookController.php b/frontend/controllers/EcommerceWebhookController.php
index 4b870b3c..b12e93ab 100644
--- a/frontend/controllers/EcommerceWebhookController.php
+++ b/frontend/controllers/EcommerceWebhookController.php
@@ -31,9 +31,6 @@ public function beforeAction($action): bool
      */
     public function actionShopify(): array
     {
-        // file_put_contents('shopify.txt', "\r\n\r\n\r\n" . Yii::$app->request->rawBody, FILE_APPEND);
-
-
         $ecommercePlatform = $this->getEcommercePlatformByName(EcommercePlatform::SHOPIFY_PLATFORM_NAME);
         $event = Yii::$app->request->get('event');
 
@@ -88,21 +85,4 @@ protected function getNewEcommerceWebhookObject(int $ecommercePlatformId): Ecomm
 
         return $ecommerceWebhook;
     }
-
-    public function actionTest()
-    {
-        $ecommerceIntegrations = EcommerceIntegration::find()
-            ->active()
-            ->orderById()
-            ->all();
-
-        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
-            $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
-
-            if ($accessToken) {
-                $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
-                return $shopifyService->getWebhooksList();
-            }
-        }
-    }
 }
diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index bbcb0676..428464c0 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -9,20 +9,24 @@
 3. Add implementation to:
 - `common\models\EcommercePlatform`
 - `common\models\EcommerceIntegration`
+- `common\models\EcommerceWebhook`
 - `frontend\controllers\EcommercePlatformController`
 - `frontend\controllers\EcommerceIntegrationController`
 - `frontend\controllers\EcommerceWebhookController`
 
 4. Create a Service similar to `common\services\platforms\ShopifyService`.
 
-5. Create a Job similar to `console\jobs\platforms\ParseShopifyOrderJob`.
+5. Create Jobs similar to `console\jobs\platforms\*`.
 
 > php yii queue/listen --verbose
 
 ### Cron:
 
 1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
-We need this method to pull existing orders from a needed E-commerce platform.
+We need this method to pull initial existing orders from a needed E-commerce platform.
+   
+2. See `console\controllers\CronController.php` -> `runEcommerceWebhooks()`.
+We need this method to process received webhooks (`status=received`).
    
 > php yii cron/frequent
 
@@ -74,7 +78,8 @@ If you're going to **test it locally**, specify:
 4. If you're going to **test it locally**, in `common\config\params-local.php` set the parameter `override_redirect_domain`
 to `https://shipwise.ngrok.io`.
    
-5. You need to request `Protected customer data access`. Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` ->
+5. You need to request `Protected customer data access` for data like shipping address. 
+Go to `Apps` -> `Your app` -> `App setup` -> Find the section `Protected customer data access` ->
 Press the button `Request access`. On the page, select and request access for:
    
 - `Protected customer data`

From 7f4e181b0a18ad740c21067873457cfb5e73637a Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Mon, 20 Mar 2023 15:58:57 +0200
Subject: [PATCH 72/81] Updated migrations - added some indexes

---
 .../m230221_124952_create_table_ecommerce_platform.php        | 4 ++++
 .../m230222_112448_add_ecommerce_integration_table.php        | 4 ++++
 2 files changed, 8 insertions(+)

diff --git a/console/migrations/m230221_124952_create_table_ecommerce_platform.php b/console/migrations/m230221_124952_create_table_ecommerce_platform.php
index 1d467ff3..d1533e94 100644
--- a/console/migrations/m230221_124952_create_table_ecommerce_platform.php
+++ b/console/migrations/m230221_124952_create_table_ecommerce_platform.php
@@ -23,6 +23,10 @@ public function safeUp()
                 PRIMARY KEY (`id`)) ENGINE = InnoDB;        
         ");
 
+        $this->execute("
+            ALTER TABLE `ecommerce_platform` ADD INDEX(`name`);
+        ");
+
         $this->execute("
             ALTER TABLE `ecommerce_platform` CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
         ");
diff --git a/console/migrations/m230222_112448_add_ecommerce_integration_table.php b/console/migrations/m230222_112448_add_ecommerce_integration_table.php
index 8863fdb0..0b822cbc 100644
--- a/console/migrations/m230222_112448_add_ecommerce_integration_table.php
+++ b/console/migrations/m230222_112448_add_ecommerce_integration_table.php
@@ -30,6 +30,10 @@ public function safeUp()
                 CHANGE `updated_date` `updated_date` DATETIME on update CURRENT_TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
         ");
 
+        $this->execute("
+            ALTER TABLE `ecommerce_integration` ADD INDEX(`status`);
+        ");
+
         $this->addForeignKey(
             '{{%fk-ecommerce_integration-user_id}}',
             '{{%ecommerce_integration}}',

From 352464a506c03264aa8cf12757f26aeb183ef32b Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Wed, 22 Mar 2023 15:33:16 +0200
Subject: [PATCH 73/81] Changes.

---
 .../platforms/ConnectShopifyStoreForm.php     |  5 ++--
 .../RegisterShopifyWebhookListenersJob.php    |  5 +---
 ..._115138_remove_previous_shopify_tables.php | 24 +++++++++++++++----
 .../views/ecommerce-integration/_shopify.php  |  4 ++--
 4 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/common/models/forms/platforms/ConnectShopifyStoreForm.php b/common/models/forms/platforms/ConnectShopifyStoreForm.php
index 07ed3670..6675c378 100644
--- a/common/models/forms/platforms/ConnectShopifyStoreForm.php
+++ b/common/models/forms/platforms/ConnectShopifyStoreForm.php
@@ -9,6 +9,7 @@
 use yii\web\ServerErrorHttpException;
 use common\services\platforms\ShopifyService;
 use common\models\{EcommerceIntegration, EcommercePlatform, Customer};
+use yii\helpers\Json;
 
 /**
  * Class ConnectShopifyStoreForm
@@ -130,7 +131,7 @@ public function auth(): void
      */
     public function saveAccessToken(bool $addWebHookListeners = true): void
     {
-        $data = unserialize(Yii::$app->session->get('shopify_connection_second_step'));
+        $data = Json::decode(Yii::$app->session->get('shopify_connection_second_step'));
 
         if (isset($data['integration_id'])) {
             $this->ecommerceIntegration = EcommerceIntegration::findOne($data['integration_id']);
@@ -161,6 +162,6 @@ protected function saveDataForSecondStep(): void
             $data['integration_id'] = $this->ecommerceIntegration->id;
         }
 
-        Yii::$app->session->set('shopify_connection_second_step', serialize($data));
+        Yii::$app->session->set('shopify_connection_second_step', Json::encode($data));
     }
 }
diff --git a/console/jobs/platforms/shopify/RegisterShopifyWebhookListenersJob.php b/console/jobs/platforms/shopify/RegisterShopifyWebhookListenersJob.php
index b387f300..8bfd80a7 100644
--- a/console/jobs/platforms/shopify/RegisterShopifyWebhookListenersJob.php
+++ b/console/jobs/platforms/shopify/RegisterShopifyWebhookListenersJob.php
@@ -84,14 +84,11 @@ protected function updateIntegrationMetaData(): void
 
     protected function sendRequest(string $event): void
     {
-        $res = $this->shopifyService->createWebhook([
+        $this->shopifyService->createWebhook([
             'topic' => $event,
             'address' => $this->domain . ShopifyService::$webhooksUrl . '?event=' . $event,
             'format' => 'json',
         ]);
-
-//        echo '
' . $event . ': ';
-//        print_r($res);
     }
 
     public function canRetry($attempt, $error): bool
diff --git a/console/migrations/m230221_115138_remove_previous_shopify_tables.php b/console/migrations/m230221_115138_remove_previous_shopify_tables.php
index a4a14224..4b169fd3 100644
--- a/console/migrations/m230221_115138_remove_previous_shopify_tables.php
+++ b/console/migrations/m230221_115138_remove_previous_shopify_tables.php
@@ -30,20 +30,36 @@ public function safeDown()
               `access_token` varchar(128) DEFAULT NULL,
               `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
               PRIMARY KEY (`id`),
-              UNIQUE KEY `shop` (`shop`),
-              KEY `customer_id` (`customer_id`)
+              UNIQUE KEY `shop` (`shop`)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;           
         ");
 
+        $this->addForeignKey(
+            '{{%fk-shopify_app-customer_id}}',
+            '{{%shopify_app}}',
+            'customer_id',
+            '{{%customers}}',
+            'id',
+            'CASCADE'
+        );
+
         $this->execute("
             CREATE TABLE `shopify_webhook` (
               `id` int NOT NULL AUTO_INCREMENT,
               `customer_id` int NOT NULL,
               `shopify_webhook_id` varchar(64) NOT NULL,
               `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-              PRIMARY KEY (`id`),
-              KEY `customer_id` (`customer_id`)
+              PRIMARY KEY (`id`)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;        
         ");
+
+        $this->addForeignKey(
+            '{{%fk-shopify_webhook-customer_id}}',
+            '{{%shopify_webhook}}',
+            'customer_id',
+            '{{%customers}}',
+            'id',
+            'CASCADE'
+        );
     }
 }
diff --git a/frontend/views/ecommerce-integration/_shopify.php b/frontend/views/ecommerce-integration/_shopify.php
index e92e0dd7..0476f959 100644
--- a/frontend/views/ecommerce-integration/_shopify.php
+++ b/frontend/views/ecommerce-integration/_shopify.php
@@ -70,7 +70,7 @@
             'label' => 'Shop URL:',
             'format' => 'raw',
             'value' => function($model) {
-                return '' . $model->array_meta_data['shop_url'] . ' ';
+                return '' . Html::encode($model->array_meta_data['shop_url']) . ' ';
             },
         ],
         [
@@ -116,7 +116,7 @@
             'label' => 'Customer:',
             'format' => 'raw',
             'value' => function($model) {
-                return Html::encode($model->customer->name);
+                return $model->customer->name;
             },
         ],
         [

From 37b38191fa4a2177f85c8e5caade4cf26b606113 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Wed, 22 Mar 2023 16:34:14 +0200
Subject: [PATCH 74/81] Order history -> Added System user

---
 common/models/OrderHistory.php                |  9 +++---
 ...2_142353_order_history_add_system_user.php | 29 +++++++++++++++++++
 frontend/views/order/view.php                 |  3 ++
 3 files changed, 36 insertions(+), 5 deletions(-)
 create mode 100644 console/migrations/m230322_142353_order_history_add_system_user.php

diff --git a/common/models/OrderHistory.php b/common/models/OrderHistory.php
index b6e48b5c..09e2b0ce 100755
--- a/common/models/OrderHistory.php
+++ b/common/models/OrderHistory.php
@@ -15,6 +15,8 @@
  */
 class OrderHistory extends BaseOrderHistory
 {
+    public const SYSTEM_USER_ID = NULL;
+
     public const SCENARIO_ORDER_CREATED = 'scenarioOrderCreated';
     public const SCENARIO_ORDER_VIEWED = 'scenarioOrderViewed';
     public const SCENARIO_ORDER_CHANGED = 'scenarioOrderChanged';
@@ -78,13 +80,10 @@ protected function orderHistoryPopulate(): void
         }
 
         if (!$this->user_id) {
-            /**
-             * TODO: Replace with something like `system`
-             */
             if (Yii::$app->request->isConsoleRequest) {
-                $this->user_id = 1;
+                $this->user_id = self::SYSTEM_USER_ID;
             } else {
-                $this->user_id = (!Yii::$app->user->isGuest) ? Yii::$app->user->id : 1;
+                $this->user_id = (!Yii::$app->user->isGuest) ? Yii::$app->user->id : self::SYSTEM_USER_ID;
             }
         }
 
diff --git a/console/migrations/m230322_142353_order_history_add_system_user.php b/console/migrations/m230322_142353_order_history_add_system_user.php
new file mode 100644
index 00000000..fa0af456
--- /dev/null
+++ b/console/migrations/m230322_142353_order_history_add_system_user.php
@@ -0,0 +1,29 @@
+execute("
+            ALTER TABLE `order_history` CHANGE `user_id` `user_id` INT NULL DEFAULT NULL;
+        ");
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function safeDown()
+    {
+        $this->execute("
+            ALTER TABLE `order_history` CHANGE `user_id` `user_id` INT NOT NULL;
+        ");
+    }
+}
diff --git a/frontend/views/order/view.php b/frontend/views/order/view.php
index a7392478..4c0354d3 100755
--- a/frontend/views/order/view.php
+++ b/frontend/views/order/view.php
@@ -232,6 +232,9 @@
                         'columns' => [
                             [
                                 'attribute' => 'user.username',
+                                'value' => function ($model) {
+                                    return ($model->user) ? $model->user->username : 'System';
+                                }
                             ],
                             'created_date:datetime',
                             [

From ff05a8c385482ebc9cb97c5d35e8391748cf8ae6 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Wed, 22 Mar 2023 17:32:56 +0200
Subject: [PATCH 75/81] Orders removed limit parameter.

---
 common/services/platforms/ShopifyService.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 5619ba22..6696c249 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -270,9 +270,7 @@ public function getCustomerAddressById(int $customerId, int $addressId): array|b
      */
     protected function getRequestParamsForOrders(): array
     {
-        $params = [
-            'limit' => 250,
-        ];
+        $params = [];
 
         if ($this->ecommerceIntegration->isMetaKeyExistsAndNotEmpty('order_statuses')) {
             $params['status'] = implode(',', $this->ecommerceIntegration->array_meta_data['order_statuses']);

From b88b7bc07a74b03b56883917aa48de646c1bb180 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Thu, 23 Mar 2023 10:32:22 +0200
Subject: [PATCH 76/81] m230221_134343_add_shopify_mock_ecommerce_platform

---
 ...p => m230221_134343_add_shopify_mock_ecommerce_platform.php} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
 rename console/migrations/{m230221_134343_add_shopify_mock_ecommerce_platfort.php => m230221_134343_add_shopify_mock_ecommerce_platform.php} (88%)

diff --git a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platform.php
similarity index 88%
rename from console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php
rename to console/migrations/m230221_134343_add_shopify_mock_ecommerce_platform.php
index 82166e55..1484ed3c 100644
--- a/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platfort.php
+++ b/console/migrations/m230221_134343_add_shopify_mock_ecommerce_platform.php
@@ -3,7 +3,7 @@
 use yii\db\Migration;
 
 /**
- * Class m230221_134343_add_shopify_mock_ecommerce_platfort
+ * Class m230221_134343_add_shopify_mock_ecommerce_platform
  */
 class m230221_134343_add_shopify_mock_ecommerce_platform extends Migration
 {

From ef92334fda0612afb4990af6f249add7770ddece Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 28 Mar 2023 13:55:40 +0300
Subject: [PATCH 77/81] Removed runEcommerceIntegrations() from CRON

---
 console/controllers/CronController.php | 41 --------------------------
 1 file changed, 41 deletions(-)

diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index 337ec5ee..2e7608dc 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -73,52 +73,11 @@ public function actionFrequent(): int
         $this->runIntegrations(Integration::ACTIVE);
         $this->runScheduledOrders();
 
-        $this->runEcommerceIntegrations();
         $this->runEcommerceWebhooks();
 
         return ExitCode::OK;
     }
 
-    /**
-     * This method is used for pulling first (initial) raw orders from E-commerce platforms like Shopify.
-     * For working with webhooks, the method `runEcommerceWebhooks()` is used.
-     * @throws InvalidConfigException
-     */
-    protected function runEcommerceIntegrations(): void
-    {
-        $ecommerceIntegrations = EcommerceIntegration::find()
-            ->active()
-            ->orderById()
-            ->limit(100)
-            ->all();
-
-        foreach ($ecommerceIntegrations as $ecommerceIntegration) {
-            switch ($ecommerceIntegration->ecommercePlatform->name) {
-
-                /**
-                 * Shopify:
-                 */
-                case EcommercePlatform::SHOPIFY_PLATFORM_NAME:
-
-                    $accessToken = $ecommerceIntegration->array_meta_data['access_token'];
-
-                    if ($accessToken) {
-                        $shopifyService = new ShopifyService($ecommerceIntegration->array_meta_data['shop_url'], $ecommerceIntegration);
-                        $orders = $shopifyService->getOrdersList();
-
-                        if ($orders) {
-                            foreach ($orders as $order) {
-                                $shopifyService->parseRawOrderJob($order);
-                            }
-                        }
-                    }
-
-                    break;
-
-            }
-        }
-    }
-
     /**
      * This method is used for creating Jobs for webhooks with the status "received".
      */

From 5cbe6c0a180fbed09a369689d4d1b442c4eb80d9 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 28 Mar 2023 14:33:30 +0300
Subject: [PATCH 78/81] Customer Order #

---
 console/jobs/platforms/shopify/ParseShopifyOrderJob.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
index c8f341b5..2f44c261 100644
--- a/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
+++ b/console/jobs/platforms/shopify/ParseShopifyOrderJob.php
@@ -93,8 +93,7 @@ protected function parseOrderData(): void
     {
         $this->parsedOrderAttributes = [
             'customer_id' => $this->ecommerceIntegration->customer_id,
-            'customer_reference' => (string)$this->rawOrder['id'],
-            'order_reference' => $this->rawOrder['name'],
+            'customer_reference' => str_replace('#', '', (string)$this->rawOrder['name']),
             'status_id' => Status::OPEN,
             'uuid' => (string)$this->rawOrder['id'],
             'created_date' => (new \DateTime($this->rawOrder['created_at']))->format('Y-m-d'),

From aca05b7627645af800d1e775befa4d21de5786ff Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 28 Mar 2023 16:02:07 +0300
Subject: [PATCH 79/81] ShopifyOrderRefundJob mock job

---
 common/models/EcommerceWebhook.php            |  2 ++
 common/services/platforms/ShopifyService.php  |  1 +
 .../shopify/ShopifyOrderRefundJob.php         | 30 +++++++++++++++++++
 3 files changed, 33 insertions(+)
 create mode 100644 console/jobs/platforms/webhooks/shopify/ShopifyOrderRefundJob.php

diff --git a/common/models/EcommerceWebhook.php b/common/models/EcommerceWebhook.php
index c336d2c7..553ffd70 100644
--- a/common/models/EcommerceWebhook.php
+++ b/common/models/EcommerceWebhook.php
@@ -15,6 +15,7 @@
     ShopifyOrderFulfilledJob,
     ShopifyOrderPaidJob,
     ShopifyOrderPartiallyFulfilledJob,
+    ShopifyOrderRefundJob,
     ShopifyOrderUpdatedJob,
     ShopifyShopRedactJob};
 
@@ -151,6 +152,7 @@ protected function createJobForShopify()
             'orders/fulfilled' => new ShopifyOrderFulfilledJob(['ecommerceWebhookId' => $this->id]),
             'orders/partially_fulfilled' => new ShopifyOrderPartiallyFulfilledJob(['ecommerceWebhookId' => $this->id]),
             'orders/paid' => new ShopifyOrderPaidJob(['ecommerceWebhookId' => $this->id]),
+            'refunds/create' => new ShopifyOrderRefundJob(['ecommerceWebhookId' => $this->id]),
             'app/uninstalled' => new ShopifyAppUninstalledJob(['ecommerceWebhookId' => $this->id]),
             'customers/data_request' => new ShopifyCustomerDataRequestJob(['ecommerceWebhookId' => $this->id]),
             'customers/redact' => new ShopifyCustomerRedactJob(['ecommerceWebhookId' => $this->id]),
diff --git a/common/services/platforms/ShopifyService.php b/common/services/platforms/ShopifyService.php
index 6696c249..f70fb91d 100644
--- a/common/services/platforms/ShopifyService.php
+++ b/common/services/platforms/ShopifyService.php
@@ -74,6 +74,7 @@ class ShopifyService
         'orders/fulfilled',
         'orders/partially_fulfilled',
         'orders/paid',
+        'refunds/create',
         'app/uninstalled'
     ];
 
diff --git a/console/jobs/platforms/webhooks/shopify/ShopifyOrderRefundJob.php b/console/jobs/platforms/webhooks/shopify/ShopifyOrderRefundJob.php
new file mode 100644
index 00000000..af343656
--- /dev/null
+++ b/console/jobs/platforms/webhooks/shopify/ShopifyOrderRefundJob.php
@@ -0,0 +1,30 @@
+refund()) {
+            $this->ecommerceWebhook->setSuccess();
+        } else {
+            $this->ecommerceWebhook->setFailed();
+        }
+    }
+
+    protected function refund(): bool
+    {
+        return true;
+    }
+}

From 343493771545d7cf5cbbe103d2ed1ec6c5203f50 Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 28 Mar 2023 16:05:52 +0300
Subject: [PATCH 80/81] Updated docs

---
 intro-docs/ecommerce-platfroms.md | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/intro-docs/ecommerce-platfroms.md b/intro-docs/ecommerce-platfroms.md
index 428464c0..f8b7a62a 100644
--- a/intro-docs/ecommerce-platfroms.md
+++ b/intro-docs/ecommerce-platfroms.md
@@ -21,11 +21,8 @@
 > php yii queue/listen --verbose
 
 ### Cron:
-
-1. See `console\controllers\CronController.php` -> `runEcommerceIntegrations()`.
-We need this method to pull initial existing orders from a needed E-commerce platform.
-   
-2. See `console\controllers\CronController.php` -> `runEcommerceWebhooks()`.
+ 
+1. See `console\controllers\CronController.php` -> `runEcommerceWebhooks()`.
 We need this method to process received webhooks (`status=received`).
    
 > php yii cron/frequent

From 0ddcacd2a027b84d7e892a3130c4356bc048ce1e Mon Sep 17 00:00:00 2001
From: Bohdan 
Date: Tue, 28 Mar 2023 16:07:54 +0300
Subject: [PATCH 81/81] Cron Controller update

---
 console/controllers/CronController.php | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/console/controllers/CronController.php b/console/controllers/CronController.php
index 2e7608dc..9cc35925 100755
--- a/console/controllers/CronController.php
+++ b/console/controllers/CronController.php
@@ -3,13 +3,10 @@
 namespace console\controllers;
 
 use common\models\BulkAction;
-use common\models\EcommerceIntegration;
-use common\models\EcommercePlatform;
 use common\models\EcommerceWebhook;
 use common\models\FulfillmentMeta;
 use common\models\Order;
 use common\models\ScheduledOrder;
-use common\services\platforms\ShopifyService;
 use console\jobs\orders\FetchJob;
 use console\jobs\orders\SendTo3PLJob;
 use yii\console\{Controller, ExitCode};