spree_google_products 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +177 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_google_products_manifest.js +5 -0
- data/app/assets/images/app_icons/google_logo.svg +1 -0
- data/app/assets/images/app_icons/google_merchant_logo.svg +52 -0
- data/app/assets/images/app_icons/welcome_scene.svg +1 -0
- data/app/controllers/spree/admin/google_merchant_settings_controller.rb +182 -0
- data/app/controllers/spree/admin/google_shopping/dashboard_controller.rb +64 -0
- data/app/controllers/spree/admin/google_shopping/issues_controller.rb +42 -0
- data/app/controllers/spree/admin/google_shopping/products_controller.rb +79 -0
- data/app/controllers/spree/admin/google_shopping/taxons_controller.rb +39 -0
- data/app/helpers/spree/admin/google_shopping_helper.rb +39 -0
- data/app/javascript/spree_google_products/application.js +16 -0
- data/app/javascript/spree_google_products/controllers/spree_google_products_controller.js +7 -0
- data/app/jobs/spree/google_shopping/fetch_status_job.rb +23 -0
- data/app/jobs/spree/google_shopping/sync_all_job.rb +18 -0
- data/app/jobs/spree/google_shopping/sync_product_job.rb +35 -0
- data/app/jobs/spree_google_products/base_job.rb +5 -0
- data/app/models/spree/google_credential.rb +29 -0
- data/app/models/spree/google_product_attribute.rb +9 -0
- data/app/models/spree/google_taxon.rb +5 -0
- data/app/models/spree/google_variant_attribute.rb +6 -0
- data/app/models/spree/product_decorator.rb +29 -0
- data/app/models/spree/store_decorator.rb +9 -0
- data/app/models/spree/variant_decorator.rb +8 -0
- data/app/models/spree_google_products/ability.rb +10 -0
- data/app/services/spree/google_shopping/content_service.rb +215 -0
- data/app/services/spree/google_shopping/status_service.rb +150 -0
- data/app/services/spree/google_token_service.rb +59 -0
- data/app/views/spree/admin/google_merchant_settings/edit.html.erb +331 -0
- data/app/views/spree/admin/google_shopping/dashboard/index.html.erb +121 -0
- data/app/views/spree/admin/google_shopping/issues/index.html.erb +106 -0
- data/app/views/spree/admin/google_shopping/products/_filters.html.erb +19 -0
- data/app/views/spree/admin/google_shopping/products/edit.html.erb +336 -0
- data/app/views/spree/admin/google_shopping/products/index.html.erb +131 -0
- data/app/views/spree/admin/google_shopping/products/issues.html.erb +48 -0
- data/app/views/spree/admin/products/google_shopping.html.erb +63 -0
- data/app/views/spree_google_products/_head.html.erb +1 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/assets.rb +8 -0
- data/config/initializers/force_encryption_keys.rb +24 -0
- data/config/initializers/spree.rb +32 -0
- data/config/initializers/spree_google_products.rb +29 -0
- data/config/locales/en.yml +35 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20260112000000_add_spree_google_shopping_tables.rb +89 -0
- data/lib/generators/spree_google_products/install/install_generator.rb +84 -0
- data/lib/generators/spree_google_products/uninstall/uninstall_generator.rb +55 -0
- data/lib/spree_google_products/configuration.rb +13 -0
- data/lib/spree_google_products/engine.rb +37 -0
- data/lib/spree_google_products/factories.rb +6 -0
- data/lib/spree_google_products/version.rb +7 -0
- data/lib/spree_google_products.rb +13 -0
- data/lib/tasks/spree_google_products.rake +41 -0
- metadata +190 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module GoogleShopping
|
|
4
|
+
class ProductsController < Spree::Admin::ResourceController
|
|
5
|
+
|
|
6
|
+
def model_class
|
|
7
|
+
Spree::Product
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
per_page = params[:per_page].to_i > 0 ? params[:per_page].to_i : 25
|
|
12
|
+
|
|
13
|
+
@search = Spree::Product.accessible_by(current_ability, :read).ransack(params[:q])
|
|
14
|
+
|
|
15
|
+
@collection = @search.result(distinct: true)
|
|
16
|
+
.includes(:google_product_attribute)
|
|
17
|
+
.page(params[:page])
|
|
18
|
+
.per(per_page)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def edit
|
|
22
|
+
@product = Spree::Product.friendly.find(params[:id])
|
|
23
|
+
@product.build_google_product_attribute unless @product.google_product_attribute
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
@product = Spree::Product.friendly.find(params[:id])
|
|
28
|
+
if @product.update(product_params)
|
|
29
|
+
|
|
30
|
+
Spree::GoogleShopping::SyncProductJob.perform_later(@product.id)
|
|
31
|
+
|
|
32
|
+
flash[:success] = "Product updated. Syncing to Google Merchant Center"
|
|
33
|
+
redirect_to admin_google_shopping_product_path(@product)
|
|
34
|
+
else
|
|
35
|
+
flash[:error] = "Could not update product"
|
|
36
|
+
render :edit
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def issues
|
|
41
|
+
@product = Spree::Product.friendly.find(params[:id])
|
|
42
|
+
|
|
43
|
+
@variant = Spree::Variant.find_by(id: params[:variant_id])
|
|
44
|
+
|
|
45
|
+
if @variant
|
|
46
|
+
|
|
47
|
+
@google_attr = Spree::GoogleVariantAttribute.find_by(variant_id: @variant.id)
|
|
48
|
+
@issues = @google_attr&.google_issues || []
|
|
49
|
+
else
|
|
50
|
+
flash[:error] = "Variant not found."
|
|
51
|
+
redirect_to admin_google_shopping_product_path(@product)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def product_params
|
|
58
|
+
params.require(:product).permit(
|
|
59
|
+
google_product_attribute_attributes: [
|
|
60
|
+
:id,
|
|
61
|
+
:brand,
|
|
62
|
+
:product_type,
|
|
63
|
+
:google_product_category,
|
|
64
|
+
:gender,
|
|
65
|
+
:age_group,
|
|
66
|
+
:condition,
|
|
67
|
+
:gtin,
|
|
68
|
+
:mpn,
|
|
69
|
+
:sale_start_at,
|
|
70
|
+
:sale_end_at,
|
|
71
|
+
:min_handling_time,
|
|
72
|
+
:max_handling_time
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module GoogleShopping
|
|
4
|
+
class TaxonsController < Spree::Admin::BaseController
|
|
5
|
+
|
|
6
|
+
def drill_down
|
|
7
|
+
parent_path = params[:parent_path]
|
|
8
|
+
|
|
9
|
+
if parent_path.blank?
|
|
10
|
+
|
|
11
|
+
categories = Spree::GoogleTaxon.pluck(:name).map { |n| n.split(' > ').first }.uniq.sort
|
|
12
|
+
else
|
|
13
|
+
|
|
14
|
+
prefix = "#{parent_path} > "
|
|
15
|
+
|
|
16
|
+
categories = Spree::GoogleTaxon
|
|
17
|
+
.where("name LIKE ?", "#{prefix}%")
|
|
18
|
+
.pluck(:name)
|
|
19
|
+
.map { |n| n.sub(prefix, '').split(' > ').first }
|
|
20
|
+
.uniq
|
|
21
|
+
.sort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
current_taxon_id = nil
|
|
25
|
+
if parent_path.present?
|
|
26
|
+
taxon = Spree::GoogleTaxon.find_by(name: parent_path)
|
|
27
|
+
current_taxon_id = taxon&.google_id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
render json: {
|
|
31
|
+
categories: categories,
|
|
32
|
+
current_id: current_taxon_id,
|
|
33
|
+
is_leaf: categories.empty?
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module GoogleShoppingHelper
|
|
4
|
+
|
|
5
|
+
def google_supported_countries_options
|
|
6
|
+
[
|
|
7
|
+
['United States (US)', 'US'], ['India (IN)', 'IN'], ['United Kingdom (GB)', 'GB'],
|
|
8
|
+
['Canada (CA)', 'CA'], ['Australia (AU)', 'AU'], ['Germany (DE)', 'DE'],
|
|
9
|
+
['France (FR)', 'FR'], ['Italy (IT)', 'IT'], ['Spain (ES)', 'ES'],
|
|
10
|
+
['Netherlands (NL)', 'NL'], ['Brazil (BR)', 'BR'], ['Japan (JP)', 'JP'],
|
|
11
|
+
['Mexico (MX)', 'MX'], ['Austria (AT)', 'AT'], ['Belgium (BE)', 'BE'],
|
|
12
|
+
['Denmark (DK)', 'DK'], ['Finland (FI)', 'FI'], ['Greece (GR)', 'GR'],
|
|
13
|
+
['Hungary (HU)', 'HU'], ['Indonesia (ID)', 'ID'], ['Ireland (IE)', 'IE'],
|
|
14
|
+
['Israel (IL)', 'IL'], ['Malaysia (MY)', 'MY'], ['New Zealand (NZ)', 'NZ'],
|
|
15
|
+
['Norway (NO)', 'NO'], ['Philippines (PH)', 'PH'], ['Poland (PL)', 'PL'],
|
|
16
|
+
['Portugal (PT)', 'PT'], ['Romania (RO)', 'RO'], ['Russia (RU)', 'RU'],
|
|
17
|
+
['Saudi Arabia (SA)', 'SA'], ['Singapore (SG)', 'SG'], ['South Africa (ZA)', 'ZA'],
|
|
18
|
+
['South Korea (KR)', 'KR'], ['Sweden (SE)', 'SE'], ['Switzerland (CH)', 'CH'],
|
|
19
|
+
['Taiwan (TW)', 'TW'], ['Thailand (TH)', 'TH'], ['Turkey (TR)', 'TR'],
|
|
20
|
+
['Ukraine (UA)', 'UA'], ['United Arab Emirates (AE)', 'AE'], ['Vietnam (VN)', 'VN']
|
|
21
|
+
# Add others if needed based on GMC docs
|
|
22
|
+
].sort
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def google_supported_currencies_options
|
|
26
|
+
[
|
|
27
|
+
['US Dollar (USD)', 'USD'], ['Indian Rupee (INR)', 'INR'], ['British Pound (GBP)', 'GBP'],
|
|
28
|
+
['Euro (EUR)', 'EUR'], ['Canadian Dollar (CAD)', 'CAD'], ['Australian Dollar (AUD)', 'AUD'],
|
|
29
|
+
['Japanese Yen (JPY)', 'JPY'], ['Brazilian Real (BRL)', 'BRL'], ['Mexican Peso (MXN)', 'MXN'],
|
|
30
|
+
['Swiss Franc (CHF)', 'CHF'], ['Hong Kong Dollar (HKD)', 'HKD'], ['New Zealand Dollar (NZD)', 'NZD'],
|
|
31
|
+
['Polish Zloty (PLN)', 'PLN'], ['Russian Ruble (RUB)', 'RUB'], ['Singapore Dollar (SGD)', 'SGD'],
|
|
32
|
+
['South African Rand (ZAR)', 'ZAR'], ['South Korean Won (KRW)', 'KRW'], ['Swedish Krona (SEK)', 'SEK'],
|
|
33
|
+
['Turkish Lira (TRY)', 'TRY'], ['Danish Krone (DKK)', 'DKK'], ['Norwegian Krone (NOK)', 'NOK']
|
|
34
|
+
].sort
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import '@hotwired/turbo-rails'
|
|
2
|
+
import { Application } from '@hotwired/stimulus'
|
|
3
|
+
|
|
4
|
+
let application
|
|
5
|
+
|
|
6
|
+
if (typeof window.Stimulus === "undefined") {
|
|
7
|
+
application = Application.start()
|
|
8
|
+
application.debug = false
|
|
9
|
+
window.Stimulus = application
|
|
10
|
+
} else {
|
|
11
|
+
application = window.Stimulus
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
import SpreeGoogleProductsController from 'spree_google_products/controllers/spree_google_products_controller'
|
|
15
|
+
|
|
16
|
+
application.register('spree_google_products', SpreeGoogleProductsController)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module GoogleShopping
|
|
3
|
+
class FetchStatusJob < SpreeGoogleProducts::BaseJob
|
|
4
|
+
#queue_as :default
|
|
5
|
+
|
|
6
|
+
def perform
|
|
7
|
+
credential = Spree::Store.default.google_credential
|
|
8
|
+
return unless credential&.active?
|
|
9
|
+
|
|
10
|
+
service = Spree::GoogleShopping::ContentService.new(credential)
|
|
11
|
+
|
|
12
|
+
Spree::GoogleVariantAttribute.where.not(google_status: nil).find_each(batch_size: 50) do |g_attr|
|
|
13
|
+
variant = Spree::Variant.find_by(id: g_attr.variant_id)
|
|
14
|
+
next unless variant
|
|
15
|
+
|
|
16
|
+
service.fetch_product_status(variant)
|
|
17
|
+
|
|
18
|
+
sleep 0.2
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module GoogleShopping
|
|
3
|
+
class SyncAllJob < SpreeGoogleProducts::BaseJob
|
|
4
|
+
#queue_as :default
|
|
5
|
+
|
|
6
|
+
def perform
|
|
7
|
+
credential = Spree::Store.default.google_credential
|
|
8
|
+
return unless credential&.active?
|
|
9
|
+
|
|
10
|
+
service = Spree::GoogleShopping::ContentService.new(credential)
|
|
11
|
+
|
|
12
|
+
Spree::Product.active.find_each(batch_size: 50) do |product|
|
|
13
|
+
service.push_product(product)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module GoogleShopping
|
|
3
|
+
class SyncProductJob < SpreeGoogleProducts::BaseJob
|
|
4
|
+
#queue_as :default
|
|
5
|
+
|
|
6
|
+
def perform(product_id)
|
|
7
|
+
product = Spree::Product.find_by(id: product_id)
|
|
8
|
+
unless product
|
|
9
|
+
Rails.logger.error "GOOGLE SYNC JOB: Product ID #{product_id} not found."
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
credential = Spree::Store.default.google_credential
|
|
14
|
+
unless credential&.active?
|
|
15
|
+
Rails.logger.warn "GOOGLE SYNC JOB: Credential missing or inactive. Aborting."
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rails.logger.info "GOOGLE SYNC JOB: Starting sync for Product '#{product.name}' (ID: #{product.id})..."
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
service = Spree::GoogleShopping::ContentService.new(credential)
|
|
23
|
+
service.push_product(product)
|
|
24
|
+
|
|
25
|
+
credential.update_column(:last_sync_at, Time.current)
|
|
26
|
+
|
|
27
|
+
Rails.logger.info "GOOGLE SYNC JOB: ✅ Completed successfully for Product #{product.id}."
|
|
28
|
+
rescue => e
|
|
29
|
+
Rails.logger.error "GOOGLE SYNC JOB: ❌ Failed for Product #{product.id}. Error: #{e.message}"
|
|
30
|
+
Rails.logger.error e.backtrace.join("\n")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class GoogleCredential < Spree::Base
|
|
3
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
4
|
+
|
|
5
|
+
validates :merchant_center_id, presence: true, on: :update
|
|
6
|
+
|
|
7
|
+
validates :target_country, length: { is: 2 }, allow_blank: true
|
|
8
|
+
validates :target_currency, length: { is: 3 }, allow_blank: true
|
|
9
|
+
|
|
10
|
+
validates :store_id, presence: true, uniqueness: true
|
|
11
|
+
|
|
12
|
+
if respond_to?(:encrypts)
|
|
13
|
+
encrypts :access_token
|
|
14
|
+
encrypts :refresh_token
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def active?
|
|
18
|
+
refresh_token.present?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def expired?
|
|
22
|
+
token_expires_at.nil? || token_expires_at <= Time.current
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ready_for_sync?
|
|
26
|
+
active? && merchant_center_id.present?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class GoogleProductAttribute < Spree::Base
|
|
3
|
+
belongs_to :product, class_name: 'Spree::Product'
|
|
4
|
+
serialize :google_issues, coder: JSON
|
|
5
|
+
validates :gender, inclusion: { in: %w[male female unisex], allow_blank: true }
|
|
6
|
+
validates :age_group, inclusion: { in: %w[adult kids toddler infant], allow_blank: true }
|
|
7
|
+
validates :condition, inclusion: { in: %w[new refurbished used], allow_blank: true }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module ProductDecorator
|
|
3
|
+
def self.prepended(base)
|
|
4
|
+
base.has_one :google_product_attribute, class_name: 'Spree::GoogleProductAttribute', dependent: :destroy
|
|
5
|
+
base.accepts_nested_attributes_for :google_product_attribute, allow_destroy: true
|
|
6
|
+
base.after_initialize :ensure_google_attribute
|
|
7
|
+
base.after_commit :sync_to_google, on: [:create, :update]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def ensure_google_attribute
|
|
13
|
+
build_google_product_attribute if google_product_attribute.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def sync_to_google
|
|
17
|
+
Spree::GoogleShopping::SyncProductJob.perform_later(self.id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sync_to_google_shopping
|
|
21
|
+
credential = Spree::Store.default.google_credential
|
|
22
|
+
return unless credential&.ready_for_sync?
|
|
23
|
+
return if saved_changes.keys.include?('google_product_attribute_updated_at')
|
|
24
|
+
Spree::GoogleShopping::SyncProductJob.perform_later(self.id)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Spree::Product.prepend(Spree::ProductDecorator)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require 'google/apis/content_v2_1'
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module GoogleShopping
|
|
5
|
+
class ContentService
|
|
6
|
+
def initialize(credential)
|
|
7
|
+
@credential = credential
|
|
8
|
+
@service = Google::Apis::ContentV2_1::ShoppingContentService.new
|
|
9
|
+
@service.authorization = Spree::GoogleTokenService.new(@credential).token
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def push_product(product)
|
|
13
|
+
([product.master] + product.variants).each do |variant|
|
|
14
|
+
price = price_object_for(variant)
|
|
15
|
+
if price.nil? || price.amount.blank? || price.amount.zero?
|
|
16
|
+
next
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
product_obj = build_product_payload(variant)
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
@service.insert_product(@credential.merchant_center_id, product_obj)
|
|
23
|
+
update_status(variant, 'pending', [])
|
|
24
|
+
rescue Google::Apis::ClientError => e
|
|
25
|
+
handle_google_error(variant, e)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fetch_product_status(variant)
|
|
31
|
+
return unless variant.sku.present?
|
|
32
|
+
|
|
33
|
+
g_id = "online:en:#{@credential.target_country}:#{variant.sku}"
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
|
|
37
|
+
product_status = @service.get_productstatus(@credential.merchant_center_id, g_id)
|
|
38
|
+
|
|
39
|
+
shopping_status = product_status.destination_statuses.find { |s| s.destination == "Shopping" }
|
|
40
|
+
|
|
41
|
+
final_status = case shopping_status&.status
|
|
42
|
+
when 'approved' then 'active'
|
|
43
|
+
when 'disapproved' then 'disapproved'
|
|
44
|
+
when 'pending' then 'pending'
|
|
45
|
+
else 'pending'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
issues = product_status.item_level_issues.map do |issue|
|
|
49
|
+
{ "description" => issue.description, "detail" => issue.detail, "code" => issue.code }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
update_status(variant, final_status, issues)
|
|
53
|
+
Rails.logger.info "GOOGLE STATUS FETCH: #{variant.sku} is now '#{final_status}'"
|
|
54
|
+
|
|
55
|
+
rescue Google::Apis::ClientError => e
|
|
56
|
+
if e.status_code == 404
|
|
57
|
+
update_status(variant, 'not_found_on_google', [])
|
|
58
|
+
else
|
|
59
|
+
Rails.logger.error "GOOGLE STATUS ERROR: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def price_object_for(variant)
|
|
67
|
+
variant.price_in(@credential.target_currency)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_product_payload(variant)
|
|
71
|
+
product = variant.product
|
|
72
|
+
g_prod_attr = product.google_product_attribute
|
|
73
|
+
g_var_attr = variant.google_variant_attribute
|
|
74
|
+
|
|
75
|
+
brand = g_prod_attr&.brand.presence || product.property('brand') || 'Generic'
|
|
76
|
+
|
|
77
|
+
raw_product_type = g_prod_attr&.product_type.presence || @credential.default_product_type
|
|
78
|
+
product_types_list = raw_product_type.present? ? [raw_product_type] : []
|
|
79
|
+
|
|
80
|
+
google_category_id = g_prod_attr&.google_product_category.presence || @credential.default_google_product_category
|
|
81
|
+
|
|
82
|
+
raw_gtin = g_var_attr&.gtin.presence || g_prod_attr&.gtin.presence
|
|
83
|
+
gtin = (raw_gtin && raw_gtin.length >= 8) ? raw_gtin : nil
|
|
84
|
+
|
|
85
|
+
mpn = g_var_attr&.mpn.presence || g_prod_attr&.mpn.presence || variant.sku
|
|
86
|
+
|
|
87
|
+
gender = g_prod_attr&.gender.presence
|
|
88
|
+
age_group = g_prod_attr&.age_group.presence
|
|
89
|
+
condition = g_prod_attr&.condition.presence || 'new'
|
|
90
|
+
|
|
91
|
+
price_obj = price_object_for(variant)
|
|
92
|
+
current_price = price_obj.amount
|
|
93
|
+
original_price = price_obj.compare_at_amount
|
|
94
|
+
|
|
95
|
+
google_price = nil
|
|
96
|
+
google_sale_price = nil
|
|
97
|
+
sale_date_range = nil
|
|
98
|
+
|
|
99
|
+
if original_price.present? && original_price > current_price
|
|
100
|
+
google_price = { value: original_price.to_s, currency: @credential.target_currency }
|
|
101
|
+
google_sale_price = { value: current_price.to_s, currency: @credential.target_currency }
|
|
102
|
+
|
|
103
|
+
start_date = g_prod_attr&.sale_start_at
|
|
104
|
+
end_date = g_prod_attr&.sale_end_at
|
|
105
|
+
|
|
106
|
+
if start_date.present? && end_date.present?
|
|
107
|
+
start_iso = start_date.beginning_of_day.iso8601
|
|
108
|
+
end_iso = end_date.end_of_day.iso8601
|
|
109
|
+
sale_date_range = "#{start_iso}/#{end_iso}"
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
google_price = { value: current_price.to_s, currency: @credential.target_currency }
|
|
113
|
+
google_sale_price = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
product_input_weight = nil
|
|
117
|
+
if variant.weight.present? && variant.weight > 0
|
|
118
|
+
product_input_weight = Google::Apis::ContentV2_1::ProductShippingWeight.new(
|
|
119
|
+
value: variant.weight.to_f,
|
|
120
|
+
unit: 'kg'
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
product_input_length = nil
|
|
125
|
+
product_input_width = nil
|
|
126
|
+
product_input_height = nil
|
|
127
|
+
|
|
128
|
+
if variant.depth.present? && variant.width.present? && variant.height.present?
|
|
129
|
+
product_input_length = Google::Apis::ContentV2_1::ProductShippingDimension.new(value: variant.depth.to_f, unit: 'cm')
|
|
130
|
+
product_input_width = Google::Apis::ContentV2_1::ProductShippingDimension.new(value: variant.width.to_f, unit: 'cm')
|
|
131
|
+
product_input_height = Google::Apis::ContentV2_1::ProductShippingDimension.new(value: variant.height.to_f, unit: 'cm')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
min_days = g_prod_attr&.min_handling_time.presence || @credential.default_min_handling_time
|
|
135
|
+
max_days = g_prod_attr&.max_handling_time.presence || @credential.default_max_handling_time
|
|
136
|
+
|
|
137
|
+
shipping_array = []
|
|
138
|
+
if min_days.present? && max_days.present?
|
|
139
|
+
shipping_array << Google::Apis::ContentV2_1::ProductShipping.new(
|
|
140
|
+
country: @credential.target_country,
|
|
141
|
+
service: "Standard", # Required field
|
|
142
|
+
price: Google::Apis::ContentV2_1::Price.new(value: "0.00", currency: @credential.target_currency), # FIX: Added Price
|
|
143
|
+
min_handling_time: min_days.to_i,
|
|
144
|
+
max_handling_time: max_days.to_i
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
final_link = product_url(product)
|
|
149
|
+
final_image_link = image_url(variant)
|
|
150
|
+
|
|
151
|
+
Google::Apis::ContentV2_1::Product.new(
|
|
152
|
+
offer_id: variant.sku,
|
|
153
|
+
title: product.name,
|
|
154
|
+
description: product.description&.truncate(5000) || product.name,
|
|
155
|
+
|
|
156
|
+
link: final_link,
|
|
157
|
+
image_link: final_image_link,
|
|
158
|
+
|
|
159
|
+
content_language: 'en',
|
|
160
|
+
target_country: @credential.target_country,
|
|
161
|
+
channel: 'online',
|
|
162
|
+
availability: variant.in_stock? ? 'in stock' : 'out of stock',
|
|
163
|
+
condition: condition,
|
|
164
|
+
price: google_price,
|
|
165
|
+
sale_price: google_sale_price,
|
|
166
|
+
sale_price_effective_date: sale_date_range,
|
|
167
|
+
shipping_weight: product_input_weight,
|
|
168
|
+
shipping_length: product_input_length,
|
|
169
|
+
shipping_width: product_input_width,
|
|
170
|
+
shipping_height: product_input_height,
|
|
171
|
+
shipping: shipping_array.presence,
|
|
172
|
+
brand: brand,
|
|
173
|
+
product_types: product_types_list,
|
|
174
|
+
google_product_category: google_category_id,
|
|
175
|
+
gtin: gtin,
|
|
176
|
+
mpn: mpn,
|
|
177
|
+
identifier_exists: gtin.present?,
|
|
178
|
+
gender: gender,
|
|
179
|
+
age_group: age_group,
|
|
180
|
+
item_group_id: product.id.to_s
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def product_url(product)
|
|
185
|
+
url = Spree::Core::Engine.routes.url_helpers.product_url(product, host: Spree::Store.default.url)
|
|
186
|
+
url.sub(/^http:/, 'https:')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def image_url(variant)
|
|
190
|
+
image = variant.images.first || variant.product.master.images.first
|
|
191
|
+
return "" unless image
|
|
192
|
+
url = Rails.application.routes.url_helpers.rails_blob_url(image.attachment, host: Spree::Store.default.url)
|
|
193
|
+
url.sub(/^http:/, 'https:')
|
|
194
|
+
rescue
|
|
195
|
+
""
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def update_status(variant, status, issues)
|
|
199
|
+
attr = Spree::GoogleVariantAttribute.find_or_initialize_by(variant_id: variant.id)
|
|
200
|
+
attr.google_status = status
|
|
201
|
+
attr.google_issues = issues
|
|
202
|
+
attr.last_synced_at = Time.current
|
|
203
|
+
attr.save!
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def handle_google_error(variant, error)
|
|
207
|
+
error_json = JSON.parse(error.body) rescue {}
|
|
208
|
+
message = error_json.dig('error', 'message') || error.message
|
|
209
|
+
|
|
210
|
+
update_status(variant, 'disapproved', [{ "description" => "API Error", "detail" => message }])
|
|
211
|
+
Rails.logger.error "GOOGLE SYNC ERROR (Variant #{variant.id}): #{message}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|