Skip to content

Commit b0ea0e1

Browse files
Implement return reasons model and distribution pipeline
- Add ReturnReason Ruby model with validation - Add return reason dist serializers (JSON, TXT) - Add return reason data serializers (YAML, localizations) - Update Category model to load and validate return_reasons - Update category serializers to include return_reasons field - Integrate return reasons into generate_dist command - Generate dist files: return_reasons.json, return_reasons.txt - Regenerate categories.json and taxonomy.json with return_reasons
1 parent fbeff51 commit b0ea0e1

File tree

14 files changed

+1007112
-42023
lines changed

14 files changed

+1007112
-42023
lines changed

dev/lib/product_taxonomy.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def data_path = DATA_PATH
2424
require_relative "product_taxonomy/models/attribute"
2525
require_relative "product_taxonomy/models/extended_attribute"
2626
require_relative "product_taxonomy/models/value"
27+
require_relative "product_taxonomy/models/return_reason"
2728
require_relative "product_taxonomy/models/category"
2829
require_relative "product_taxonomy/models/taxonomy"
2930
require_relative "product_taxonomy/models/mapping_rule"
@@ -46,6 +47,13 @@ def data_path = DATA_PATH
4647
require_relative "product_taxonomy/models/serializers/value/data/localizations_serializer"
4748
require_relative "product_taxonomy/models/serializers/value/dist/json_serializer"
4849
require_relative "product_taxonomy/models/serializers/value/dist/txt_serializer"
50+
require_relative "product_taxonomy/models/serializers/return_reason/dist/json_serializer"
51+
require_relative "product_taxonomy/models/serializers/return_reason/dist/txt_serializer"
52+
require_relative "product_taxonomy/models/serializers/return_reason/data/localizations_serializer"
53+
require_relative "product_taxonomy/models/serializers/return_reason/data/data_serializer"
54+
require_relative "product_taxonomy/models/serializers/return_reason/docs/base_serializer"
55+
require_relative "product_taxonomy/models/serializers/return_reason/docs/reversed_serializer"
56+
require_relative "product_taxonomy/models/serializers/return_reason/docs/search_serializer"
4957
require_relative "product_taxonomy/commands/command"
5058
require_relative "product_taxonomy/commands/generate_dist_command"
5159
require_relative "product_taxonomy/commands/find_unmapped_external_categories_command"
@@ -54,9 +62,12 @@ def data_path = DATA_PATH
5462
require_relative "product_taxonomy/commands/dump_categories_command"
5563
require_relative "product_taxonomy/commands/dump_attributes_command"
5664
require_relative "product_taxonomy/commands/dump_values_command"
65+
require_relative "product_taxonomy/commands/dump_return_reasons_command"
5766
require_relative "product_taxonomy/commands/dump_integration_full_names_command"
5867
require_relative "product_taxonomy/commands/sync_en_localizations_command"
5968
require_relative "product_taxonomy/commands/add_category_command"
6069
require_relative "product_taxonomy/commands/add_attribute_command"
6170
require_relative "product_taxonomy/commands/add_attributes_to_categories_command"
6271
require_relative "product_taxonomy/commands/add_value_command"
72+
require_relative "product_taxonomy/commands/add_return_reason_command"
73+
require_relative "product_taxonomy/commands/add_return_reasons_to_categories_command"

dev/lib/product_taxonomy/commands/generate_dist_command.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def execute
4242
def generate_dist_files(locale)
4343
logger.info("Generating files for #{locale}")
4444
FileUtils.mkdir_p("#{OUTPUT_PATH}/#{locale}")
45-
["categories", "attributes", "taxonomy", "attribute_values"].each do |type|
45+
["categories", "attributes", "taxonomy", "attribute_values", "return_reasons"].each do |type|
4646
generate_txt_file(locale:, type:)
4747
generate_json_file(locale:, type:)
4848
end
@@ -54,6 +54,7 @@ def generate_txt_file(locale:, type:)
5454
when "categories" then Serializers::Category::Dist::TxtSerializer.serialize_all(version: @version, locale:)
5555
when "taxonomy" then return
5656
when "attribute_values" then Serializers::Value::Dist::TxtSerializer.serialize_all(version: @version, locale:)
57+
when "return_reasons" then Serializers::ReturnReason::Dist::TxtSerializer.serialize_all(version: @version, locale:)
5758
end
5859

