Shopify/Providers/ShopifyServiceProvider.php

526 lines
17 KiB
PHP

<?php
namespace Modules\Shopify\Providers;
use Carbon\Carbon;
use App\Mailbox;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
// Module alias.
define('SHOPIFY_MODULE', 'shopify');
class ShopifyServiceProvider extends ServiceProvider
{
const MAX_ORDERS = 5;
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
\Log::info('[Shopify] ServiceProvider boot() method called');
$this->registerConfig();
$this->registerViews();
$this->registerFactories();
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
$this->hooks();
}
/**
* Module hooks.
*/
public function hooks()
{
\Log::info('[Shopify] Service Provider hooks() method called');
// Add module's CSS file to the application layout.
\Eventy::addFilter('stylesheets', function($styles) {
$styles[] = \Module::getPublicPath(SHOPIFY_MODULE).'/css/module.css';
return $styles;
});
// Add module's JS file to the application layout.
\Eventy::addFilter('javascripts', function($javascripts) {
$javascripts[] = \Module::getPublicPath(SHOPIFY_MODULE).'/js/laroute.js';
$javascripts[] = \Module::getPublicPath(SHOPIFY_MODULE).'/js/module.js';
return $javascripts;
});
// Add item to the mailbox menu.
\Eventy::addAction('mailboxes.settings.menu', function($mailbox) {
if (auth()->user()->isAdmin()) {
echo \View::make('shopify::partials/settings_menu', ['mailbox' => $mailbox])->render();
}
}, 34);
// Section settings.
\Eventy::addFilter('settings.sections', function($sections) {
$sections[SHOPIFY_MODULE] = ['title' => 'Shopify', 'icon' => 'shopping-cart', 'order' => 550];
return $sections;
}, 35);
// Section parameters.
\Eventy::addFilter('settings.section_params', function($params, $section) {
if ($section != SHOPIFY_MODULE) {
return $params;
}
$params['settings'] = [
'shopify.shop_domain' => [
'env' => 'SHOPIFY_SHOP_DOMAIN',
],
'shopify.access_token' => [
'env' => 'SHOPIFY_ACCESS_TOKEN',
],
'shopify.api_version' => [
'env' => 'SHOPIFY_API_VERSION',
],
];
// Validation.
// $params['validator_rules'] = [
// 'settings.shopify\.shop_domain' => 'required',
// ];
return $params;
}, 20, 2);
// Settings view.
\Eventy::addFilter('settings.view', function($view, $section) {
if ($section != SHOPIFY_MODULE) {
return $view;
} else {
return 'shopify::settings';
}
}, 20, 2);
// Section settings.
\Eventy::addFilter('settings.section_settings', function($settings, $section) {
if ($section != SHOPIFY_MODULE) {
return $settings;
}
$settings['shopify.shop_domain'] = config('shopify.shop_domain');
$settings['shopify.access_token'] = config('shopify.access_token');
$settings['shopify.api_version'] = config('shopify.api_version');
$mailboxes_enabled = \Auth::user()->mailboxesCanView(true);
foreach ($mailboxes_enabled as $i => $mailbox) {
if (!self::isMailboxApiEnabled($mailbox)) {
$mailboxes_enabled->forget($i);
}
}
$settings['mailboxes_enabled'] = $mailboxes_enabled;
return $settings;
}, 20, 2);
// Before saving settings.
\Eventy::addFilter('settings.before_save', function($request, $section, $settings) {
if ($section != SHOPIFY_MODULE) {
return $request;
}
if (!empty($request->settings['shopify.shop_domain'])) {
$settings = $request->settings;
$settings['shopify.shop_domain'] = preg_replace("/https?:\/\//i", '', $settings['shopify.shop_domain']);
if (!\Helper::sanitizeRemoteUrl('https://'.$settings['shopify.shop_domain'])) {
$settings['shopify.shop_domain'] = '';
}
$request->merge([
'settings' => $settings,
]);
}
return $request;
}, 20, 3);
// After saving settings.
\Eventy::addFilter('settings.after_save', function($response, $request, $section, $settings) {
if ($section != SHOPIFY_MODULE) {
return $response;
}
if (self::isApiEnabled()) {
// Check API credentials - create dummy customer object for testing
$test_customer = new \stdClass();
$test_customer->shopify_customer_id = null;
$result = self::apiGetOrders('test@example.org', $test_customer);
if (!empty($result['error'])) {
$request->session()->flash('flash_error', __('Error occurred connecting to the API').': '.$result['error']);
} else {
$request->session()->flash('flash_success', __('Successfully connected to the API.'));
}
}
return $response;
}, 20, 4);
// Show recent orders.
\Eventy::addAction('conversation.after_prev_convs', function($customer, $conversation, $mailbox) {
\Log::info('[Shopify] Hook triggered - Customer: ' . ($customer ? $customer->id : 'null'));
$load = false;
$orders = [];
if (!$customer) {
\Log::info('[Shopify] Hook: No customer object');
return;
}
// Check all customer emails.
$customer_emails = $customer->emails_cached->pluck('email');
if (!count($customer_emails)) {
\Log::info('[Shopify] Hook: No customer emails found');
return;
}
$global_enabled = \Shopify::isApiEnabled();
$mailbox_enabled = \Shopify::isMailboxApiEnabled($mailbox);
\Log::info('[Shopify] Hook: Global enabled: ' . ($global_enabled ? 'yes' : 'no') . ', Mailbox enabled: ' . ($mailbox_enabled ? 'yes' : 'no'));
if (!$global_enabled && !$mailbox_enabled) {
\Log::info('[Shopify] Hook: API not enabled, skipping widget');
return;
}
// Initialize variables
$orders = [];
$load = true;
$url = '';
// Check cache for existing orders
foreach ($customer_emails as $customer_email) {
if (self::isMailboxApiEnabled($mailbox)) {
$settings = self::getMailboxShopifySettings($mailbox);
$url = $settings['shop_domain'] ?? '';
$cached_orders_json = \Cache::get('shopify_orders_'.$mailbox->id.'_'.$customer_email);
} else {
$cached_orders_json = \Cache::get('shopify_orders_'.$customer_email);
}
if ($cached_orders_json && is_array($cached_orders_json)) {
$orders = $cached_orders_json;
$load = false;
break;
}
}
// if (self::isApiEnabled()) {
// $result = self::apiGetOrders($customer_email);
// if (!empty($result['error'])) {
// \Log::error('[WooCommerce] '.$result['error']);
// } elseif (!empty($result['data'])) {
// $orders = $result['data'];
// // Cache orders for an hour.
// \Cache::put('wc_orders_'.$customer_email, $orders, now()->addMinutes(60));
// }
// }
echo \View::make('shopify::partials/orders', [
'orders' => $orders,
'customer_emails' => $customer_emails,
'load' => $load,
'url' => \Shopify::getSanitizedShopDomain($url),
])->render();
}, 12, 3);
// Custom menu in conversation.
\Eventy::addAction('conversation.customer.menu', function($customer, $conversation) {
?>
<li role="presentation" class="col3-hidden"><a data-toggle="collapse" href=".shopify-collapse-orders" tabindex="-1" role="menuitem"><?php echo __("Recent Orders") ?></a></li>
<?php
}, 12, 2);
}
public static function isApiEnabled()
{
return (config('shopify.shop_domain') && config('shopify.access_token') && config('shopify.api_version'));
}
public static function isMailboxApiEnabled($mailbox)
{
if (empty($mailbox) || empty($mailbox->shopify)) {
return false;
}
$settings = self::getMailboxShopifySettings($mailbox);
return (!empty($settings['shop_domain']) && !empty($settings['access_token']) && !empty($settings['api_version']));
}
public static function getMailboxShopifySettings($mailbox)
{
return json_decode($mailbox->shopify ?: '', true);
}
public static function formatDate($date)
{
$date_carbon = Carbon::parse($date);
if (!$date_carbon) {
return '';
}
return $date_carbon->format('M j, Y');
}
public static function getSanitizedShopDomain($shop_domain = '')
{
if (empty($shop_domain)) {
$shop_domain = config('shopify.shop_domain');
}
// Remove protocol if present
$shop_domain = preg_replace("/https?:\/\//i", '', $shop_domain);
// Remove trailing slash if present
$shop_domain = rtrim($shop_domain, '/');
return 'https://' . $shop_domain;
}
/**
* Retrieve Shopify orders for customer.
* Uses customer ID caching to optimize API calls.
*/
public static function apiGetOrders($customer_email, $customer, $mailbox = null)
{
$response = [
'error' => '',
'data' => [],
];
// Get credentials (global or per-mailbox)
if ($mailbox && \Shopify::isMailboxApiEnabled($mailbox)) {
$settings = self::getMailboxShopifySettings($mailbox);
$shop_domain = $settings['shop_domain'];
$access_token = $settings['access_token'];
$api_version = $settings['api_version'];
\Log::info('[Shopify] Using mailbox settings - Domain: ' . $shop_domain . ', Version: ' . $api_version . ', Token: ' . substr($access_token, 0, 10) . '...');
} else {
$shop_domain = config('shopify.shop_domain');
$access_token = config('shopify.access_token');
$api_version = config('shopify.api_version');
\Log::info('[Shopify] Using global settings - Domain: ' . $shop_domain . ', Version: ' . $api_version . ', Token: ' . substr($access_token, 0, 10) . '...');
}
$shop_url = self::getSanitizedShopDomain($shop_domain);
\Log::info('[Shopify] Shop URL: ' . $shop_url . ', Customer: ' . $customer_email);
// OPTIMIZATION: Check if we already have Shopify customer ID cached
$shopify_customer_id = $customer->shopify_customer_id ?? null;
if (!$shopify_customer_id) {
// Step 1: Lookup customer by email
$customer_search_url = $shop_url . '/admin/api/' . $api_version . '/customers/search.json?query=email:' . urlencode($customer_email);
$customer_result = self::makeShopifyApiCall($customer_search_url, $access_token);
if (!empty($customer_result['error'])) {
return ['error' => $customer_result['error'], 'data' => []];
}
if (empty($customer_result['data']['customers'][0]['id'])) {
// Customer not found in Shopify
return ['error' => '', 'data' => []];
}
$shopify_customer_id = $customer_result['data']['customers'][0]['id'];
// Store customer ID for future use
$customer->shopify_customer_id = $shopify_customer_id;
$customer->save();
}
// Step 2: Fetch orders by customer ID (using customer-specific orders endpoint)
$orders_url = $shop_url . '/admin/api/' . $api_version . '/customers/' . $shopify_customer_id . '/orders.json?status=any&limit=' . self::MAX_ORDERS;
$orders_result = self::makeShopifyApiCall($orders_url, $access_token);
if (!empty($orders_result['error'])) {
return ['error' => $orders_result['error'], 'data' => []];
}
return ['error' => '', 'data' => $orders_result['data']['orders'] ?? []];
}
/**
* Make Shopify API call with authentication.
*/
private static function makeShopifyApiCall($url, $access_token)
{
$response = ['error' => '', 'data' => []];
try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
\Helper::setCurlDefaultOptions($ch);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-Shopify-Access-Token: ' . $access_token,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_USERAGENT, config('app.curl_user_agent') ?: 'FreeScout-Shopify-Integration');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$json = curl_exec($ch);
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$json_decoded = json_decode($json, true);
\Log::info('[Shopify] API Response - Status: ' . $status_code . ', URL: ' . $url);
\Log::info('[Shopify] API Response Body: ' . substr($json, 0, 500));
if ($status_code == 200) {
$response['data'] = $json_decoded;
} else {
$response['error'] = 'HTTP Status Code: ' . $status_code . ' (' . self::errorCodeDescr($status_code) . ')';
if (!empty($json_decoded['errors'])) {
$response['error'] .= ' | API Error: ' . json_encode($json_decoded['errors']);
}
}
} catch (\Exception $e) {
$response['error'] = $e->getMessage();
}
if ($response['error']) {
$response['error'] .= ' | Requested resource: ' . $url;
}
return $response;
}
public static function errorCodeDescr($code)
{
switch ($code) {
case 400:
$descr = __('Bad request');
break;
case 401:
case 403:
$descr = __('Authentication error. Check your Admin API access token and ensure it has the correct permissions.');
break;
case 0:
case 404:
$descr = __('Shop not found. Verify your shop domain is correct (e.g., mystore.myshopify.com)');
break;
case 429:
$descr = __('Shopify API rate limit exceeded. Please try again in a moment.');
break;
case 500:
$descr = __('Internal shop error');
break;
default:
$descr = __('Unknown error');
break;
}
return $descr;
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerTranslations();
}
/**
* Register config.
*
* @return void
*/
protected function registerConfig()
{
$this->publishes([
__DIR__.'/../Config/config.php' => config_path('shopify.php'),
], 'config');
$this->mergeConfigFrom(
__DIR__.'/../Config/config.php', 'shopify'
);
}
/**
* Register views.
*
* @return void
*/
public function registerViews()
{
$viewPath = resource_path('views/modules/woocommerce');
$sourcePath = __DIR__.'/../Resources/views';
$this->publishes([
$sourcePath => $viewPath
],'views');
$this->loadViewsFrom(array_merge(array_map(function ($path) {
return $path . '/modules/woocommerce';
}, \Config::get('view.paths')), [$sourcePath]), 'shopify');
}
/**
* Register translations.
*
* @return void
*/
public function registerTranslations()
{
$this->loadJsonTranslationsFrom(__DIR__ .'/../Resources/lang');
}
/**
* Register an additional directory of factories.
* @source https://github.com/sebastiaanluca/laravel-resource-flow/blob/develop/src/Modules/ModuleServiceProvider.php#L66
*/
public function registerFactories()
{
if (! app()->environment('production')) {
app(Factory::class)->load(__DIR__ . '/../Database/factories');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
}