Skip to content

Commit 7160261

Browse files
ricardotejedorsanzHetul-Patel-2
authored andcommitted
Add CLI command for mapping return reasons to categories
- Add add_return_reasons_to_categories_command.rb - Maps specified return reasons to specified categories - Validates return reason references - Auto-sorts alphabetically with Unknown/Other last - Supports --include-descendants flag for cascade - Register command in cli.rb
1 parent 16026d8 commit 7160261

9 files changed

+555
-15
lines changed

dev/lib/product_taxonomy.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ def data_path = DATA_PATH
5959
require_relative "product_taxonomy/commands/dump_categories_command"
6060
require_relative "product_taxonomy/commands/dump_attributes_command"
6161
require_relative "product_taxonomy/commands/dump_values_command"
62+
require_relative "product_taxonomy/commands/dump_return_reasons_command"
6263
require_relative "product_taxonomy/commands/dump_integration_full_names_command"
6364
require_relative "product_taxonomy/commands/sync_en_localizations_command"
6465
require_relative "product_taxonomy/commands/add_category_command"
6566
require_relative "product_taxonomy/commands/add_attribute_command"
6667
require_relative "product_taxonomy/commands/add_attributes_to_categories_command"
6768
require_relative "product_taxonomy/commands/add_value_command"
69+
require_relative "product_taxonomy/commands/add_return_reason_command"
70+
require_relative "product_taxonomy/commands/add_return_reasons_to_categories_command"

dev/lib/product_taxonomy/cli.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,12 @@ def add_return_reason(name, description)
105105
def dump_return_reasons
106106
DumpReturnReasonsCommand.new(options).run
107107
end
108+
109+
desc "add_return_reasons_to_categories RETURN_REASON_FRIENDLY_IDS CATEGORY_IDS",
110+
"Add one or more return reasons to one or more categories. RETURN_REASON_FRIENDLY_IDS is a comma-separated list of return reason friendly IDs."
111+
option :include_descendants, type: :boolean, desc: "When set, the return reasons will be added to all descendants of the specified categories"
112+
def add_return_reasons_to_categories(return_reason_friendly_ids, category_ids)
113+
AddReturnReasonsToCategoriesCommand.new(options.merge(return_reason_friendly_ids:, category_ids:)).run
114+
end
108115
end
109116
end

dev/lib/product_taxonomy/commands/add_return_reason_command.rb

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,13 @@ def execute
1717
private
1818

