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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +177 -0
  3. data/Rakefile +21 -0
  4. data/app/assets/config/spree_google_products_manifest.js +5 -0
  5. data/app/assets/images/app_icons/google_logo.svg +1 -0
  6. data/app/assets/images/app_icons/google_merchant_logo.svg +52 -0
  7. data/app/assets/images/app_icons/welcome_scene.svg +1 -0
  8. data/app/controllers/spree/admin/google_merchant_settings_controller.rb +182 -0
  9. data/app/controllers/spree/admin/google_shopping/dashboard_controller.rb +64 -0
  10. data/app/controllers/spree/admin/google_shopping/issues_controller.rb +42 -0
  11. data/app/controllers/spree/admin/google_shopping/products_controller.rb +79 -0
  12. data/app/controllers/spree/admin/google_shopping/taxons_controller.rb +39 -0
  13. data/app/helpers/spree/admin/google_shopping_helper.rb +39 -0
  14. data/app/javascript/spree_google_products/application.js +16 -0
  15. data/app/javascript/spree_google_products/controllers/spree_google_products_controller.js +7 -0
  16. data/app/jobs/spree/google_shopping/fetch_status_job.rb +23 -0
  17. data/app/jobs/spree/google_shopping/sync_all_job.rb +18 -0
  18. data/app/jobs/spree/google_shopping/sync_product_job.rb +35 -0
  19. data/app/jobs/spree_google_products/base_job.rb +5 -0
  20. data/app/models/spree/google_credential.rb +29 -0
  21. data/app/models/spree/google_product_attribute.rb +9 -0
  22. data/app/models/spree/google_taxon.rb +5 -0
  23. data/app/models/spree/google_variant_attribute.rb +6 -0
  24. data/app/models/spree/product_decorator.rb +29 -0
  25. data/app/models/spree/store_decorator.rb +9 -0
  26. data/app/models/spree/variant_decorator.rb +8 -0
  27. data/app/models/spree_google_products/ability.rb +10 -0
  28. data/app/services/spree/google_shopping/content_service.rb +215 -0
  29. data/app/services/spree/google_shopping/status_service.rb +150 -0
  30. data/app/services/spree/google_token_service.rb +59 -0
  31. data/app/views/spree/admin/google_merchant_settings/edit.html.erb +331 -0
  32. data/app/views/spree/admin/google_shopping/dashboard/index.html.erb +121 -0
  33. data/app/views/spree/admin/google_shopping/issues/index.html.erb +106 -0
  34. data/app/views/spree/admin/google_shopping/products/_filters.html.erb +19 -0
  35. data/app/views/spree/admin/google_shopping/products/edit.html.erb +336 -0
  36. data/app/views/spree/admin/google_shopping/products/index.html.erb +131 -0
  37. data/app/views/spree/admin/google_shopping/products/issues.html.erb +48 -0
  38. data/app/views/spree/admin/products/google_shopping.html.erb +63 -0
  39. data/app/views/spree_google_products/_head.html.erb +1 -0
  40. data/config/importmap.rb +6 -0
  41. data/config/initializers/assets.rb +8 -0
  42. data/config/initializers/force_encryption_keys.rb +24 -0
  43. data/config/initializers/spree.rb +32 -0
  44. data/config/initializers/spree_google_products.rb +29 -0
  45. data/config/locales/en.yml +35 -0
  46. data/config/routes.rb +33 -0
  47. data/db/migrate/20260112000000_add_spree_google_shopping_tables.rb +89 -0
  48. data/lib/generators/spree_google_products/install/install_generator.rb +84 -0
  49. data/lib/generators/spree_google_products/uninstall/uninstall_generator.rb +55 -0
  50. data/lib/spree_google_products/configuration.rb +13 -0
  51. data/lib/spree_google_products/engine.rb +37 -0
  52. data/lib/spree_google_products/factories.rb +6 -0
  53. data/lib/spree_google_products/version.rb +7 -0
  54. data/lib/spree_google_products.rb +13 -0
  55. data/lib/tasks/spree_google_products.rake +41 -0
  56. metadata +190 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5d3d6b3ccd94d66bee77b1760f4b1a6943b942a329c85dbc8b9f0750540e53a
