spree_bestprice 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 037e25c6d63748d61af761af2f8eeda5e83583098b88f3858e386e47c5361437
4
+ data.tar.gz: 4059819ae737953a3cce2c3a8a87b4050855a0db537ef7245dc031b3ce13bb39
5
+ SHA512:
6
+ metadata.gz: 7529023166756a2904131933d37788e611b146dc026fc58dc46934b8bf050f76bc69f4982326bf6dbb978798cb1486698d1e6718257a6f905dea189e90976b96
7
+ data.tar.gz: 5017f1436a86bd42f8802640737d676c5d215d2a2b22820a7b511c852460c95fc43255ad7bd52d0dc07af7324f147630641a6f5feaca7c5b08ab170639e3b414
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Spree BestPrice
2
+
3
+ This is a BestPrice extension for [Spree Commerce](https://spreecommerce.org), an open‑source e-commerce platform built with Ruby on Rails. It adds the ability to provide products listings to BestPrice Martketplace.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/spree_bestprice.svg)](https://badge.fury.io/rb/spree_bestprice)
6
+
7
+ ## Installation
8
+
9
+ 1. Add this extension to your Gemfile with this line:
10
+
11
+ ```bash
12
+ bundle add spree_bestprice
13
+ ```
14
+
15
+ 2. Restart your server
16
+
17
+ ## Usage
18
+
19
+ 1. **Deploy your endpoint**
20
+ Once the extension is installed and enabled for your store, the feed is publicly served at:
21
+ `https://yoursite.com/bestprice/products.xml`
22
+
23
+ 2. **Whitelist BestPrice's bot IPs**
24
+ BestPrice's crawler needs access to the feed URL. Make sure these IPs aren't blocked by a firewall, WAF, or bot-protection layer:
25
+
26
+ - `139.91.201.40`
27
+ - `139.91.200.222`
28
+ - `62.103.124.6`
29
+ - `62.103.124.3`
30
+
31
+ 3. **Register the feed with BestPrice**
32
+ Share your feed URL with BestPrice (via your Partner Platform contact) to have it added as a data source. New products are typically ingested within 3 business days of the first successful fetch; this isn't something you configure — BestPrice controls the fetch schedule on their end. If a fetch fails, they retry and will email you if the problem persists.
33
+
34
+ ## Developing
35
+
36
+ 1. Create a dummy app
37
+
38
+ ```bash
39
+ bundle update
40
+ bundle exec rake test_app
41
+ ```
42
+
43
+ 2. Add your new code
44
+
45
+ 3. Run tests
46
+
47
+ ```bash
48
+ bundle exec rspec
49
+ ```
50
+
51
+ When testing your applications integration with this extension you may use it's factories.
52
+ Simply add this require statement to your spec_helper:
53
+
54
+ ```ruby
55
+ require 'spree_bestprice/factories'
56
+ ```
57
+
58
+ ## Releasing a new version
59
+
60
+ ```bash
61
+ bundle exec gem bump -p -t
62
+ bundle exec gem release
63
+ ```
64
+
65
+ For more options please see [gem-release README](https://github.com/svenfuchs/gem-release)
66
+
67
+ ## Contributing
68
+
69
+ If you'd like to contribute, please take a look at the
70
+ [instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
71
+ pull request.
72
+
73
+ Copyright (c) 2026 OlympusOne, released under the MIT License.
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_bestprice'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1 @@
1
+ //= link_tree ../images
@@ -0,0 +1,28 @@
1
+ module Spree
2
+ class BestpriceController < StoreController
3
+ include BaseHelper
4
+ include StorefrontHelper
5
+
6
+ before_action :ensure_feed_enabled
7
+
8
+ def products
9
+ respond_to do |format|
10
+ format.xml do
11
+ render template: 'spree_bestprice/products', formats: [:xml]
12
+ end
13
+ format.gzip do
14
+ gz_xml = ActiveSupport::Gzip.compress(render_to_string(template: 'spree_bestprice/products', formats: [:xml]))
15
+ send_data(gz_xml, filename: 'products.xml.gz', type: 'application/x-gzip', disposition: 'inline')
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def ensure_feed_enabled
23
+ unless store_integration('bestprice').present?
24
+ render plain: 'BestPrice extension is not enabled for this store.', status: :not_found
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Spree
2
+ module Integrations
3
+ class Bestprice < Spree::Integration
4
+ IN_STOCK_AVAILABILITY = {
5
+ pickup: 'Σε απόθεμα',
6
+ delivery: 'Παράδοση σε 1-3 ημέρες'
7
+ }.freeze
8
+
9
+ OUT_OF_STOCK_AVAILABILITY_OPTIONS = [
10
+ 'Παράδοση σε 4-7 ημέρες',
11
+ 'Παράδοση σε 4-10 ημέρες',
12
+ 'Παράδοση σε 8-14 ημέρες',
13
+ 'Παράδοση σε 15-30 ημέρες',
14
+ 'Κατόπιν παραγγελίας',
15
+ 'Προπαραγγελία',
16
+ 'Εξαντλήθηκε'
17
+ ].freeze
18
+
19
+ AVAILABILITY_OPTIONS = [
20
+ *IN_STOCK_AVAILABILITY.values,
21
+ *OUT_OF_STOCK_AVAILABILITY_OPTIONS
22
+ ].freeze
23
+
24
+ preference :physical_pickup, :boolean, default: false
25
+ preference :default_availability, :string
26
+
27
+ def self.integration_group
28
+ 'marketing'
29
+ end
30
+
31
+ def self.icon_path
32
+ 'integration_icons/bestprice-logo.png'
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module ProductDecorator
3
+ def bestprice_availability
4
+ private_metadata['bestprice_availability'].presence
5
+ end
6
+
7
+ def bestprice_availability=(value)
8
+ self.private_metadata = private_metadata.merge('bestprice_availability' => value.presence)
9
+ end
10
+
11
+ Spree::Product.prepend self
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ <div class="col-12 col-md-6 p-0">
2
+ <div class="card mb-4">
3
+ <div class="card-body">
4
+ <p>
5
+ <%= Spree.t('admin.integrations.bestprice.description') %>
6
+ </p>
7
+
8
+ <%= preference_field(@integration, form, 'physical_pickup', i18n_scope: 'admin.integrations.bestprice') %>
9
+
10
+ <div class="form-group">
11
+ <%= form.label :preferred_default_availability, Spree.t('admin.integrations.bestprice.preferred_default_availability') %>
12
+ <%= form.select :preferred_default_availability,
13
+ options_for_select(Spree::Integrations::Bestprice::OUT_OF_STOCK_AVAILABILITY_OPTIONS, @integration.preferred_default_availability),
14
+ { include_blank: Spree.t('admin.integrations.bestprice.preferred_default_availability_none') },
15
+ { class: 'custom-select', data: { action: 'auto-submit#submit' } } %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
@@ -0,0 +1,19 @@
1
+ <div class="card mb-4">
2
+ <div class="card-header">
3
+ <h6 class="mb-0"><%= Spree.t('admin.integrations.bestprice.title') %></h6>
4
+ </div>
5
+ <div class="card-body">
6
+ <div class="form-group">
7
+ <%= f.label :bestprice_availability, Spree.t('admin.products.bestprice_availability') %>
8
+ <%= f.select :bestprice_availability,
9
+ options_for_select(
10
+ [[Spree.t('admin.products.bestprice_availability_default'), '']] +
11
+ Spree::Integrations::Bestprice::OUT_OF_STOCK_AVAILABILITY_OPTIONS,
12
+ product.bestprice_availability.to_s
13
+ ),
14
+ {},
15
+ { class: 'custom-select' } %>
16
+ <small class="form-text text-muted"><%= Spree.t('admin.products.bestprice_availability_hint') %></small>
17
+ </div>
18
+ </div>
19
+ </div>
@@ -0,0 +1,123 @@
1
+ xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
2
+
3
+ bestprice_integration = store_integration('bestprice')
4
+ default_in_stock_availability = if bestprice_integration&.preferred_physical_pickup
5
+ Spree::Integrations::Bestprice::IN_STOCK_AVAILABILITY[:pickup]
6
+ else
7
+ Spree::Integrations::Bestprice::IN_STOCK_AVAILABILITY[:delivery]
8
+ end
9
+ default_out_of_stock_availability = bestprice_integration&.preferred_default_availability.presence ||
10
+ Spree::Integrations::Bestprice::OUT_OF_STOCK_AVAILABILITY_OPTIONS.last
11
+
12
+ resolve_availability = lambda do |item_in_stock, override|
13
+ override.presence || (item_in_stock ? default_in_stock_availability : default_out_of_stock_availability)
14
+ end
15
+
16
+ option_values_by_type_name = lambda do |variants, type_name|
17
+ variants.flat_map(&:option_values)
18
+ .select { |ov| ov.option_type.name.downcase == type_name }
19
+ .map(&:presentation)
20
+ .uniq
21
+ end
22
+
23
+ xml.store do
24
+ cache [storefront_products_scope, current_currency] do
25
+ xml.date Time.current.strftime("%Y-%m-%d %H:%M")
26
+
27
+ xml.products do
28
+ storefront_products_scope.find_each do |product|
29
+ color_option_type = product.option_types.find { |ot| ot.name.downcase == 'color' }
30
+
31
+ entries = if color_option_type
32
+ product.variants
33
+ .group_by { |v| v.option_values.find { |ov| ov.option_type_id == color_option_type.id } }
34
+ .reject { |color_value, _| color_value.nil? }
35
+ .map do |color_value, variants|
36
+ title = if product.name.downcase.include?(color_value.presentation.downcase)
37
+ product.name
38
+ else
39
+ "#{product.name} - #{color_value.presentation}"
40
+ end
41
+
42
+ {
43
+ id: "#{product.id}-#{color_value.id}",
44
+ title: title,
45
+ variants: variants
46
+ }
47
+ end
48
+ else
49
+ [{ id: product.id.to_s, title: product.name, variants: product.variants.presence || [product.master] }]
50
+ end
51
+
52
+ breadcrumb_taxons = product_breadcrumb_taxons(product)
53
+
54
+ entries.each do |entry|
55
+ variants = entry[:variants]
56
+ in_stock = variants.any?(&:in_stock?)
57
+ availability = resolve_availability.call(in_stock, product.bestprice_availability)
58
+
59
+ representative_variant = variants.find(&:in_stock?) || variants.first
60
+
61
+ xml.product do
62
+ xml.tag! "productId", entry[:id]
63
+
64
+ xml.tag! "title" do
65
+ xml.cdata! entry[:title]
66
+ end
67
+
68
+ xml.tag! "productURL" do
69
+ xml.cdata! spree_storefront_resource_url(product)
70
+ end
71
+
72
+ entry_images = variants.flat_map(&:images).uniq
73
+ entry_images = [product.default_image].compact if entry_images.empty?
74
+
75
+ xml.tag! "image" do
76
+ xml.cdata! spree_image_url(entry_images.first, width: 500, height: 500) if entry_images.first
77
+ end
78
+
79
+ entry_images[1..].each do |image|
80
+ xml.tag! "additional_image" do
81
+ xml.cdata! spree_image_url(image, width: 500, height: 500)
82
+ end
83
+ end
84
+
85
+ xml.tag! "category_id", breadcrumb_taxons.last.id if breadcrumb_taxons.any?
86
+
87
+ xml.tag! "category_name" do
88
+ xml.cdata! breadcrumb_taxons.map(&:name).join('->')
89
+ end
90
+
91
+ xml.tag! "price", format('%.2f', representative_variant.display_price.to_d)
92
+
93
+ xml.tag! "availability", availability
94
+
95
+ xml.tag! "stock", in_stock ? 'Y' : 'N'
96
+
97
+ xml.tag! "manufacturer" do
98
+ xml.cdata! product.brand_taxon&.name.to_s
99
+ end
100
+
101
+ xml.tag! "ean", representative_variant.barcode.presence || product.barcode
102
+
103
+ size = option_values_by_type_name.call(variants, 'size')
104
+ xml.tag! "size", size.join(',') if size.any?
105
+
106
+ xml.tag! "weight", "#{product.weight} #{product.weight_unit}" if product.weight.present?
107
+
108
+ color = option_values_by_type_name.call(variants, 'color')
109
+ xml.tag! "color", color.join(',') if color.any?
110
+
111
+ if product.storefront_description.present?
112
+ xml.tag! "description" do
113
+ xml.cdata! product.storefront_description&.truncate(10000)
114
+ end
115
+ end
116
+
117
+ xml.tag! "quantity", variants.sum(&:total_on_hand)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,7 @@
1
+ Rails.application.config.after_initialize do
2
+ Spree.integrations << Spree::Integrations::Bestprice
3
+
4
+ Spree::PermittedAttributes.product_attributes.push(:bestprice_availability)
5
+
6
+ Rails.application.config.spree_admin.product_form_partials << 'spree/admin/products/bestprice_availability'
7
+ end
@@ -0,0 +1,15 @@
1
+ ---
2
+ el:
3
+ spree:
4
+ admin:
5
+ integrations:
6
+ bestprice:
7
+ title: BestPrice
8
+ description: Το BestPrice είναι εργαλείο για καταχωρίσεις προϊόντων στο BestPrice
9
+ physical_pickup: Το κατάστημα διαθέτει φυσικό σημείο παραλαβής
10
+ preferred_default_availability: Προεπιλεγμένη διαθεσιμότητα (εκτός αποθέματος)
11
+ preferred_default_availability_none: "Χωρίς προεπιλογή (τα εκτός αποθέματος εξαιρούνται)"
12
+ products:
13
+ bestprice_availability: Διαθεσιμότητα BestPrice (override)
14
+ bestprice_availability_default: Χρήση προεπιλογής καταστήματος
15
+ bestprice_availability_hint: Αναφέρετε πάντα αυτή τη διαθεσιμότητα για το προϊόν, ανεξαρτήτως αποθέματος.
@@ -0,0 +1,15 @@
1
+ ---
2
+ en:
3
+ spree:
4
+ admin:
5
+ integrations:
6
+ bestprice:
7
+ title: BestPrice
8
+ description: BestPrice is a channel integration for product listings on BestPrice
9
+ physical_pickup: Store has a physical pickup location
10
+ preferred_default_availability: Default availability (out of stock)
11
+ preferred_default_availability_none: "No default (out-of-stock products excluded)"
12
+ products:
13
+ bestprice_availability: BestPrice availability override
14
+ bestprice_availability_default: Use store default
15
+ bestprice_availability_hint: Always report this availability status for this product, regardless of stock level.
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Spree::Core::Engine.add_routes do
2
+ get '/bestprice/products.xml', to: 'bestprice#products', defaults: { format: :xml }
3
+ get '/bestprice/products.xml.gz', to: 'bestprice#products'
4
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeBestPrice
2
+ class Configuration < Spree::Preferences::Configuration
3
+
4
+ # Some example preferences are shown below, for more information visit:
5
+ # https://docs.spreecommerce.org/developer/contributing/creating-an-extension
6
+
7
+ # preference :enabled, :boolean, default: true
8
+ # preference :dark_chocolate, :boolean, default: true
9
+ # preference :color, :string, default: 'Red'
10
+ # preference :favorite_number, :integer
11
+ # preference :supported_locales, :array, default: [:en]
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module SpreeBestPrice
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'spree_bestprice'
6
+
7
+ # use rspec for tests
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer 'spree_bestprice.environment', before: :load_config_initializers do |_app|
13
+ SpreeBestPrice::Config = SpreeBestPrice::Configuration.new
14
+ end
15
+
16
+ initializer 'spree_bestprice.assets' do |app|
17
+ app.config.assets.precompile += %w[spree_bestprice_manifest]
18
+ end
19
+
20
+ initializer 'spree_bestprice.importmap', before: 'importmap' do |app|
21
+ app.config.importmap.paths << root.join('config/importmap.rb')
22
+ end
23
+
24
+ def self.activate
25
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
26
+ Rails.configuration.cache_classes ? require(c) : load(c)
27
+ end
28
+ end
29
+
30
+ config.to_prepare(&method(:activate).to_proc)
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :bestprice_integration, class: Spree::Integrations::Bestprice do
3
+ active { true }
4
+ store { Spree::Store.default }
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module SpreeBestPrice
2
+ VERSION = '1.0.0'.freeze
3
+
4
+ def gem_version
5
+ Gem::Version.new(VERSION)
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ require 'spree_core'
2
+ require 'spree_extension'
3
+ require 'spree_bestprice/engine'
4
+ require 'spree_bestprice/version'
5
+ require 'spree_bestprice/configuration'
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree_bestprice
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - OlympusOne
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: spree
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 5.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 5.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: spree_storefront
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 5.2.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: spree_admin
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 5.2.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 5.2.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: spree_extension
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: spree_dev_tools
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: Adds the ability to provide products listings to BestPrice.
83
+ email: info@olympusone.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - README.md
89
+ - Rakefile
90
+ - app/assets/config/spree_bestprice_manifest.js
91
+ - app/assets/images/integration_icons/bestprice-logo.png
92
+ - app/controllers/spree/bestprice_controller.rb
93
+ - app/models/spree/integrations/bestprice.rb
94
+ - app/models/spree/product_decorator.rb
95
+ - app/views/spree/admin/integrations/forms/_bestprice.html.erb
96
+ - app/views/spree/admin/products/_bestprice_availability.html.erb
97
+ - app/views/spree_bestprice/products.xml.builder
98
+ - config/initializers/spree.rb
99
+ - config/locales/el.yml
100
+ - config/locales/en.yml
101
+ - config/routes.rb
102
+ - lib/spree_bestprice.rb
103
+ - lib/spree_bestprice/configuration.rb
104
+ - lib/spree_bestprice/engine.rb
105
+ - lib/spree_bestprice/factories.rb
106
+ - lib/spree_bestprice/version.rb
107
+ homepage: https://github.com/olympusone/spree_bestprice
108
+ licenses:
109
+ - MIT
110
+ metadata:
111
+ bug_tracker_uri: https://github.com/olympusone/spree_bestprice/issues
112
+ changelog_uri: https://github.com/olympusone/spree_bestprice/releases/tag/v1.0.0
113
+ documentation_uri: https://github.com/olympusone/spree_bestprice
114
+ homepage_uri: https://github.com/olympusone/spree_bestprice
115
+ source_code_uri: https://github.com/olympusone/spree_bestprice/tree/v1.0.0
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '3.0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements:
130
+ - none
131
+ rubygems_version: 4.0.3
132
+ specification_version: 4
133
+ summary: Spree Commerce BestPrice Extension
134
+ test_files: []