namespace App\Services;
use GuzzleHttp\Client;
use App\Models\Product;
use App\Models\Collection;
use App\Models\Condition;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\Order;
use App\Models\LineItem;
use App\Models\Address;
use App\Models\ShopifyVariant;
use App\Models\ShopifyOption;
use App\Models\ShopifyImage;
use App\Models\User;
class ShopifyService
{
protected $client;
protected $apiKey;
protected $password;
protected $shopDomain;
public function __construct()
{
$this->client = new Client();
$this->apiKey = config('shopify.api_key');
$this->apiPassword = config('shopify.password');
$this->shopDomain = config('shopify.shop_domain');
$this->apiVersion = config('shopify.api_version');
$this->client = new Client([
'base_uri' => "https://{$this->shopDomain}.myshopify.com/admin/api/{$this->apiVersion}/",
'auth' => [$this->apiKey, $this->apiPassword],
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]);
}
public function get($endpoint, $params = [])
{
$response = $this->client->get("https://{$this->shopDomain}.myshopify.com/admin/api/2023-04/{$endpoint}", [
'auth' => [$this->apiKey, $this->apiPassword],
'query' => $params,
]);
return json_decode($response->getBody()->getContents(), true);
}
public function post($endpoint, $data = [])
{
$response = $this->client->post("https://{$this->shopDomain}.myshopify.com/admin/api/2023-04/{$endpoint}", [
'auth' => [$this->apiKey, $this->apiPassword],
'json' => $data,
]);
return json_decode($response->getBody()->getContents(), true);
}
public function getProduct($id)
{
try {
$response = $this->client->get("products/{$id}.json");
$product = json_decode($response->getBody()->getContents(), true)['product'];
dd($product);
return $product;
} catch (\Exception $e) {
throw new \Exception('Failed to fetch product: ' . $e->getMessage());
}
}
public function getProductsByCollection($collectionId)
{
$response = $this->client->get("collections/{$collectionId}/products.json");
return json_decode($response->getBody()->getContents(), true);
}
public function syncProducts()
{
$response = $this->client->get('products.json');
$products = json_decode($response->getBody()->getContents(), true)['products'];
foreach ($products as $product) {
$productModel = Product::updateOrCreate(
['shopify_id' => $product['id']],
[
'title' => $product['title'],
'body_html' => $product['body_html'],
'vendor' => $product['vendor'],
'product_type' => $product['product_type'],
'handle' => $product['handle'],
'created_at_shopify' => $product['created_at'],
'updated_at_shopify' => $product['updated_at'],
'published_at' => $product['published_at'],
'template_suffix' => $product['template_suffix'],
'published_scope' => $product['published_scope'],
'tags' => $product['tags'],
'status' => $product['status'],
'admin_graphql_api_id' => $product['admin_graphql_api_id'],
]
);
// Sync product collections
$this->syncProductCollections($productModel, $product['id']);
// Sync product variants
$this->syncProductVariants($productModel, $product['variants']);
// Sync product options
$this->syncProductOptions($productModel, $product['options']);
// Sync product images and link primary image
$this->syncProductImages($productModel, $product['images']);
if (isset($product['image'])) {
$primaryImage = ShopifyImage::where('shopify_id', $product['image']['id'])->first();
if ($primaryImage) {
$productModel->primary_image_id = $primaryImage->id;
$productModel->save();
}
}
}
}
protected function syncProductImages(Product $product, $images)
{
foreach ($images as $image) {
ShopifyImage::updateOrCreate(
['shopify_id' => $image['id']],
[
'product_id' => $product->id,
'src' => $image['src'],
'width' => $image['width'],
'height' => $image['height'],
'alt' => $image['alt'],
'position' => $image['position'],
'admin_graphql_api_id' => $image['admin_graphql_api_id'],
]
);
}
}
public function syncCollections()
{
$response = $this->client->get('custom_collections.json');
$collections = json_decode($response->getBody()->getContents(), true)['custom_collections'];
foreach ($collections as $collection) {
// echo($collection['title']. '
');
Collection::updateOrCreate(
['shopify_id' => $collection['id']],
[
'title' => $collection['title'],
'body_html' => $collection['body_html'],
'handle' => $collection['handle'],
]
);
}
}
protected function syncProductCollections(Product $product, $shopifyProductId)
{
// Get collection associations for this product using the collects endpoint
$response = $this->client->get("collects.json?product_id={$shopifyProductId}");
$collects = json_decode($response->getBody()->getContents(), true)['collects'];
$collectionIds = [];
foreach ($collects as $collect) {
$collectionModel = Collection::where('shopify_id', $collect['collection_id'])->first();
if ($collectionModel) {
$collectionIds[] = $collectionModel->id;
}
}
// Sync the product-collection relationships
$product->collections()->sync($collectionIds);
}
protected function syncProductVariants(Product $product, $variants)
{
foreach ($variants as $variant) {
ShopifyVariant::updateOrCreate(
['shopify_id' => $variant['id']],
[
'product_id' => $product->id,
'title' => $variant['title'],
'sku' => $variant['sku'],
'price' => $variant['price'],
'position' => $variant['position'],
'inventory_policy' => $variant['inventory_policy'],
'compare_at_price' => $variant['compare_at_price'],
'fulfillment_service' => $variant['fulfillment_service'],
'inventory_management' => $variant['inventory_management'],
'option1' => $variant['option1'],
'option2' => $variant['option2'],
'option3' => $variant['option3'],
'taxable' => $variant['taxable'],
'barcode' => $variant['barcode'],
'grams' => $variant['grams'],
'weight' => $variant['weight'],
'weight_unit' => $variant['weight_unit'],
'inventory_item_id' => $variant['inventory_item_id'],
'inventory_quantity' => $variant['inventory_quantity'],
'old_inventory_quantity' => $variant['old_inventory_quantity'],
'requires_shipping' => $variant['requires_shipping'],
'admin_graphql_api_id' => $variant['admin_graphql_api_id'],
'image_id' => $variant['image_id']
]
);
}
}
protected function syncProductOptions(Product $product, $options)
{
foreach ($options as $option) {
ShopifyOption::updateOrCreate(
['shopify_id' => $option['id']],
[
'product_id' => $product->id,
'name' => $option['name'],
'position' => $option['position']
]
);
}
}
public function getCustomers()
{
$response = $this->client->get('customers.json');
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody()->getContents(), true)['customers'];
}
return [];
}
public function syncCustomers()
{
$customers = $this->getCustomers();
foreach ($customers as $customer) {
if ($this->isSegmentCustomer($customer)) {
User::updateOrCreate(
['shopify_id' => $customer['id']],
[
'name' => $customer['first_name'] . ' ' . $customer['last_name'],
'email' => $customer['email'],
'password' => bcrypt('defaultpassword'), // Set a default password or handle password management
'first_name' => $customer['first_name'],
'last_name' => $customer['last_name'],
'phone' => $customer['phone'],
'tags' => $customer['tags'],
'is_admin' => 0,
'business_id' => $customer['business_id'] ?? null,
]
);
}
}
}
private function isSegmentCustomer($customer)
{
// Define the criteria for segment customers
return in_array('B2B', explode(',', $customer['tags']));
}
public function getDiscounts()
{
$response = $this->client->get('price_rules.json');
$priceRules = json_decode($response->getBody()->getContents(), true)['price_rules'];
$discounts = [];
foreach ($priceRules as $rule) {
$discounts[] = [
'id' => $rule['id'],
'title' => $rule['title'],
'value' => $rule['value'],
'value_type' => $rule['value_type'],
'starts_at' => $rule['starts_at'],
'ends_at' => $rule['ends_at'],
'usage_limit' => $rule['usage_limit'],
'created_at' => $rule['created_at'],
'updated_at' => $rule['updated_at']
];
}
return $discounts;
}
public function getOrders()
{
$response = $this->client->get('orders.json');
return json_decode($response->getBody()->getContents(), true)['orders'];
}
public function getDraftOrders()
{
$response = $this->client->get('draft_orders.json');
return json_decode($response->getBody()->getContents(), true)['draft_orders'];
}
public function syncDiscounts()
{
try {
$response = $this->client->get('price_rules.json');
$priceRules = json_decode($response->getBody()->getContents(), true)['price_rules'];
foreach ($priceRules as $rule) {
Discount::updateOrCreate(
['shopify_id' => $rule['id']],
[
'title' => $rule['title'],
'value_type' => $rule['value_type'],
'value' => $rule['value'],
'customer_selection' => $rule['customer_selection'],
'target_type' => $rule['target_type'],
'target_selection' => $rule['target_selection'],
'allocation_method' => $rule['allocation_method'],
'allocation_limit' => $rule['allocation_limit'],
'once_per_customer' => $rule['once_per_customer'],
'usage_limit' => $rule['usage_limit'],
'starts_at' => $rule['starts_at'],
'ends_at' => $rule['ends_at'],
'created_at' => $rule['created_at'],
'updated_at' => $rule['updated_at'],
'entitled_product_ids' => json_encode($rule['entitled_product_ids']),
'entitled_variant_ids' => json_encode($rule['entitled_variant_ids']),
'entitled_collection_ids' => json_encode($rule['entitled_collection_ids']),
'entitled_country_ids' => json_encode($rule['entitled_country_ids']),
'prerequisite_product_ids' => json_encode($rule['prerequisite_product_ids']),
'prerequisite_variant_ids' => json_encode($rule['prerequisite_variant_ids']),
'prerequisite_collection_ids' => json_encode($rule['prerequisite_collection_ids']),
'customer_segment_prerequisite_ids' => json_encode($rule['customer_segment_prerequisite_ids']),
'prerequisite_customer_ids' => json_encode($rule['prerequisite_customer_ids']),
'prerequisite_subtotal_range' => json_encode($rule['prerequisite_subtotal_range']),
'prerequisite_quantity_range' => json_encode($rule['prerequisite_quantity_range']),
'prerequisite_shipping_price_range' => json_encode($rule['prerequisite_shipping_price_range']),
'prerequisite_to_entitlement_quantity_ratio' => json_encode($rule['prerequisite_to_entitlement_quantity_ratio']),
'prerequisite_to_entitlement_purchase' => json_encode($rule['prerequisite_to_entitlement_purchase']),
'admin_graphql_api_id' => $rule['admin_graphql_api_id']
]
);
}
} catch (\Exception $e) {
throw new \Exception('Failed to sync discounts: ' . $e->getMessage());
}
}
public function getDiscount($id)
{
try {
$response = $this->client->get("price_rules/{$id}.json");
$discount = json_decode($response->getBody()->getContents(), true)['price_rule'];
return $discount;
} catch (\Exception $e) {
throw new \Exception('Failed to fetch discount: ' . $e->getMessage());
}
}
public function getOrderList()
{
try {
$response = $this->client->get('orders.json');
$orders = json_decode($response->getBody()->getContents(), true)['orders'];
return array_map(function ($order) {
return [
'id' => $order['id'],
'created_at' => $order['created_at'],
'shopify_id' => $order['id']
];
}, $orders);
} catch (\Exception $e) {
throw new \Exception('Failed to fetch orders: ' . $e->getMessage());
}
}
public function getOrderDetails($id)
{
try {
$response = $this->client->get("orders/{$id}.json");
$order = json_decode($response->getBody()->getContents(), true)['order'];
return $order;
} catch (\Exception $e) {
throw new \Exception('Failed to fetch order details: ' . $e->getMessage());
}
}
public function getDraftOrder($id)
{
try {
$response = $this->client->get("draft_orders/{$id}.json");
$draftOrder = json_decode($response->getBody()->getContents(), true)['draft_order'];
return $draftOrder;
} catch (\Exception $e) {
throw new \Exception('Failed to fetch draft order: ' . $e->getMessage());
}
}
public function syncDraftOrders()
{
$orders = $this->getDraftOrders();
foreach ($orders as $orderData) {
// Sync customer
if (isset($orderData['customer'])) {
$customerData = $orderData['customer'];
$user = User::updateOrCreate(
['shopify_id' => $customerData['id']],
[
'email' => $customerData['email'] ?? '',
'name' => ($customerData['first_name'] ?? '') . ' ' . ($customerData['last_name'] ?? ''),
'first_name' => $customerData['first_name'] ?? '',
'last_name' => $customerData['last_name'] ?? '',
'orders_count' => $customerData['orders_count'] ?? 0,
'phone' => $customerData['phone'] ?? '',
'password' => bcrypt('defaultpassword'), // Set a default password
'total_spent' => $customerData['total_spent'] ?? 0,
'tags' => json_encode($customerData['tags'] ?? []),
]
);
$user = User::where('shopify_id', $customerData['id'])->first();
} else {
continue; // Skip order if customer data is not available
}
$order = Order::updateOrCreate(
['shopify_id' => $orderData['id']],
[
'note' => $orderData['note'] ?? '',
'email' => $orderData['email'] ?? '',
'taxes_included' => $orderData['taxes_included'] ?? false,
'currency' => $orderData['currency'] ?? '',
'invoice_sent_at' => $orderData['invoice_sent_at'] ?? null,
'created_at_shopify' => $orderData['created_at'] ?? null,
'updated_at_shopify' => $orderData['updated_at'] ?? null,
'tax_exempt' => $orderData['tax_exempt'] ?? false,
'completed_at' => $orderData['completed_at'] ?? null,
'name' => $orderData['name'] ?? '',
'status' => $orderData['status'] ?? '',
'subtotal_price' => $orderData['subtotal_price'] ?? 0,
'total_price' => $orderData['total_price'] ?? 0,
'total_tax' => $orderData['total_tax'] ?? 0,
'invoice_url' => $orderData['invoice_url'] ?? '',
'order_id' => $orderData['order_id'] ?? null,
'shipping_title' => $orderData['shipping_line']['title'] ?? '',
'shipping_price' => $orderData['shipping_line']['price'] ?? 0,
'user_id' => $user->id,
]
);
// Sync line items
if (isset($orderData['line_items'])) {
foreach ($orderData['line_items'] as $lineItemData) {
LineItem::updateOrCreate(
['shopify_id' => $lineItemData['id']],
[
'order_id' => $order->id,
'variant_id' => $lineItemData['variant_id'] ?? null,
'product_id' => $lineItemData['product_id'] ?? null,
'title' => $lineItemData['title'] ?? '',
'variant_title' => $lineItemData['variant_title'] ?? '',
'sku' => $lineItemData['sku'] ?? '',
'vendor' => $lineItemData['vendor'] ?? '',
'quantity' => $lineItemData['quantity'] ?? 0,
'requires_shipping' => $lineItemData['requires_shipping'] ?? false,
'taxable' => $lineItemData['taxable'] ?? false,
'gift_card' => $lineItemData['gift_card'] ?? false,
'fulfillment_service' => $lineItemData['fulfillment_service'] ?? '',
'grams' => $lineItemData['grams'] ?? 0,
'price' => $lineItemData['price'] ?? 0,
'tax_lines' => json_encode($lineItemData['tax_lines'] ?? []),
'properties' => json_encode($lineItemData['properties'] ?? []),
]
);
}
}
// Sync shipping address
if (isset($orderData['shipping_address'])) {
Address::updateOrCreate(
['order_id' => $order->id, 'type' => 'shipping'],
[
'first_name' => $orderData['shipping_address']['first_name'] ?? '',
'last_name' => $orderData['shipping_address']['last_name'] ?? '',
'company' => $orderData['shipping_address']['company'] ?? '',
'address1' => $orderData['shipping_address']['address1'] ?? '',
'address2' => $orderData['shipping_address']['address2'] ?? '',
'city' => $orderData['shipping_address']['city'] ?? '',
'province' => $orderData['shipping_address']['province'] ?? '',
'country' => $orderData['shipping_address']['country'] ?? '',
'zip' => $orderData['shipping_address']['zip'] ?? '',
'phone' => $orderData['shipping_address']['phone'] ?? '',
'name' => $orderData['shipping_address']['name'] ?? '',
'country_code' => $orderData['shipping_address']['country_code'] ?? '',
'province_code' => $orderData['shipping_address']['province_code'] ?? '',
]
);
}
// Sync billing address
if (isset($orderData['billing_address'])) {
Address::updateOrCreate(
['order_id' => $order->id, 'type' => 'billing'],
[
'first_name' => $orderData['billing_address']['first_name'] ?? '',
'last_name' => $orderData['billing_address']['last_name'] ?? '',
'company' => $orderData['billing_address']['company'] ?? '',
'address1' => $orderData['billing_address']['address1'] ?? '',
'address2' => $orderData['billing_address']['address2'] ?? '',
'city' => $orderData['billing_address']['city'] ?? '',
'province' => $orderData['billing_address']['province'] ?? '',
'country' => $orderData['billing_address']['country'] ?? '',
'zip' => $orderData['billing_address']['zip'] ?? '',
'phone' => $orderData['billing_address']['phone'] ?? '',
'name' => $orderData['billing_address']['name'] ?? '',
'country_code' => $orderData['billing_address']['country_code'] ?? '',
'province_code' => $orderData['billing_address']['province_code'] ?? '',
]
);
}
}
}
public function saveCart($cartCollection)
{
// Assuming we have a logged-in user
$user = auth()->user();
// Format the data to match Shopify's Draft Order requirements
$lineItems = [];
foreach ($cartCollection as $item) {
// Retrieve the correct Shopify variant ID
$shopifyVariant = ShopifyVariant::where('id', $item->id)->first();
if (!$shopifyVariant) {
\Log::error('Invalid variant ID:', [$item->id]);
continue; // Skip this item if the variant ID is not valid
}
// Log the Shopify variant ID for debugging purposes
\Log::info('Shopify Variant ID:', [$shopifyVariant->shopify_id]);
// Get price with conditions applied
$lineItemPrice = $item->getPriceWithConditions();
// Create discounts for item-specific conditions
$appliedDiscount = null;
foreach ($item->conditions as $condition) {
if ($condition->getType() === 'discount') {
$discount = $this->createShopifyDiscount($condition, $shopifyVariant->shopify_id, $user);
if ($discount) {
$appliedDiscount = $discount;
}
}
}
// Assuming attributes are stored in an array format
$properties = [];
foreach ($item->attributes as $key => $value) {
if (is_array($value)) {
// Convert array to a JSON string
$properties[] = [
'name' => $key,
'value' => json_encode($value),
];
} else {
$properties[] = [
'name' => $key,
'value' => (string)$value, // Convert value to string
];
}
}
$lineItems[] = [
'variant_id' => $shopifyVariant->shopify_id,
'quantity' => $item->quantity,
'price' => (string)$lineItemPrice, // Adjusted price with conditions
'title' => $item->name,
'properties' => $properties,
'applied_discount' => $appliedDiscount,
];
}
// Calculate conditions for the cart total
$cartTotal = \Cart::getTotal();
$cartConditions = \Cart::getConditions();
foreach ($cartConditions as $condition) {
// Assuming conditions are either discounts (negative) or additional charges (positive)
$conditionValue = $condition->getValue();
if (strpos($conditionValue, '%') !== false) {
// If the condition is a percentage, apply it to the total
$percentage = str_replace('%', '', $conditionValue) / 100;
$cartTotal += $cartTotal * $percentage;
} else {
// If the condition is a fixed amount, add/subtract it directly
$cartTotal += $conditionValue;
}
}
// Include customer information in the draft order
$customerData = [
'email' => $user->email,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'phone' => $user->phone,
];
$draftOrderData = [
'draft_order' => [
'line_items' => $lineItems,
'note' => 'Draft order created from Laravel cart',
'customer' => $customerData, // Add customer data here
'email' => $user->email, // Add customer email here
'total_price' => (string)$cartTotal, // Adjusted cart total with conditions
]
];
// Send request to Shopify Draft Order API
$response = $this->post('draft_orders.json', $draftOrderData);
if (isset($response['draft_order'])) {
return redirect()->back()->with('success', 'Cart saved as draft order successfully.');
} else {
\Log::error('Failed to save draft order:', $response);
return redirect()->back()->with('error', 'Failed to save cart as draft order.');
}
}
public function createShopifyDiscount($condition, $variantId, $user)
{
\Log::info('Creating Shopify discount for user:', [$user->shopify_id]);
// Format the value
$valueType = (strpos($condition->value, '%') !== false) ? 'percentage' : 'fixed_amount';
$value = str_replace('%', '', $condition->value);
$discountData = [
'price_rule' => [
'title' => $condition->name,
'target_type' => 'line_item',
'target_selection' => 'entitled', // Specific to the variant
'allocation_method' => 'across',
'value_type' => $valueType,
'value' => $value,
'customer_selection' => 'prerequisite',
'prerequisite_customer_ids' => [$user->shopify_id],
'entitled_variant_ids' => [
$variantId
],
'starts_at' => now()->toISOString(),
]
];
\Log::info('Discount data being sent to Shopify:', $discountData);
try {
$response = $this->post('price_rules.json', $discountData);
\Log::info('Shopify discount creation response:', $response);
if (isset($response['price_rule'])) {
return [
'price_rule_id' => $response['price_rule']['id'],
'value' => $response['price_rule']['value'],
'value_type' => $response['price_rule']['value_type'],
'allocation_method' => $response['price_rule']['allocation_method'],
];
} else {
\Log::error('Failed to create discount:', $response);
return null;
}
} catch (\Exception $e) {
\Log::error('Exception while creating discount:', ['message' => $e->getMessage(), 'data' => $discountData]);
return null;
}
}
protected function postGraphQL($query, $variables = [])
{
$response = $this->client->post("https://{$this->shopDomain}.myshopify.com/admin/api/graphql.json", [
'auth' => [$this->apiKey, $this->apiPassword],
'json' => ['query' => $query, 'variables' => $variables],
]);
return json_decode($response->getBody()->getContents(), true);
}
public function createDiscountOnShopify(Condition $condition)
{
// Determine the value type based on the condition value
$valueType = 'percentage';
$value = str_replace('%', '', $condition->value);
if (strpos($condition->value, '%') === false) {
$valueType = 'fixed_amount';
}
// Prepare the discount data for the REST API
$discountData = [
'price_rule' => [
'title' => $condition->name,
'target_type' => 'line_item',
'target_selection' => 'entitled',
'allocation_method' => 'across',
'value_type' => $valueType,
'value' => -1 * floatval($value),
'customer_selection' => 'prerequisite',
'prerequisite_customer_ids' => [$condition->customer_id],
'entitled_variant_ids' => [$condition->variant_id],
'starts_at' => now()->toIso8601String(),
],
];
// Send request to Shopify Price Rules API
$response = $this->post('price_rules.json', $discountData);
if (isset($response['price_rule'])) {
$priceRuleId = $response['price_rule']['id'];
$discountCodeData = [
'discount_code' => [
'code' => $condition->name,
],
];
$discountCodeResponse = $this->post("price_rules/{$priceRuleId}/discount_codes.json", $discountCodeData);
if (isset($discountCodeResponse['discount_code'])) {
return $discountCodeResponse['discount_code'];
} else {
\Log::error('Failed to create discount code on Shopify:', $discountCodeResponse);
throw new \Exception('Failed to create discount code on Shopify.');
}
} else {
\Log::error('Failed to create price rule on Shopify:', $response);
throw new \Exception('Failed to create price rule on Shopify.');
}
}
// public function createShopifyDiscount($condition, $variantId, $user)
// {
// \Log::info('Creating Shopify discount for user:', [$user->shopify_id]);
// $discountData = [
// 'price_rule' => [
// 'title' => $condition->getName(),
// 'target_type' => 'line_item',
// 'target_selection' => 'entitled', // Specific to the variant
// 'allocation_method' => 'across',
// 'value_type' => (strpos($condition->getValue(), '%') !== false) ? 'percentage' : 'fixed_amount',
// 'value' => str_replace('%', '', $condition->getValue()),
// 'customer_selection' => 'prerequisite',
// 'prerequisite_customer_ids' => [$user->shopify_id],
// 'entitled_variant_ids' => [
// $variantId
// ],
// 'starts_at' => now()->toISOString(),
// ]
// ];
// \Log::info('Discount data being sent to Shopify:', $discountData);
// try {
// $response = $this->post('price_rules.json', $discountData);
// \Log::info('Shopify discount creation response:', $response);
// if (isset($response['price_rule'])) {
// return [
// 'price_rule_id' => $response['price_rule']['id'],
// 'value' => $response['price_rule']['value'],
// 'value_type' => $response['price_rule']['value_type'],
// 'allocation_method' => $response['price_rule']['allocation_method'],
// ];
// } else {
// \Log::error('Failed to create discount:', $response);
// return null;
// }
// } catch (\Exception $e) {
// \Log::error('Exception while creating discount:', ['message' => $e->getMessage(), 'data' => $discountData]);
// return null;
// }
// }
}