4
+ data.tar.gz: eb9538793bd6233441a77c8892de2a43c5da94960697e7fd9ad6fe426953976e
5
+ SHA512:
6
+ metadata.gz: a7656f0f6f6ce4f5645a8a315cf2fcc035be97a912c576c4b973a7d504723853665c4aae3d62c1968461654af36472ba911a23b6afe037b29572edcd16d42a12
7
+ data.tar.gz: 11ad2e4b1590638d8c14fcdec637fa2b6bafdd8fb3907d58c45c653162fa8dd38bfd9229075e521da91120ba3dc28aa5b5b399468bedac35181a5c27fc733dde
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Spree Google Shopping (Merchant Center)<br>
2
+
3
+ ![logo_light_mc2](https://github.com/user-attachments/assets/8eecad8c-107d-46a5-8e62-4e061286f251)
4
+
5
+ <br>
6
+ A production-grade Spree Commerce extension that integrates directly with Google Merchant Center. Automatically sync products, variants, inventory, and pricing in real-time using the Content API v2.1
7
+
8
+ <br>
9
+ Includes a full Admin Dashboard for tracking approval status, fixing data quality issues, and managing OAuth connections securely.
10
+
11
+ <br>
12
+
13
+ ## 🚀 Key Features
14
+ 1. Real-Time Sync: Products are pushed to Google immediately upon update/create via background jobs.<br>
15
+ 2. OAuth 2.0 Integration: Secure, token-based authentication (No static JSON key files required).<br>
16
+ 3. Admin Dashboard: Live visualization of Approved, Pending, and Disapproved products.<br>
17
+ 4. Data Quality Issues: View specific error messages from Google (e.g., "Missing GTIN", "Image too small") directly in Spree.<br>
18
+ 5. Granular Control:<br>
19
+ • Set Global Defaults (Target Country, Currency, Shipping Rules).<br>
20
+ • Override attributes per Product (Brand, MPN, GTIN, Gender, Age Group).
21
+ 6. Drill-Down Taxonomy: Built-in selector for Google's official Product Taxonomy (6,000+ categories).
22
+ 7. Secure: Uses Rails 7 Active Record Encryption to store OAuth tokens safely.
23
+
24
+ <br>
25
+
26
+ ## 📦 Installation
27
+ 1. Add this line to your application's Gemfile:
28
+ ```ruby
29
+ gem 'spree_google_products', github: 'your-username/spree_google_products'
30
+ ```
31
+
32
+ 2. Install the gem:
33
+ ```ruby
34
+ bundle install
35
+ ```
36
+
37
+ 3. Run the Installer:
38
+ This command will copy migrations, generate encryption keys, and seed the Google Taxonomy database.
39
+ ```bash
40
+ bundle exec rails g spree_google_products:install
41
+ ```
42
+
43
+ Follow the on-screen prompts to run migrations and seed data.
44
+
45
+ <br>
46
+
47
+ ## ⚙️ Configuration
48
+ ### Google Cloud Console Setup<br>
49
+ Before using the extension, you must create credentials in Google Cloud.<br>
50
+ 1. Go to [Google Cloud Console.](https://console.cloud.google.com/)<br>
51
+ 2. Create a new project (or select existing).<br>
52
+ 3. Enable API: Go to "APIs & Services" > "Library" -> Search for "Content API for Shopping" -> Enable it.<br>
53
+ 4. Create Credentials:<br>
54
+
55
+ • Go to "APIs & Services" > "Credentials".<br>
56
+
57
+ • Click Create Credentials -> OAuth Client ID.<br>
58
+
59
+ • Application Type: Web Application.<br>
60
+
61
+ • Authorized Redirect URI: https://your-store.com/admin/google_merchant_settings/callback<br>
62
+ (Note: Use http://localhost:3000/... for local development).<br>
63
+ 5. Copy the Client ID and Client Secret.<br>
64
+ <br>
65
+ <img width="900" height="auto" alt="Screenshot 2026-01-11 at 11 58 54 PM" src="https://github.com/user-attachments/assets/bff8a7cc-050d-4fa1-9bfe-190719ae984a" />
66
+ <br>
67
+
68
+ ### Environment Variables<br>
69
+ The installer automatically adds these to your .env file. Fill in your Google credentials:<br>
70
+
71
+ ```bash
72
+ GOOGLE_CLIENT_ID=your_client_id_here
73
+ GOOGLE_CLIENT_SECRET=your_client_secret_here
74
+ ```
75
+
76
+ <br>
77
+
78
+ ### Connect in Spree Admin
79
+ 1. Login to your Spree Admin Panel.
80
+ 2. Navigate to Google Shopping > Settings (Sidebar).
81
+ 3. Click Connect Account and log in with the Google Account that manages your Merchant Center.
82
+ 4. Once connected, enter your Merchant Center ID and Target Country/Currency.
83
+ 5. Click Save Settings.
84
+ <br>
85
+ <img width="900" height="auto" alt="Screenshot 2026-01-11 at 11 59 17 PM" src="https://github.com/user-attachments/assets/fea6f8c2-dd1e-443c-8906-dd61869b8226" />
86
+ <br>
87
+
88
+ ## 🛠️ Usage
89
+
90
+ ### Dashboard
91
+ Visit Google Shopping > Dashboard to see a live overview of your product feed health.
92
+ • Approved: Live on Google Shopping.
93
+
94
+ • Limited: Live but restricted (e.g., adult content, partial regions).
95
+
96
+ • Disapproved: Critical issues preventing display.
97
+
98
+ • Sync Now: Force a full catalog sync manually.
99
+
100
+ <img width="900" height="auto" alt="Screenshot 2026-01-12 at 12 43 34 AM" src="https://github.com/user-attachments/assets/b53174b2-37ee-42ea-9526-00630b46c60e" />
101
+
102
+ <br>
103
+
104
+
105
+ ### Managing Products
106
+ You can manage Google-specific attributes in the Product Edit tab under "Google Shopping Attributes".
107
+ • GTIN / MPN: Crucial for approval. If your products have barcodes, enter them here.
108
+
109
+ • Google Category: Use the drill-down selector to pick the exact Google Taxonomy ID.
110
+
111
+ • Demographics: Set Gender, Age Group, and Condition (New/Used).
112
+
113
+ <br>
114
+ <img width="900" height="auto" alt="Screenshot 2026-01-12 at 12 51 02 AM" src="https://github.com/user-attachments/assets/37c064cf-d794-42bb-90eb-3ade6ec2bbab" />
115
+ <img width="900" height="auto" alt="Screenshot 2026-01-12 at 12 32 11 AM" src="https://github.com/user-attachments/assets/aee1955d-481f-47d3-a15f-c7a4670f9e78" />
116
+ <img width="871" height="456" alt="Screenshot 2026-01-12 at 12 39 32 AM" src="https://github.com/user-attachments/assets/2a509733-2151-48c0-8b63-2d3db654b797" />
117
+
118
+ <br>
119
+
120
+ ### Background Jobs
121
+ This extension uses ActiveJob. Ensure you have a queue adapter configured (Sidekiq, Solid Queue, or Delayed Job) for production.<br>
122
+ • Spree::GoogleShopping::SyncProductJob: Syncs individual product updates.<br>
123
+ • Spree::GoogleShopping::SyncAllJob: Bulk syncs entire catalog.<br>
124
+ • Spree::GoogleShopping::FetchStatusJob: Pulls approval status from Google.<br>
125
+
126
+ <br>
127
+
128
+ ## 🔒 Security & Encryption
129
+ This plugin strictly adheres to security best practices. OAuth Refresh Tokens are encrypted at rest in the database using Rails Active Record Encryption.<br>
130
+
131
+ The installer generates these keys in your .env file automatically:
132
+
133
+ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY<br>
134
+ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY<br>
135
+ ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT<br>
136
+
137
+ <br>
138
+
139
+ ### ⚠️ IMPORTANT: Keep these keys safe. If you lose them, you will need to disconnect and reconnect your Google Account.
140
+
141
+ <br>
142
+
143
+ ## 🗑️ Uninstallation
144
+ To cleanly remove the extension, drop tables, and cleanup migrations, run:
145
+
146
+ ```bash
147
+ bundle exec rails g spree_google_products:uninstall
148
+ ```
149
+
150
+ This command will ask for confirmation before dropping the Google-related tables.
151
+
152
+ <br>
153
+
154
+ ## 🗺️ Roadmap
155
+ • [x] Google Merchant Center (Content API) Sync<br>
156
+ • [x] Product Approval Status Dashboard<br>
157
+ • [x] Issue Reporting<br>
158
+ • [ ] Google Ads Integration: Create Performance Max campaigns directly from Spree.<br>
159
+ • [ ] Conversion Tracking: Auto-inject Google Ads conversion pixels.<br>
160
+ • [ ] Free Listings: specialized support for "Free Listings" enhanced attributes.<br>
161
+
162
+ <br>
163
+
164
+ ## ❓ FAQ
165
+ Q: I get "Missing Active Record encryption credential" error.
166
+ A: Restart your server. The encryption keys are loaded from .env on boot. If using a custom deployment, ensure the ACTIVE_RECORD_ENCRYPTION_* variables are set in your environment.
167
+
168
+ Q: Why are my products still "Pending" after syncing?
169
+ A: Google takes 3-5 business days to review new products. Check the Dashboard for real-time status updates.
170
+
171
+ Q: Can I sync to multiple countries?
172
+ A: Currently, the extension supports one primary Target Country per store. Multi-country feeds via the "Shipping" attribute are planned for v2.0.
173
+
174
+ <br>
175
+
176
+ ### License
177
+ Copyright (c) 2026. Released under the BSD-3-Clause [License](https://github.com/umeshravani/spree_google_products/blob/main/LICENSE.md).
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir['spec/dummy'].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir('../../')
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'spree_google_products'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,5 @@
1
+ //= link_tree ../images
2
+ //= link spree_google_products/application.js
3
+ //= link_tree ../../javascript/spree_google_products/controllers .js
4
+ //= link_tree ../../../vendor/javascript .js
5
+ //= link_tree ../../../vendor/stylesheets .css
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
@@ -0,0 +1,52 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2
+ <mask id="mask0_2_319" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="30" height="30">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.25H6C4.7625 32.25 3.75 31.2375 3.75 30V6C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.25Z" fill="white"/>
4
+ </mask>
5
+ <g mask="url(#mask0_2_319)">
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 32.25V3.75H32.25V32.25H3.75ZM22.1869 9V8.99625C22.7794 8.96438 23.25 8.48063 23.25 7.8825C23.25 7.28437 22.7794 6.79875 22.1869 6.76688V6.76312H19.4981C19.4981 6.75938 19.4986 6.75563 19.4991 6.75188C19.4995 6.74813 19.5 6.74437 19.5 6.74062C19.5 5.9175 18.8287 5.25 18 5.25C17.1713 5.25 16.5 5.9175 16.5 6.74062C16.5 6.74437 16.5005 6.74813 16.5009 6.75188C16.5014 6.75563 16.5019 6.75938 16.5019 6.76312H13.8131V6.76688C13.2206 6.80063 12.75 7.28625 12.75 7.8825C12.75 8.47875 13.2206 8.96438 13.8131 8.99625V9H22.1869Z" fill="#4285F4"/>
7
+ </g>
8
+ <mask id="mask1_2_319" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="30" height="30">
9
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.25H6C4.7625 32.25 3.75 31.2375 3.75 30V6C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.25Z" fill="white"/>
10
+ </mask>
11
+ <g mask="url(#mask1_2_319)">
12
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 3.75H6C4.7625 3.75 3.75 4.7625 3.75 6V6.1875C3.75 4.95 4.7625 3.9375 6 3.9375H30C31.2375 3.9375 32.25 4.95 32.25 6.1875V6C32.25 4.7625 31.2375 3.75 30 3.75Z" fill="white" fill-opacity="0.2"/>
13
+ </g>
14
+ <mask id="mask2_2_319" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="30" height="30">
15
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.25H6C4.7625 32.25 3.75 31.2375 3.75 30V6C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.25Z" fill="white"/>
16
+ </mask>
17
+ <g mask="url(#mask2_2_319)">
18
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 10.5V3.75H32.25V10.5H3.75ZM22.1869 9V8.99625C22.7794 8.96438 23.25 8.48063 23.25 7.8825C23.25 7.28437 22.7794 6.79875 22.1869 6.76688V6.76312H19.4981C19.4981 6.75938 19.4986 6.75563 19.4991 6.75188C19.4995 6.74813 19.5 6.74437 19.5 6.74062C19.5 5.9175 18.8287 5.25 18 5.25C17.1713 5.25 16.5 5.9175 16.5 6.74062C16.5 6.74437 16.5005 6.74813 16.5009 6.75188C16.5014 6.75563 16.5019 6.75938 16.5019 6.76312H13.8131V6.76688C13.2206 6.80063 12.75 7.28625 12.75 7.8825C12.75 8.47875 13.2206 8.96438 13.8131 8.99625V9H22.1869Z" fill="#3F51B5"/>
19
+ </g>
20
+ <mask id="mask3_2_319" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="30" height="30">
21
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.25H6C4.7625 32.25 3.75 31.2375 3.75 30V6C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.25Z" fill="white"/>
22
+ </mask>
23
+ <g mask="url(#mask3_2_319)">
24
+ <path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M22.1868 9.00937V9.01312H13.8131V9.00937C13.2525 8.97937 12.8006 8.53875 12.7556 7.98187C12.7537 8.01375 12.7518 8.04375 12.7518 8.07562C12.7518 8.67562 13.2225 9.165 13.815 9.19687V9.20062H22.1906V9.19687C22.7831 9.165 23.2537 8.67562 23.2537 8.07562C23.2537 8.04375 23.2518 8.01375 23.25 7.98187C23.1993 8.53687 22.7475 8.9775 22.1868 9.00937Z" fill="white"/>
25
+ </g>
26
+ <mask id="mask4_2_319" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="30" height="30">
27
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.25H6C4.7625 32.25 3.75 31.2375 3.75 30V6C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.25Z" fill="white"/>
28
+ </mask>
29
+ <g mask="url(#mask4_2_319)">
30
+ <path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M22.1869 6.5775V6.57375H19.4981C19.4981 6.56625 19.5 6.55875 19.5 6.55125C19.5 5.7225 18.8288 5.05125 18 5.05125C17.1713 5.05125 16.5 5.7225 16.5 6.55125C16.5 6.55875 16.5019 6.56625 16.5019 6.57375H13.8131V6.5775C13.2206 6.60937 12.75 7.09875 12.75 7.69875C12.75 7.73062 12.7519 7.76062 12.7538 7.7925C12.8006 7.23562 13.2506 6.795 13.8113 6.765V6.76125H16.5C16.5 6.75375 16.4981 6.74625 16.4981 6.73875C16.4981 5.91 17.1694 5.23875 17.9981 5.23875C18.8269 5.23875 19.4981 5.91 19.4981 6.73875C19.4981 6.74625 19.4963 6.75375 19.4963 6.76125H22.185V6.765C22.7456 6.795 23.1975 7.23562 23.2425 7.7925C23.2444 7.76062 23.2463 7.73062 23.2463 7.69875C23.25 7.09875 22.7794 6.60937 22.1869 6.5775Z" fill="#1A237E"/>
31
+ </g>
32
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30 32.0625H6C4.7625 32.0625 3.75 31.05 3.75 29.8125V30C3.75 31.2375 4.7625 32.25 6 32.25H30C31.2375 32.25 32.25 31.2375 32.25 30V29.8125C32.25 31.05 31.2375 32.0625 30 32.0625Z" fill="#1A237E" fill-opacity="0.2"/>
33
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6 3.9375H30C31.2375 3.9375 32.25 4.95 32.25 6.1875V6C32.25 4.7625 31.2375 3.75 30 3.75H6C4.7625 3.75 3.75 4.7625 3.75 6V6.1875C3.75 4.95 4.7625 3.9375 6 3.9375Z" fill="white" fill-opacity="0.2"/>
34
+ <rect opacity="0.2" x="9" y="12.375" width="18.765" height="18.765" fill="url(#pattern0)"/>
35
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M17.67 14.58L24.795 21.705C24.9994 21.9075 25.125 22.1887 25.125 22.5C25.125 22.8094 24.9994 23.0906 24.795 23.295L19.92 28.17C19.7175 28.3744 19.4344 28.5 19.125 28.5C18.8156 28.5 18.5344 28.3744 18.33 28.17L11.205 21.045C11.0006 20.8425 10.875 20.5613 10.875 20.25V15.375C10.875 14.7544 11.3794 14.25 12 14.25H16.875C17.1863 14.25 17.4675 14.3756 17.67 14.58ZM12 16.125C12 16.5394 12.3356 16.875 12.75 16.875C13.1644 16.875 13.5 16.5394 13.5 16.125C13.5 15.7106 13.1644 15.375 12.75 15.375C12.3356 15.375 12 15.7106 12 16.125Z" fill="#F5F5F5"/>
36
+ <path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M17.67 14.58L24.795 21.705C24.9994 21.9075 25.125 22.1887 25.125 22.5C25.125 22.521 25.1234 22.5411 25.1217 22.5616C25.1209 22.5722 25.12 22.5829 25.1194 22.5938C25.0969 22.3219 24.9787 22.0762 24.795 21.8925L17.67 14.7675C17.4675 14.5631 17.1863 14.4375 16.875 14.4375H12C11.3794 14.4375 10.875 14.9419 10.875 15.5625V15.375C10.875 14.7544 11.3794 14.25 12 14.25H16.875C17.1863 14.25 17.4675 14.3756 17.67 14.58ZM13.5 16.125C13.5 16.5394 13.1644 16.875 12.75 16.875C12.3356 16.875 12 16.5394 12 16.125C12 16.1016 12.003 16.0792 12.0061 16.0563C12.0072 16.0481 12.0084 16.0397 12.0094 16.0312C12.0562 16.4006 12.3675 16.6875 12.75 16.6875C13.1325 16.6875 13.4437 16.4006 13.4906 16.0312C13.4916 16.0397 13.4928 16.0481 13.4939 16.0563C13.497 16.0792 13.5 16.1016 13.5 16.125Z" fill="white"/>
37
+ <path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M13.4906 16.2188C13.4437 15.8494 13.1325 15.5625 12.75 15.5625C12.3675 15.5625 12.0562 15.8494 12.0094 16.2188C12.0084 16.2103 12.0072 16.2019 12.0061 16.1937C12.003 16.1708 12 16.1484 12 16.125C12 15.7106 12.3356 15.375 12.75 15.375C13.1644 15.375 13.5 15.7106 13.5 16.125C13.5 16.1484 13.497 16.1708 13.4939 16.1937C13.4928 16.2019 13.4916 16.2103 13.4906 16.2188ZM19.92 28.17L24.795 23.295C24.9787 23.1131 25.0969 22.8675 25.1194 22.5938C25.1231 22.6256 25.125 22.6556 25.125 22.6875C25.125 22.9969 24.9994 23.2781 24.795 23.4825L19.92 28.3575C19.7175 28.5619 19.4344 28.6875 19.125 28.6875C18.8156 28.6875 18.5344 28.5619 18.33 28.3575L11.205 21.2325C11.0006 21.03 10.875 20.7488 10.875 20.4375V20.25C10.875 20.5613 11.0006 20.8425 11.205 21.045L18.33 28.17C18.5325 28.3744 18.8156 28.5 19.125 28.5C19.4344 28.5 19.7156 28.3744 19.92 28.17Z" fill="#1A237E"/>
38
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M32.25 29.1713L24.885 21.8063C25.035 21.9975 25.125 22.2375 25.125 22.5C25.125 22.8113 24.9994 23.0925 24.795 23.295L19.92 28.17C19.7156 28.3744 19.4344 28.5 19.125 28.5C18.8625 28.5 18.6225 28.41 18.4313 28.26L22.4213 32.25H32.25V29.1713Z" fill="url(#paint0_linear_2_319)"/>
39
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M32.25 6V30C32.25 31.2375 31.2375 32.25 30 32.2481H6C4.7625 32.2481 3.75 31.2375 3.75 30V5.99813C3.75 4.7625 4.7625 3.75 6 3.75H30C31.2375 3.75 32.25 4.7625 32.25 6ZM22.1869 9V8.99625C22.7794 8.96438 23.25 8.48063 23.25 7.8825C23.25 7.28437 22.7794 6.79875 22.1869 6.76688V6.76312H19.4981C19.4981 6.75938 19.4986 6.75563 19.4991 6.75188C19.4995 6.74813 19.5 6.74437 19.5 6.74062C19.5 5.9175 18.8287 5.25 18 5.25C17.1713 5.25 16.5 5.9175 16.5 6.74062C16.5 6.74437 16.5005 6.74813 16.5009 6.75188C16.5014 6.75563 16.5019 6.75938 16.5019 6.76312H13.8131V6.76688C13.2206 6.80063 12.75 7.28625 12.75 7.8825C12.75 8.47875 13.2206 8.96438 13.8131 8.99625V9H22.1869Z" fill="url(#paint1_radial_2_319)"/>
40
+ <defs>
41
+ <pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
42
+ </pattern>
43
+ <linearGradient id="paint0_linear_2_319" x1="17.0167" y1="28.6632" x2="22.4013" y2="38.0903" gradientUnits="userSpaceOnUse">
44
+ <stop stop-color="#1A237E" stop-opacity="0.2"/>
45
+ <stop offset="1" stop-color="#1A237E" stop-opacity="0.02"/>
46
+ </linearGradient>
47
+ <radialGradient id="paint1_radial_2_319" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4.6875 4.125) scale(38.1249 38.1224)">
48
+ <stop stop-color="white" stop-opacity="0.1"/>
49
+ <stop offset="1" stop-color="white" stop-opacity="0.01"/>
50
+ </radialGradient>
51
+ </defs>
52
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="316" height="112"><defs><linearGradient id="a" x1="66" y1="90" x2="102" y2="90" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#bdc1c6" stop-opacity="0"/><stop offset=".137" stop-color="#bdc1c6" stop-opacity=".021"/><stop offset=".279" stop-color="#bdc1c6" stop-opacity=".084"/><stop offset=".424" stop-color="#bdc1c6" stop-opacity=".189"/><stop offset=".57" stop-color="#bdc1c6" stop-opacity=".336"/><stop offset=".718" stop-color="#bdc1c6" stop-opacity=".525"/><stop offset=".864" stop-color="#bdc1c6" stop-opacity=".753"/><stop offset="1" stop-color="#bdc1c6"/></linearGradient><linearGradient id="b" data-name="Linear ground shadow - light-B" x1="224" y1="110" x2="246" y2="110" xlink:href="#a"/><linearGradient id="c" data-name="Linear ground shadow - light-B" x1="26" y1="110" x2="58" y2="110" xlink:href="#a"/><linearGradient id="d" x1="65.992" y1="70" x2="65.992" y2="112" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#669df6"/><stop offset=".094" stop-color="#669df6" stop-opacity=".826"/><stop offset=".225" stop-color="#669df6" stop-opacity=".61"/><stop offset=".358" stop-color="#669df6" stop-opacity=".423"/><stop offset=".49" stop-color="#669df6" stop-opacity=".27"/><stop offset=".621" stop-color="#669df6" stop-opacity=".152"/><stop offset=".75" stop-color="#669df6" stop-opacity=".068"/><stop offset=".877" stop-color="#669df6" stop-opacity=".017"/><stop offset="1" stop-color="#669df6" stop-opacity="0"/></linearGradient><linearGradient id="e" x1="260" y1="112" x2="260" y2="80" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f29900"/><stop offset="1" stop-color="#f29900" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="269" y1="80" x2="269" y2="112" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fdd663"/><stop offset=".107" stop-color="#fdd663" stop-opacity=".803"/><stop offset=".236" stop-color="#fdd663" stop-opacity=".593"/><stop offset=".367" stop-color="#fdd663" stop-opacity=".411"/><stop offset=".497" stop-color="#fdd663" stop-opacity=".263"/><stop offset=".626" stop-color="#fdd663" stop-opacity=".148"/><stop offset=".754" stop-color="#fdd663" stop-opacity=".066"/><stop offset=".879" stop-color="#fdd663" stop-opacity=".017"/><stop offset="1" stop-color="#fdd663" stop-opacity="0"/></linearGradient></defs><g style="isolation:isolate"><path fill="url(#a)" style="mix-blend-mode:multiply" d="M66 88h36v4H66z"/><rect x="99" y="21" width="118" height="70" rx="3" fill="#fff"/><path d="M214 22a2.002 2.002 0 0 1 2 2v64a2.002 2.002 0 0 1-2 2H102a2.002 2.002 0 0 1-2-2V24a2.002 2.002 0 0 1 2-2h112m0-2H102a4 4 0 0 0-4 4v64a4 4 0 0 0 4 4h112a4 4 0 0 0 4-4V24a4 4 0 0 0-4-4z" fill="#e8eaed"/><path fill="url(#b)" style="mix-blend-mode:multiply" d="M224 108h22v4h-22z"/><path fill="url(#c)" style="mix-blend-mode:multiply" d="M26 108h32v4H26z"/><rect x="82" y="32" width="108" height="32" rx="4" fill="#4285f4"/><rect x="126" y="48" width="108" height="32" rx="5.367" fill="#f1f3f4"/><rect x="216" y="54" width="2" height="20" rx="1" fill="#ea4335"/><path fill="none" d="M138 56h16v16h-16z"/><path fill="#1a73e8" d="M138 62.75h16v2.5h-16z"/><path fill="#1a73e8" d="m143.083 56.447 8 13.856-2.166 1.25-8-13.856z"/><path fill="#1a73e8" d="m151.083 57.697-8 13.856-2.166-1.25 8-13.856z"/><path fill="none" d="M164 56h16v16h-16z"/><path fill="#1a73e8" d="M164 62.75h16v2.5h-16z"/><path fill="#1a73e8" d="m169.083 56.447 8 13.856-2.166 1.25-8-13.856z"/><path fill="#1a73e8" d="m177.083 57.697-8 13.856-2.166-1.25 8-13.856z"/><path fill="none" d="M190 56h16v16h-16z"/><path fill="#1a73e8" d="M190 62.75h16v2.5h-16z"/><path fill="#1a73e8" d="m195.083 56.447 8 13.856-2.166 1.25-8-13.856z"/><path fill="#1a73e8" d="m203.083 57.697-8 13.856-2.166-1.25 8-13.856z"/><rect x="98" y="38" width="2" height="20" rx="1" fill="#fff"/><path d="M180 22V10m-52.343 25.657L118 26m114.343 9.657L242 26" fill="none" stroke="#fbbc04" stroke-linecap="round" stroke-miterlimit="10" stroke-width="3"/><path d="M57.984 112a27.497 27.497 0 0 0 15.67-21.27A41.143 41.143 0 0 0 74 86.147V75l-16-5-16 5v11.147a41.143 41.143 0 0 0 .346 4.583A27.43 27.43 0 0 0 57.985 112z" fill="#4285f4"/><path d="M57.984 112a27.497 27.497 0 0 0 15.67-21.27A41.143 41.143 0 0 0 74 86.147V75l-16-5z" fill="url(#d)" style="mix-blend-mode:lighten" data-name="Shape"/><rect x="242" y="80" width="36" height="32" rx="4" fill="#fbbc04"/><path d="M250 80v-7a10 10 0 0 1 10-10 10 10 0 0 1 10 10v7" fill="none" stroke="#9aa0a6" stroke-miterlimit="10" stroke-width="3"/><rect x="242" y="80" width="36" height="32" rx="4" opacity=".5" fill="url(#e)" style="mix-blend-mode:multiply"/><path d="M274 112a4 4 0 0 0 4-4V84a4 4 0 0 0-4-4h-14v32z" fill="url(#f)" style="mix-blend-mode:lighten"/><path fill="none" d="M0 0h316v112H0z"/><path d="M260 25.971a3.637 3.637 0 0 1 4.265-3.602 10.972 10.972 0 0 1 19.47-2.519A6.4 6.4 0 0 1 292 25.971m-6.4-6.4a6.38 6.38 0 0 0-4.526 1.875m-14.831 1.939a3.646 3.646 0 0 0-2.586-1.07M20 38a9.29 9.29 0 0 1 17-5.184A5.419 5.419 0 0 1 44 38m-5.42-5.42a5.402 5.402 0 0 0-3.831 1.588M76 16a9.29 9.29 0 0 0-17-5.184A5.419 5.419 0 0 0 52 16m5.42-5.42a5.402 5.402 0 0 1 3.831 1.588" fill="none" stroke="#e8eaed" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></g></svg>
@@ -0,0 +1,182 @@
1
+ require 'signet/oauth_2/client'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'openssl'
5
+
6
+ module Spree
7
+ module Admin
8
+ class GoogleMerchantSettingsController < ResourceController
9
+ helper Spree::Admin::GoogleShoppingHelper
10
+
11
+ def model_class
12
+ Spree::GoogleCredential
13
+ end
14
+
15
+ def edit
16
+ @credential = current_store.google_credential || current_store.build_google_credential
17
+ end
18
+
19
+ def update
20
+ @credential = current_store.google_credential || current_store.build_google_credential
21
+
22
+ if @credential.update(permitted_params)
23
+ Spree::GoogleShopping::SyncAllJob.perform_later
24
+
25
+ flash[:success] = Spree.t(:successfully_updated, resource: 'Google Settings')
26
+ redirect_to spree.edit_admin_google_merchant_settings_path
27
+ else
28
+ flash[:error] = "Failed to save settings. Please check the errors below."
29
+ render :edit
30
+ end
31
+ end
32
+
33
+ def connect
34
+ redirect_uri = spree.callback_admin_google_merchant_settings_url
35
+ Rails.logger.info "GOOGLE CONNECT: Generated Redirect URI: #{redirect_uri}"
36
+
37
+ client = Signet::OAuth2::Client.new(
38
+ authorization_uri: 'https://accounts.google.com/o/oauth2/auth',
39
+ token_credential_uri: 'https://oauth2.googleapis.com/token',
40
+ client_id: ENV['GOOGLE_CLIENT_ID'],
41
+ client_secret: ENV['GOOGLE_CLIENT_SECRET'],
42
+ scope: [
43
+ 'https://www.googleapis.com/auth/content',
44
+ 'https://www.googleapis.com/auth/adwords',
45
+ 'https://www.googleapis.com/auth/userinfo.email'
46
+ ],
47
+ redirect_uri: redirect_uri,
48
+ additional_parameters: {
49
+ access_type: 'offline',
50
+ prompt: 'consent'
51
+ }
52
+ )
53
+ redirect_to client.authorization_uri.to_s, allow_other_host: true
54
+ end
55
+
56
+ def callback
57
+ Rails.logger.info "GOOGLE CALLBACK: Received parameters: #{params.inspect}"
58
+
59
+ if params[:error].present?
60
+ flash[:error] = "Google Access Denied: #{params[:error]}"
61
+ return redirect_to spree.edit_admin_google_merchant_settings_path
62
+ end
63
+
64
+ if params[:code].blank?
65
+ flash[:error] = "No code received from Google."
66
+ return redirect_to spree.edit_admin_google_merchant_settings_path
67
+ end
68
+
69
+ redirect_uri = spree.callback_admin_google_merchant_settings_url
70
+
71
+ client = Signet::OAuth2::Client.new(
72
+ token_credential_uri: 'https://oauth2.googleapis.com/token',
73
+ client_id: ENV['GOOGLE_CLIENT_ID'],
74
+ client_secret: ENV['GOOGLE_CLIENT_SECRET'],
75
+ redirect_uri: redirect_uri,
76
+ code: params[:code]
77
+ )
78
+ begin
79
+ response = client.fetch_access_token!
80
+
81
+ Rails.logger.info "GOOGLE TOKEN: Success! Expires in: #{response['expires_in']}"
82
+
83
+ access_token = response['access_token']
84
+ expires_in = response['expires_in'].to_i
85
+ expires_in = 3600 if expires_in <= 0
86
+
87
+ email = fetch_google_email(access_token)
88
+
89
+ cred = current_store.google_credential || current_store.build_google_credential
90
+
91
+ new_refresh_token = response['refresh_token'].presence || cred.refresh_token
92
+
93
+ cred.assign_attributes(
94
+ access_token: access_token,
95
+ refresh_token: new_refresh_token,
96
+ token_expires_at: Time.current + expires_in.seconds,
97
+ scope: response['scope'],
98
+ email: email,
99
+ store: current_store
100
+ )
101
+
102
+ if cred.save(validate: false)
103
+ flash[:success] = "Google Account (#{email}) Connected Successfully!"
104
+ else
105
+ Rails.logger.error "DB ERROR: #{cred.errors.full_messages}"
106
+ flash[:error] = "Database Error: #{cred.errors.full_messages.join(', ')}"
107
+ end
108
+
109
+ redirect_to spree.edit_admin_google_merchant_settings_path
110
+
111
+ rescue Signet::AuthorizationError => e
112
+
113
+ current_cred = current_store.google_credential
114
+ if current_cred&.active? && current_cred.updated_at > 2.minutes.ago
115
+ Rails.logger.warn "GOOGLE: Race condition detected (invalid_grant ignored). Login was successful."
116
+
117
+ connected_email = current_cred.email || "Google Account"
118
+ flash[:success] = "Google Account (#{connected_email}) Connected Successfully!"
119
+
120
+ redirect_to spree.edit_admin_google_merchant_settings_path
121
+ return
122
+ end
123
+
124
+ Rails.logger.error "GOOGLE AUTH ERROR: #{e.message}"
125
+ flash[:error] = "Google Authorization Failed: #{e.message}"
126
+ redirect_to spree.edit_admin_google_merchant_settings_path
127
+
128
+ rescue => e
129
+ Rails.logger.error "GOOGLE GENERIC ERROR: #{e.message}"
130
+ flash[:error] = "Error: #{e.message}"
131
+ redirect_to spree.edit_admin_google_merchant_settings_path
132
+ end
133
+ end
134
+
135
+ def disconnect
136
+ cred = current_store.google_credential
137
+ if cred&.destroy
138
+ flash[:success] = "Google Account Disconnected."
139
+ else
140
+ flash[:error] = "Could not disconnect account."
141
+ end
142
+ redirect_to spree.edit_admin_google_merchant_settings_path
143
+ end
144
+
145
+ private
146
+
147
+ def fetch_google_email(access_token)
148
+ uri = URI('https://www.googleapis.com/oauth2/v2/userinfo')
149
+ request = Net::HTTP::Get.new(uri)
150
+ request['Authorization'] = "Bearer #{access_token}"
151
+ request['User-Agent'] = "SpreeGoogleShopping/1.0"
152
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
153
+ http.request(request)
154
+ end
155
+
156
+ if response.is_a?(Net::HTTPSuccess)
157
+ data = JSON.parse(response.body)
158
+ data['email']
159
+ else
160
+ Rails.logger.warn "GOOGLE EMAIL FETCH FAILED: #{response.code} - #{response.body}"
161
+ "Connected Account"
162
+ end
163
+ rescue => e
164
+ Rails.logger.error "GOOGLE EMAIL FETCH ERROR: #{e.message}"
165
+ "Connected Account"
166
+ end
167
+
168
+ def permitted_params
169
+ params.require(:google_credential).permit(
170
+ :merchant_center_id,
171
+ :ad_account_id,
172
+ :target_country,
173
+ :target_currency,
174
+ :default_product_type,
175
+ :default_google_product_category,
176
+ :default_min_handling_time,
177
+ :default_max_handling_time
178
+ )
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ module Admin
3
+ module GoogleShopping
4
+ class DashboardController < Spree::Admin::BaseController
5
+ helper Spree::Admin::BaseHelper
6
+
7
+ before_action :check_connection, only: [:index]
8
+
9
+ def index
10
+ @store = Spree::Store.default
11
+ @credential = @store.google_credential
12
+
13
+ if @credential&.active? && @credential.merchant_center_id.present?
14
+ service = Spree::GoogleShopping::StatusService.new(@credential)
15
+ @stats = service.fetch_counts
16
+ @last_sync = @credential.last_sync_at
17
+
18
+ if @stats[:error]
19
+ flash.now[:error] = "Could not fetch live stats from Google. Showing cached or empty data."
20
+ end
21
+ else
22
+
23
+ @stats = { approved: 0, limited: 0, pending: 0, disapproved: 0 }
24
+ @last_sync = nil
25
+ end
26
+ end
27
+
28
+ def sync
29
+ products = Spree::Product.all
30
+ @store = Spree::Store.default
31
+
32
+ if products.empty?
33
+ flash[:error] = "No products found to sync."
34
+ elsif @store.google_credential.merchant_center_id.blank?
35
+ flash[:error] = "Please enter your Google Merchant Center ID in Settings before syncing."
36
+ else
37
+ products.each do |product|
38
+ Spree::GoogleShopping::SyncProductJob.perform_later(product.id)
39
+ end
40
+
41
+ if @store.google_credential&.merchant_center_id
42
+ Rails.cache.delete("google_shopping_stats_#{@store.google_credential.merchant_center_id}")
43
+ end
44
+
45
+ flash[:success] = "Sync started for #{products.count} products! Statuses will update shortly."
46
+ end
47
+
48
+ redirect_to admin_google_shopping_dashboard_path
49
+ end
50
+
51
+ private
52
+
53
+ def check_connection
54
+ credential = Spree::Store.default.google_credential
55
+
56
+ unless credential&.active?
57
+ flash[:warning] = "Please connect your Google Merchant Center account to access the dashboard."
58
+ redirect_to edit_admin_google_merchant_settings_path
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Admin
3
+ module GoogleShopping
4
+ class IssuesController < Spree::Admin::BaseController
5
+
6
+ def index
7
+ @total_variants = Spree::Variant.count
8
+ @issues_summary = []
9
+
10
+ attributes_with_issues = Spree::GoogleVariantAttribute
11
+ .where.not(google_issues: nil)
12
+ .where.not(google_issues: "[]")
13
+ grouped_issues = {}
14
+
15
+ attributes_with_issues.each do |attr|
16
+ next if attr.google_issues.blank?
17
+
18
+ attr.google_issues.each do |issue|
19
+ key = issue['description']
20
+ if grouped_issues[key].nil?
21
+ variant = Spree::Variant.find_by(id: attr.variant_id)
22
+ product_id = variant&.product_id
23
+ grouped_issues[key] = {
24
+ short_title: issue['description'],
25
+ long_title: issue['detail'],
26
+ code: issue['code'],
27
+ affected_count: 0,
28
+ severity: issue['servability'],
29
+ example_product_id: product_id
30
+ }
31
+ end
32
+
33
+ grouped_issues[key][:affected_count] += 1
34
+ end
35
+ end
36
+
37
+ @issues_summary = grouped_issues.values.sort_by { |i| -i[:affected_count] }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end