5960
File.write("#{OUTPUT_PATH}/#{locale}/#{type}.txt", txt_data + "\n")
@@ -75,6 +76,8 @@ def generate_json_file(locale:, type:)
7576
@categories_json_by_locale[locale].merge(@attributes_json_by_locale[locale])
7677
when "attribute_values"
7778
Serializers::Value::Dist::JsonSerializer.serialize_all(version: @version, locale:)
79+
when "return_reasons"
80+
Serializers::ReturnReason::Dist::JsonSerializer.serialize_all(version: @version, locale:)
7881
end
7982

8083
File.write("#{OUTPUT_PATH}/#{locale}/#{type}.json", JSON.pretty_generate(json_data) + "\n")

dev/lib/product_taxonomy/models/category.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def load_from_source(source_data)
2222
id: item["id"],
2323
name: item["name"],
2424
attributes: Array(item["attributes"]).map { Attribute.find_by(friendly_id: _1) || _1 },
25+
return_reasons: Array(item["return_reasons"]).map { ReturnReason.find_by(friendly_id: _1) || _1 },
2526
)
2627
end
2728

@@ -74,6 +75,7 @@ def add_children(type:, item:, parent:)
7475
validates :id, format: { with: /\A[a-z]{2}(-\d+)*\z/ }, on: :create
7576
validates :name, presence: true, on: :create
7677
validate :attributes_found?, on: :create
78+
validate :return_reasons_found?, on: :create
7779
validates_with ProductTaxonomy::Indexed::UniquenessValidator, attributes: [:id], on: :create
7880

7981
# Validations that can only be performed after the category tree has been loaded.
@@ -84,19 +86,21 @@ def add_children(type:, item:, parent:)
8486

8587
localized_attr_reader :name, keyed_by: :id
8688

87-
attr_reader :id, :children, :secondary_children, :attributes
89+
attr_reader :id, :children, :secondary_children, :attributes, :return_reasons
8890
attr_accessor :parent, :secondary_parents
8991

9092
# @param id [String] The ID of the category.
9193
# @param name [String] The name of the category.
9294
# @param attributes [Array<Attribute>] The attributes of the category.
95+
# @param return_reasons [Array<ReturnReason>] The return reasons for the category.
9396
# @param parent [Category] The parent category of the category.
94-
def initialize(id:, name:, attributes: [], parent: nil)
97+
def initialize(id:, name:, attributes: [], return_reasons: [], parent: nil)
9598
@id = id
9699
@name = name
97100
@children = []
98101
@secondary_children = []
99102
@attributes = attributes
103+
@return_reasons = return_reasons
100104
@parent = parent
101105
@secondary_parents = []
102106
end
@@ -286,6 +290,18 @@ def attributes_found?
286290
end
287291
end
288292

