Skip to content

Commit

Permalink
Merge pull request #2 from rubygitflow/ozon_market
Browse files Browse the repository at this point in the history
Ozon market
  • Loading branch information
rubygitflow authored Feb 1, 2024
2 parents ccc7c12 + 15cf318 commit 1ea46e8
Show file tree
Hide file tree
Showing 98 changed files with 4,131 additions and 345 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ YANDEX_APP_ID=
OZON_APP_ID=
REDIS_URL=redis://127.0.0.1:6379/0
PRODUCTS_DOWNLOADER_FROM_ARCHIVE=true
PRODUCTS_DOWNLOADER_OZON_DESCRIPTIONS=false
LIMITING_REMAINING_REQUESTS=5
2 changes: 2 additions & 0 deletions .github/workflows/rubyonrails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ jobs:
env:
RAILS_ENV: test
YANDEX_APP_ID: 'f00bae19ad4a4c7d2190f4b1516ea277'
OZON_APP_ID: 'OzonSeller'
DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test"
PRODUCTS_DOWNLOADER_FROM_ARCHIVE: true
PRODUCTS_DOWNLOADER_OZON_DESCRIPTIONS: false
LIMITING_REMAINING_REQUESTS: 5
steps:
- name: Checkout code
Expand Down
23 changes: 20 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ require:
RSpec/FilePath:
Enabled: false

# 7
RSpec/MultipleExpectations:
Max: 7
Max: 11

# 5
RSpec/MultipleMemoizedHelpers:
Expand All @@ -27,7 +28,7 @@ RSpec/NamedSubject:

RSpec/MessageSpies:
Enabled: false

AllCops:
Exclude:
- bin/**/*
Expand All @@ -43,8 +44,9 @@ Style/Documentation:
Description: 'Document classes and non-namespace modules.'
Enabled: false

# 20
Metrics/MethodLength:
Max: 20
Max: 25

# 40
Metrics/BlockLength:
Expand Down Expand Up @@ -158,3 +160,18 @@ Layout/LineLength:
Exclude:
- spec/shared/**/*
- spec/business_logic/tasks/import_products_spec.rb

Style/NumericLiterals:
Exclude:
- spec/shared/**/*
- spec/factories/**/*
- spec/services/ozon/products_downloader/loading_info_list_spec.rb
- spec/services/cash_ozon_category_spec.rb

# 7
Metrics/CyclomaticComplexity:
Max: 10

# 7
Metrics/PerceivedComplexity:
Max: 10
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ gem 'clockwork', '~> 3.0', '>= 3.0.2'
gem 'faraday', '~> 2.9'
gem 'faraday-retry', '~> 2.2'

gem 'activerecord-import', '~> 1.5', '>= 1.5.1'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: %i[mri mingw x64_mingw]
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ GIT
GEM
remote: https://rubygems.org/
specs:
activerecord-import (1.5.1)
activerecord (>= 4.2)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
Expand Down Expand Up @@ -304,6 +306,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
activerecord-import (~> 1.5, >= 1.5.1)
byebug
clockwork (~> 3.0, >= 3.0.2)
database_cleaner-active_record (~> 2.1)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ _An example of connecting to marketplaces and downloading data from them_

