diff --git a/app/services/flowcommerce_spree/import_items_hs_codes.rb b/app/services/flowcommerce_spree/import_items_hs_codes.rb new file mode 100644 index 00000000..68c0fa45 --- /dev/null +++ b/app/services/flowcommerce_spree/import_items_hs_codes.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module FlowcommerceSpree + # A service object to import the data for product variants belonging to a flow.io Experience + class ImportItemsHsCodes + def self.run(client: FlowcommerceSpree.client, organization: ORGANIZATION) + new(client: client, organization: organization).run + end + + def run # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + page_size = 100 + offset = 0 + items = [] + updated = [] + with_errors = [] + total = 0 + + while offset == 0 || items.length != 0 + # show current list size + @logger.info "\nGetting items: rows #{offset} - #{offset + page_size}" + + begin + items = @client.hs10.get(@organization, limit: page_size, offset: offset) + rescue Io::Flow::V0::HttpClient::PreconditionException => e + @logger.info "flow.io API error: #{e.message}" + break + end + + offset += page_size + log_str = +'' + + items.group_by { |hs_code| hs_code.item.number }.each do |item| + total += 1 + begin + variant_sku = item.first + next unless (variant = Spree::Variant.find_by(sku: variant_sku)) + + hs_code_data_list = item.last + hs_code = hs_code_data_list&.first&.code&.[](0..5) + variant.flow_data ||= {} + variant.flow_data['hs_code'] = hs_code + variant.update_column(:meta, variant.meta.to_json) + log_str << "#{variant.sku}, " + updated << variant.sku + rescue StandardError + with_errors = variant.sku + end + end + @logger.info log_str + end + + VariantService.new.update_flow_classification(updated) + + @logger.info "\nData for #{total.to_s.green} products was imported." + end + + private + + def initialize(client:, organization:) + @client = client + @logger = client.instance_variable_get(:@http_handler).logger + @organization = organization + end + end +end diff --git a/lib/tasks/spree_variant.rake b/lib/tasks/spree_variant.rake new file mode 100644 index 00000000..f7ecce9a --- /dev/null +++ b/lib/tasks/spree_variant.rake @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +namespace :spree_variant do + desc 'Import Flow Hs Codes from CSV' + task import_flow_hs_code_from_csv: :environment do + s3_file_path = CSVUploader.download_url('script/flow_hs_codes.csv', 'flow_hs_code') + csv = CSV.new(URI.parse(s3_file_path).open, headers: true, col_sep: ',') + + not_found = [] + updated = [] + with_errors = [] + + csv.each do |row| + begin + hs_code = row['hs6'] + next unless hs_code.present? + + sku = row['item_number'] + next not_found << sku unless (variant = Spree::Variant.find_by(sku: row['item_number'])) + + variant.flow_data ||= {} + variant.flow_data['hs_code'] = hs_code + variant.update_column(:meta, variant.meta.to_json) + updated << sku + rescue StandardError + with_errors << sku + end + end + + VariantService.new.update_flow_classification(updated) + + puts "\n#{Time.zone.now} | Not found in the DB #{not_found.size}." + puts not_found.inspect + + puts "\n#{Time.zone.now} | Unexpected errors while updating #{with_errors.size}." + puts with_errors.inspect + + puts "\n#{Time.zone.now} | Updated #{updated.size}." + puts "Updated #{updated} variants." + + puts '[Important] Review task output to see if all products where properly synchronized with Fulfil.' + end + + desc 'Import Flow Hs Codes from Api' + task import_flow_hs_code: :environment do + FlowcommerceSpree::ImportItemsHsCodes.run + end +end diff --git a/spec/dummy/app/models/concerns/csv_uploader.rb b/spec/dummy/app/models/concerns/csv_uploader.rb new file mode 100644 index 00000000..77cc8cef --- /dev/null +++ b/spec/dummy/app/models/concerns/csv_uploader.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CSVUploader + def initialize(file) + timestamp = Time.current.strftime('%Y%m%d%H%M%S') + @filename = "#{timestamp}_#{file.original_filename}" + @file_content = CSV.parse(file.read) + end + + def self.download_url(file_path, filename); end +end diff --git a/spec/dummy/app/services/variant_service.rb b/spec/dummy/app/services/variant_service.rb new file mode 100644 index 00000000..25e2edd4 --- /dev/null +++ b/spec/dummy/app/services/variant_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class VariantService + attr_accessor :variant + + def initialize(variant = nil) + @variant = variant + end + + def update_flow_classification(variant_skus = []); end +end diff --git a/spec/factories/flow_io/hs_code.rb b/spec/factories/flow_io/hs_code.rb new file mode 100644 index 00000000..5fbd8ed0 --- /dev/null +++ b/spec/factories/flow_io/hs_code.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :flow_hs_code, class: Io::Flow::V0::Models::Hs10 do + id { Faker::Guid.guid } + origin { 'CAN' } + destination { 'DEU' } + code { '71131910' } + item do + Io::Flow::V0::Models::HarmonizedItemReference.new( + description: 'jewellery precious_metal', + id: 'cit-b1d68224735e4828a51b0be90ad37c6f', + number: Spree::Variant.first&.sku || create(:base_variant).sku + ) + end + initialize_with { new(**attributes) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 14f82eeb..2a521431 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -17,6 +17,7 @@ require 'support/database_cleaner.rb' require 'support/flow.rb' require 'support/controller_requests.rb' +require 'support/tasks.rb' # Add additional requires below this line. Rails is not loaded until this point! diff --git a/spec/service/import_items_hs_codes_spec.rb b/spec/service/import_items_hs_codes_spec.rb new file mode 100644 index 00000000..8d00b070 --- /dev/null +++ b/spec/service/import_items_hs_codes_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'colorize' + +module FlowcommerceSpree + RSpec.describe ImportItemsHsCodes do + let(:hs_code_data) { build(:flow_hs_code) } + + before do + allow_any_instance_of(Io::Flow::V0::Clients::Hs10) + .to(receive(:get).with('mejuridevs', limit: 100, offset: 0).and_return([hs_code_data])) + allow_any_instance_of(Io::Flow::V0::Clients::Hs10) + .to(receive(:get).with('mejuridevs', limit: 100, offset: 100).and_return([])) + end + + it 'Update variant HS code' do + FlowcommerceSpree::ImportItemsHsCodes.run + variant = Spree::Variant.find_by(sku: hs_code_data.item.number) + expect(variant.flow_data['hs_code']).to(eq(hs_code_data.code[0..5])) + end + + it 'Calls VariantService#update_flow_classification method' do + expect_any_instance_of(VariantService).to(receive(:update_flow_classification) + .with([hs_code_data.item.number])) + FlowcommerceSpree::ImportItemsHsCodes.run + end + end +end diff --git a/spec/support/tasks.rb b/spec/support/tasks.rb new file mode 100644 index 00000000..ff645b49 --- /dev/null +++ b/spec/support/tasks.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rake' + +# Task names should be used in the top-level describe, with an optional +# "rake "-prefix for better documentation. Both of these will work: +# +# 1) describe "foo:bar" do ... end +# +# 2) describe "rake foo:bar" do ... end +# +# Favor including "rake "-prefix as in the 2nd example above as it produces +# doc output that makes it clear a rake task is under test and how it is +# invoked. +module TaskFormat + extend ActiveSupport::Concern + + included do + let(:task_name) { self.class.top_level_description.sub(/\Arake /, '') } + let(:tasks) { Rake::Task } # Make the Rake task available as `task` in your examples: + subject(:task) { tasks[task_name] } + end +end + +RSpec.configure do |config| # Tag Rake specs with `:task` metadata or put them in the spec/tasks dir + config.define_derived_metadata(file_path: %r{/spec/tasks/}) do |metadata| + metadata[:type] = :task + end + + config.include TaskFormat, type: :task + + config.before(:suite) do + Rails.application.load_tasks + end +end diff --git a/spec/tasks/spree_variant_spec.rb b/spec/tasks/spree_variant_spec.rb new file mode 100644 index 00000000..a2ced0fe --- /dev/null +++ b/spec/tasks/spree_variant_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'csv' + +describe 'rake spree_variants', type: :task do + let(:variant) { create(:base_variant, :with_flow_data) } + + describe 'import_flow_hs_code_from_csv' do + let(:hs_code) { '711319' } + let(:stubed_csv_content) { [variant.sku, hs_code, variant.product.id, variant.product.name] } + let(:stubed_csv) { CSV.new("item_number,hs6,product_id,item_name\n#{stubed_csv_content.join(',')}", headers: true) } + let(:run_codes_rake_task) do + Rake::Task['spree_variant:import_flow_hs_code_from_csv'].reenable + Rake.application.invoke_task('spree_variant:import_flow_hs_code_from_csv') + end + + before(:each) do + allow(CSVUploader).to(receive(:download_url).and_return('https://s3.amazonaws.com/test/script/flow_hs_codes.csv')) + allow_any_instance_of(URI::HTTPS).to(receive(:open)) + + allow(CSV).to(receive(:new).and_return(stubed_csv)) + end + + it 'updates variant`s flow data with the hs_code' do + expect(variant.flow_data['hs_code']).to(be_blank) + run_codes_rake_task + expect(variant.reload.flow_data['hs_code']).to(eq(hs_code)) + end + + it 'Calls VariantService#update_flow_classification method' do + expect_any_instance_of(VariantService).to(receive(:update_flow_classification).with([variant.sku])) + run_codes_rake_task + end + + context 'when no hs_code is present' do + let(:stubed_csv_content) { [variant.sku, '', variant.product.id, variant.product.name] } + + it 'does not update variant' do + run_codes_rake_task + expect(variant.reload.flow_data['hs_code']).to(be_blank) + end + end + end +end