293+
def return_reasons_found?
294+
return_reasons&.each do |return_reason|
295+
next if return_reason.is_a?(ReturnReason)
296+
297+
errors.add(
298+
:return_reasons,
299+
:not_found,
300+
message: "not found for friendly ID \"#{return_reason}\"",
301+
)
302+
end
303+
end
304+
289305
def children_found?
290306
children&.each do |child|
291307
next if child.is_a?(Category)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
class ReturnReason
5+
include ActiveModel::Validations
6+
include FormattedValidationErrors
7+
extend Localized
8+
extend Indexed
9+
10+
class << self
11+
# Load return reasons from source data. By default, this data is deserialized from a YAML file in the `data` directory.
12+
#
13+
# @param source_data [Array<Hash>] The source data to load return reasons from.
14+
def load_from_source(source_data)
15+
raise ArgumentError, "source_data must be an array" unless source_data.is_a?(Array)
16+
17+
source_data.each do |return_reason_data|
18+
raise ArgumentError, "source_data must contain hashes" unless return_reason_data.is_a?(Hash)
19+
20+
return_reason = return_reason_from(return_reason_data)
21+
ReturnReason.add(return_reason)
22+
return_reason.validate!(:create)
23+
end
24+
end
25+
26+
# Reset all class-level state
27+
def reset
28+
@localizations = nil
29+
@hashed_models = nil
30+
end
31+
32+
# Get the next ID for a newly created return reason.
33+
#
34+
# @return [Integer] The next ID.
35+
def next_id = (all.max_by(&:id)&.id || 0) + 1
36+
37+
private
38+
39+
def return_reason_from(return_reason_data)
40+
ReturnReason.new(
41+
id: return_reason_data["id"],
42+
name: return_reason_data["name"],
43+
description: return_reason_data["description"],
44+
friendly_id: return_reason_data["friendly_id"],
45+
handle: return_reason_data["handle"],
46+
)
47+
end
48+
end
49+
50+
validates :id, presence: true, numericality: { only_integer: true }, on: :create
51+
validates :name, presence: true, on: :create
52+
validates :friendly_id, presence: true, on: :create
53+
validates :handle, presence: true, on: :create
54+
validates :description, presence: true, on: :create
55+
validates_with ProductTaxonomy::Indexed::UniquenessValidator, attributes: [:friendly_id], on: :create
56+
57+
localized_attr_reader :name, :description
58+
59+
attr_reader :id, :friendly_id, :handle
60+
61+
# @param id [Integer] The ID of the return reason.
62+
# @param name [String] The name of the return reason.
63+
# @param description [String] The description of the return reason.
64+
# @param friendly_id [String] The friendly ID of the return reason.
65+
# @param handle [String] The handle of the return reason.
66+
def initialize(id:, name:, description:, friendly_id:, handle:)
67+
@id = id
68+
@name = name
69+
@description = description
70+
@friendly_id = friendly_id
71+
@handle = handle
72+
end
73+
74+
# The global ID of the return reason
75+
#
76+
# @return [String]
77+
def gid
78+
"gid://shopify/ReturnReasonDefinition/#{id}"
79+
end
80+
end
81+
end
82+

dev/lib/product_taxonomy/models/serializers/category/data/data_serializer.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,19 @@ def serialize_all(root = nil)
1414
# @param [Category] category
1515
# @return [Hash]
1616
def serialize(category)
17+
return_reason_ids = category.return_reasons.map(&:friendly_id)
18+
special_reasons = return_reason_ids.select { |r| ['unknown', 'other'].include?(r) }
19+
regular_reasons = return_reason_ids.reject { |r| ['unknown', 'other'].include?(r) }
20+
sorted_return_reasons = AlphanumericSorter.sort(regular_reasons) +
21+
(special_reasons.include?('unknown') ? ['unknown'] : []) +
22+
(special_reasons.include?('other') ? ['other'] : [])
23+
1724
{
1825
"id" => category.id,
1926
"name" => category.name,
2027
"children" => category.children.sort_by(&:id_parts).map(&:id),
2128
"attributes" => AlphanumericSorter.sort(category.attributes.map(&:friendly_id), other_last: true),
29+
"return_reasons" => sorted_return_reasons,
2230
}
2331
end
2432
end