1919
def create_return_reason!
20-
@return_reason = ReturnReason.new(
20+
@return_reason = ReturnReason.create_validate_and_add!(
2121
id: ReturnReason.next_id,
2222
name: @name,
2323
description: @description,
2424
friendly_id:,
2525
handle:,
2626
)
27-
28-
begin
29-
@return_reason.validate!(:create)
30-
rescue ActiveModel::ValidationError => e
31-
raise ActiveModel::ValidationError.new(e.model), "Failed to create return reason: #{e.message}"
32-
end
33-
34-
ReturnReason.add(@return_reason)
3527
logger.info("Created return reason `#{@return_reason.name}` with friendly_id=`#{@return_reason.friendly_id}`")
3628
end
3729

@@ -50,6 +42,3 @@ def handle
5042
end
5143
end
5244
end
53-
54-
55-
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
class AddReturnReasonsToCategoriesCommand < Command
5+
def initialize(options)
6+
super
7+
load_taxonomy
8+
@return_reason_friendly_ids = options[:return_reason_friendly_ids]
9+
@category_ids = options[:category_ids]
10+
@include_descendants = options[:include_descendants]
11+
end
12+
13+
def execute
14+
add_return_reasons_to_categories!
15+
update_data_files!
16+
end
17+
18+
private
19+
20+
def add_return_reasons_to_categories!
21+
@return_reasons = return_reason_friendly_ids.map { |friendly_id| ReturnReason.find_by!(friendly_id:) }
22+
@categories = category_ids.map { |id| Category.find_by!(id:) }
23+
@categories = @categories.flat_map(&:descendants_and_self) if @include_descendants
24+
25+
@categories.each do |category|
26+
@return_reasons.each do |return_reason|
27+
if category.return_reasons.include?(return_reason)
28+
logger.info("Category `#{category.name}` already has return reason `#{return_reason.friendly_id}` - skipping")
29+
else
30+
category.add_return_reason(return_reason)
31+
end
32+
end
33+
34+
sort_return_reasons!(category)
35+
end
36+
37+
logger.info("Added #{@return_reasons.size} return reason(s) to #{@categories.size} categories")
38+
end
39+
40+
def sort_return_reasons!(category)
41+
special_reasons = category.return_reasons.select { |r| ["unknown", "other"].include?(r.friendly_id) }
42+
regular_reasons = category.return_reasons.reject { |r| ["unknown", "other"].include?(r.friendly_id) }
43+
44+
sorted_friendly_ids = AlphanumericSorter.sort(regular_reasons.map(&:friendly_id))
45+
regular_reasons = sorted_friendly_ids.map { |fid| regular_reasons.find { |r| r.friendly_id == fid } }
46+
47+
# Add special reasons at the end in the correct order
48+
unknown = special_reasons.find { |r| r.friendly_id == "unknown" }
49+
other = special_reasons.find { |r| r.friendly_id == "other" }
50+
51+
sorted_reasons = regular_reasons + [unknown, other].compact
52+
53+
category.return_reasons.replace(sorted_reasons)
54+
end
55+
56+
def update_data_files!
57+
roots = @categories.map(&:root).uniq.map(&:id)
58+
DumpCategoriesCommand.new(verticals: roots).execute
59+
SyncEnLocalizationsCommand.new(targets: "categories").execute
60+
GenerateDocsCommand.new({}).execute
61+
end
62+
63+
def return_reason_friendly_ids
64+
@return_reason_friendly_ids.split(",").map(&:strip)
65+
end
66+
67+
def category_ids
68+
@category_ids.split(",").map(&:strip)
69+
end
70+
end
71+
end

dev/lib/product_taxonomy/commands/dump_return_reasons_command.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,3 @@ def execute
1717
end
1818
end
1919
end
20-
21-
22-

dev/lib/product_taxonomy/models/category.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ def add_attribute(attribute)
138138
@attributes << attribute
139139
end
140140

141+
# Add a return reason to the category
142+
#
143+
# @param [ReturnReason] return_reason
144+
def add_return_reason(return_reason)
145+
@return_reasons << return_reason
146+
end
147+
141148
#
142149
# Information
143150
#
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
module ProductTaxonomy
6+
class AddReturnReasonCommandTest < TestCase
7+
setup do
8+
@existing_return_reason = ReturnReason.new(
9+
id: 1,
10+
name: "Defective or Doesn't Work",
11+
description: "Item is broken, defective, or doesn't function as expected",
12+
friendly_id: "defective_or_doesnt_work",
13+
handle: "defective-or-doesnt-work",
14+
)
15+
16+
ReturnReason.add(@existing_return_reason)
17+
18+
AddReturnReasonCommand.any_instance.stubs(:load_taxonomy)
19+
DumpReturnReasonsCommand.any_instance.stubs(:load_taxonomy)
20+
SyncEnLocalizationsCommand.any_instance.stubs(:load_taxonomy)
21+
GenerateDocsCommand.any_instance.stubs(:load_taxonomy)
22+
end
23+
24+
test "execute successfully adds a new return reason" do
25+
DumpReturnReasonsCommand.any_instance.expects(:execute).once
26+
SyncEnLocalizationsCommand.expects(:new).with(targets: "return_reasons").returns(stub(execute: true))
27+
GenerateDocsCommand.any_instance.expects(:execute).once
28+
29+
AddReturnReasonCommand.new(
30+
name: "Wrong Size or Fit",
31+
description: "Item doesn't fit properly or is not the expected size",
32+
).execute
33+
34+
new_return_reason = ReturnReason.find_by(friendly_id: "wrong_size_or_fit")
35+
assert_not_nil new_return_reason
36+
assert_equal 2, new_return_reason.id # Since id 1 already exists
37+
assert_equal "Wrong Size or Fit", new_return_reason.name
38+
assert_equal "Item doesn't fit properly or is not the expected size", new_return_reason.description
39+
assert_equal "wrong_size_or_fit", new_return_reason.friendly_id
40+
assert_equal "wrong-size-or-fit", new_return_reason.handle
41+
end
42+
43+
test "execute generates correct friendly_id and handle from name" do
44+
stub_commands
45+
46+
AddReturnReasonCommand.new(
47+
name: "Not As Described",
48+
description: "Item received differs from the product description",
49+
).execute
50+
51+
new_return_reason = ReturnReason.find_by(friendly_id: "not_as_described")
52+
assert_not_nil new_return_reason
53+
assert_equal "not_as_described", new_return_reason.friendly_id
54+
assert_equal "not-as-described", new_return_reason.handle
55+
end
56+
57+
test "execute raises error when return reason with same friendly_id already exists" do
58+
stub_commands
59+
60+
assert_raises(ActiveModel::ValidationError) do
61+
AddReturnReasonCommand.new(
62+
name: "Defective or Doesn't Work", # This will generate the same friendly_id as @existing_return_reason
63+
description: "Another defective description",
64+
).execute
65+
end
66+
end
67+
68+
test "execute raises error when name is empty" do
69+
stub_commands
70+
71+
assert_raises(ActiveModel::ValidationError) do
72+
AddReturnReasonCommand.new(
73+
name: "",
74+
description: "Valid description",
75+
).execute
76+
end
77+
end
78+
79+
test "execute raises error when description is empty" do
80+
stub_commands
81+
82+
assert_raises(ActiveModel::ValidationError) do
83+
AddReturnReasonCommand.new(
84+
name: "Valid Name",
85+
description: "",
86+
).execute
87+
end
88+
end
89+
90+
test "execute updates data files after creating return reason" do
91+
DumpReturnReasonsCommand.any_instance.expects(:execute).once
92+
SyncEnLocalizationsCommand.expects(:new).with(targets: "return_reasons").returns(stub(execute: true))
93+
GenerateDocsCommand.any_instance.expects(:execute).once
94+
95+
AddReturnReasonCommand.new(
96+
name: "Changed My Mind",
97+
description: "Customer no longer wants the item",
98+
).execute
99+
end
100+
101+
test "execute assigns sequential IDs to multiple return reasons" do
102+
stub_commands
103+
104+
AddReturnReasonCommand.new(
105+
name: "First Reason",
106+
description: "First description",
107+
).execute
108+
109+
first_reason = ReturnReason.find_by(friendly_id: "first_reason")
110+
assert_equal 2, first_reason.id
111+
112+
AddReturnReasonCommand.new(
113+
name: "Second Reason",
114+
description: "Second description",
115+
).execute
116+
117+
second_reason = ReturnReason.find_by(friendly_id: "second_reason")
118+
assert_equal 3, second_reason.id
119+
end
120+
121+
test "execute handles special characters in name properly" do
122+
stub_commands
123+
124+
AddReturnReasonCommand.new(
125+
name: "Item's Quality & Appearance (Not Good!)",
126+
description: "Quality or appearance issues with the product",
127+
).execute
128+
129+
new_return_reason = ReturnReason.find_by(friendly_id: "items_quality_appearance_not_good")
130+
assert_not_nil new_return_reason
131+
assert_equal "Item's Quality & Appearance (Not Good!)", new_return_reason.name
132+
assert_equal "items_quality_appearance_not_good", new_return_reason.friendly_id
133+
assert_equal "items-quality-appearance-not-good", new_return_reason.handle
134+
end
135+
136+
private
137+
138+
def stub_commands
139+
DumpReturnReasonsCommand.any_instance.stubs(:execute)
140+
SyncEnLocalizationsCommand.any_instance.stubs(:execute)
141+
GenerateDocsCommand.any_instance.stubs(:execute)
142+
end
143+
end
144+
end

0 commit comments

Comments
 (0)