[![Test&Lint](https://github.com/rubygitflow/marketplace_aggregator/actions/workflows/rubyonrails.yml/badge.svg)](https://github.com/rubygitflow/marketplace_aggregator/actions)

**This project demonstrates a way to connect to the Yandex.Market marketplace API with:**
**This project demonstrates a way to connect to the Yandex.Market and Ozon marketplace-APIs with:**
- Using UUIDs in tables with personalized information instead of the usual numeric IDs;
- Using PostgreSql native data types - Array, Enumerated, Hstore, Composite Types;
- Setting up queues in Sidekiq;
- Pre-processing the Rate Limits based on marketplace response headers and Post-processing based on restrictions stored in Kredis;
- Logging of critical events when interacting with the API of marketplace.
- Utilizing the Abstract Factory Pattern;
- Code coverage with RSpec tests;
- Enabling GitHub Actions;

Expand All @@ -32,6 +33,7 @@ $ rails bd:seed
$ bundle exec sidekiq -C config/sidekiq_live.yml
$ bundle exec sidekiq -C config/sidekiq_scheduled.yml
```
Seed the Ozon categories by completing the task `rails ozon_categories:load`. This will be done if you have at least one marketplace_credentials from Ozon.

* Run Tests
```bash
Expand Down
44 changes: 44 additions & 0 deletions app/business_logic/handles/ozon_product_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Handles
module OzonProductStatus
# Product
# enum :status, {
# preliminary: 'preliminary',
# on_moderation: 'on_moderation',
# failed_moderation: 'failed_moderation',
# published: 'published',
# unpublished: 'unpublished',
# archived: 'archived'
# }

# rubocop:disable Metrics/CyclomaticComplexity
def take_ozon_card_status(item)
visible = item[:visible]
status = item[:status] || {}
state_failed = status[:state_failed] || ''
validation_state = status[:validation_state] || ''
is_failed = status[:is_failed] || false

case validation_state
when 'success'
visible ? 'published' : 'unpublished'
when 'pending'
case state_failed
when 'imported'
'failed_moderation'
else
case is_failed
when true
visible ? 'failed_moderation' : 'unpublished'
else
'on_moderation'
end
end
else
'unpublished'
end
end
# rubocop:enable Metrics/CyclomaticComplexity
end
end
31 changes: 17 additions & 14 deletions app/business_logic/handles/products_downloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@

module Handles
class ProductsDownloader
CARD_STATUS = {
'PUBLISHED' => 'published',
'CHECKING' => 'on_moderation',
'DISABLED_BY_PARTNER' => 'unpublished',
'DISABLED_AUTOMATICALLY' => 'unpublished',
'REJECTED_BY_MARKET' => 'failed_moderation',
'CREATING_CARD' => 'preliminary',
'NO_CARD' => 'preliminary',
'NO_STOCKS' => 'unpublished'
}.freeze
extend YandexProductStatus
extend OzonProductStatus

class << self
def from_archive
ENV.fetch('PRODUCTS_DOWNLOADER_FROM_ARCHIVE', nil) || false
to_bool(ENV.fetch('PRODUCTS_DOWNLOADER_FROM_ARCHIVE', nil)) || false
end

def take_card_status(offer)
status = offer&.fetch(:campaigns, [])&.first&.fetch(:status, nil)
CARD_STATUS.fetch(status, 'preliminary')
def ozon_descriptions
to_bool(ENV.fetch('PRODUCTS_DOWNLOADER_OZON_DESCRIPTIONS', nil)) || false
end

def to_bool(inp)
case inp
when String
case inp.downcase
when '0', 'f', 'false', 'off', 'no'
false
when '1', 't', 'true', 'on', 'yes'
true
end
end
end
end
end
Expand Down
21 changes: 21 additions & 0 deletions app/business_logic/handles/yandex_product_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Handles
module YandexProductStatus
YANDEX_CARD_STATUS = {
'PUBLISHED' => 'published',
'CHECKING' => 'on_moderation',
'DISABLED_BY_PARTNER' => 'unpublished',
'DISABLED_AUTOMATICALLY' => 'unpublished',
'REJECTED_BY_MARKET' => 'failed_moderation',
'CREATING_CARD' => 'preliminary',
'NO_CARD' => 'preliminary',
'NO_STOCKS' => 'unpublished'
}.freeze

def take_yandex_card_status(offer)
status = offer&.fetch(:campaigns, [])&.first&.fetch(:status, nil)
YANDEX_CARD_STATUS.fetch(status, 'preliminary')
end
end
end
2 changes: 1 addition & 1 deletion app/business_logic/operations/check_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def do_check_credentials
# 2. Define the class to be called
checker = @mp_credential.marketplace.to_constant_with(CHECKER_CLASS)
# 3. Make a HEAD HTTP request
@result_valid = checker.new.call(@mp_credential)
@result_valid = checker.new(@mp_credential).call
# 4. Notify client if needed
notify_client unless @result_valid[:ok]
rescue StandardError => e
Expand Down
2 changes: 1 addition & 1 deletion app/business_logic/tasks/import_products.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def call
MarketplaceCredential.find_each(batch_size: 100) do |mp_credential|
# 1. correcting client mistakes
mp_credential.fix_credentials!
# 2. checking the validity of credentials
# 2. verifying the validity of credentials
Operations::CheckCredentials.new(mp_credential).call
# 3. refresh the record
mp_credential.save! if mp_credential.changed?
Expand Down
18 changes: 18 additions & 0 deletions app/jobs/marketplace_interaction/import_ozon_categories_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module MarketplaceInteraction
class ImportOzonCategoriesJob < ApplicationJob
queue_as :default

def perform
mp_credential = MarketplaceCredential.ozon.valid.last
back_time = Time.now
Ozon::LoadCategories.new(mp_credential).call

# TODO: To send a report on the successful update of the list of categories
Rails.logger.info "download: :ozon_categories — OK (in #{(Time.now - back_time).round(3)} sec)"
rescue StandardError => e
ErrorLogger.push_trace e
end
end
end
25 changes: 22 additions & 3 deletions app/jobs/products/import_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@ class ImportJob < ApplicationJob
def perform(is_client_queue, mp_credential_id)
@is_client_queue = is_client_queue
@mp_credential = MarketplaceCredential.find_by(id: mp_credential_id)
return if @mp_credential.blank? || @mp_credential.deleted_at
return if irrelevant?

@mp_credential.update(last_sync_at_products: Time.current) if downloadable?
end

private

def irrelevant?
@mp_credential.blank? ||
@mp_credential.credentials.blank? ||
@mp_credential.deleted_at
end

# rubocop:disable Metrics/AbcSize
def downloadable?
downloader = @mp_credential.marketplace.to_constant_with(DOWNLOADER_CLASS)
downloader.new(@mp_credential).call
.new(@mp_credential)
back_time = Time.now
downloader.call
Rails.logger.info log(downloader.parsed_ids.size, back_time).strip
rescue NameError => e
# We are checking the code. It's fixable
ErrorLogger.push_trace e
Expand All @@ -44,8 +54,17 @@ def downloadable?
false
rescue MarketplaceAggregator::ApiError => e
ErrorLogger.push e
# restart task after an hour (for example)
# TODO: restart task after an hour (for example)
false
end
# rubocop:enable Metrics/AbcSize

def log(count, back_time)
<<~MESSAGE
import: :mp_credential[#{@mp_credential.id}] — \
[#{count}] - \
OK (in #{(Time.now - back_time).round(3)} sec)
MESSAGE
end
end
end
8 changes: 8 additions & 0 deletions app/models/marketplace_credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ class MarketplaceCredential < ApplicationRecord
where(is_valid: true).where('credentials is null')
.or(where(is_valid: false))
end
scope :ozon, -> do
where.associated(:marketplace)
.where('marketplaces.name = ?', Marketplace.ozon.name)
end
scope :yandex, -> do
where.associated(:marketplace)
.where('marketplaces.name = ?', Marketplace.yandex.name)
end

validates :instance_name, presence: true

Expand Down
4 changes: 4 additions & 0 deletions app/models/ozon_category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class OzonCategory < ApplicationRecord
end
Loading

0 comments on commit 1ea46e8

Please sign in to comment.