dev/lib/product_taxonomy/models/serializers/category/dist/json_serializer.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ def serialize(category, locale: "en")
3838
"extended" => attr.is_a?(ExtendedAttribute),
3939
}
4040
end,
41+
"return_reasons" => category.return_reasons.map do |return_reason|
42+
{
43+
"id" => return_reason.gid,
44+
"name" => return_reason.name(locale:),
45+
"handle" => return_reason.handle,
46+
"description" => return_reason.description(locale:),
47+
}
48+
end,
4149
"children" => category.children.map do |child|
4250
{
4351
"id" => child.gid,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
module Serializers
5+
module ReturnReason
6+
module Data
7+
module DataSerializer
8+
class << self
9+
def serialize_all
10+
ProductTaxonomy::ReturnReason.all.sort_by(&:id).map { serialize(_1) }
11+
end
12+
13+
# @param [ReturnReason] return_reason
14+
# @return [Hash]
15+
def serialize(return_reason)
16+
{
17+
"id" => return_reason.id,
18+
"name" => return_reason.name,
19+
"description" => return_reason.description,
20+
"friendly_id" => return_reason.friendly_id,
21+
"handle" => return_reason.handle,
22+
}
23+
end
24+
end
25+
end
26+
end
27+
end
28+
end
29+
end
30+
31+
32+
33+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
module Serializers
5+
module ReturnReason
6+
module Data
7+
class LocalizationsSerializer
8+
class << self
9+
# @param locale [String]
10+
# @return [Hash]
11+
def serialize_all(locale: "en")
12+
{
13+
locale => {
14+
"return_reasons" => ProductTaxonomy::ReturnReason.all.sort_by(&:friendly_id).each_with_object({}) do |return_reason, hash|
15+
hash[return_reason.friendly_id] = serialize(return_reason, locale:)
16+
end,
17+
},
18+
}
19+
end
20+
21+
# @param return_reason [ReturnReason]
22+
# @param locale [String]
23+
# @return [Hash]
24+
def serialize(return_reason, locale: "en")
25+
{
26+
"name" => return_reason.name(locale:),
27+
"description" => return_reason.description(locale:),
28+
}
29+
end
30+
end
31+
end
32+
end
33+
end
34+
end
35+
end
36+
37+
38+
39+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
module Serializers
5+
module ReturnReason
6+
module Dist
7+
class JsonSerializer
8+
class << self
9+
def serialize_all(version:, locale: "en")
10+
{
11+
"version" => version,
12+
"return_reasons" => ProductTaxonomy::ReturnReason.all.sort_by(&:name).map { serialize(_1, locale:) },
13+
}
14+
end
15+
16+
# @param return_reason [ReturnReason]
17+
# @param locale [String] The locale to use for localized return reasons.
18+
# @return [Hash]
19+
def serialize(return_reason, locale: "en")
20+
{
21+
"id" => return_reason.gid,
22+
"name" => return_reason.name(locale:),
23+
"handle" => return_reason.handle,
24+
"description" => return_reason.description(locale:),
25+
}
26+
end
27+
end
28+
end
29+
end
30+
end
31+
end
32+
end
33+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
module Serializers
5+
module ReturnReason
6+
module Dist
7+
class TxtSerializer
8+
class << self
9+
def serialize_all(version:, locale: "en", padding: longest_gid_length)
10+
header = <<~HEADER
11+
# Shopify Product Taxonomy - Return Reasons: #{version}
12+
# Format: {GID} : {Return reason name}
13+
14+
HEADER
15+
16+
return_reasons_txt = ProductTaxonomy::ReturnReason
17+
.all
18+
.sort_by(&:name)
19+
.map { serialize(_1, padding:, locale:) }
20+
.join("\n")
21+
22+
header + return_reasons_txt
23+
end
24+
25+
# @param return_reason [ReturnReason]
26+
# @param padding [Integer] The padding to use for the GID.
27+
# @param locale [String] The locale to use for localized return reasons.
28+
# @return [String]
29+
def serialize(return_reason, padding: 0, locale: "en")
30+
"#{return_reason.gid.ljust(padding)} : #{return_reason.name(locale:)}"
31+
end
32+
33+
private
34+
35+
def longest_gid_length
36+
ProductTaxonomy::ReturnReason.all.map { _1.gid.length }.max || 0
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end
43+
end
44+
45+
46+
47+

0 commit comments

Comments
 (0)