dscf-marketplace 0.16.2 → 0.17.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 +4 -4
- data/app/controllers/dscf/marketplace/retailers_controller.rb +45 -36
- data/app/models/dscf/marketplace/retailer/registration.rb +155 -0
- data/app/models/dscf/marketplace/retailer.rb +12 -1
- data/app/models/dscf/marketplace/retailer_category_preference.rb +10 -0
- data/app/serializers/dscf/marketplace/retailer_serializer.rb +3 -1
- data/db/migrate/20260730000001_add_registration_details_to_dscf_marketplace_retailers.rb +26 -0
- data/lib/dscf/marketplace/version.rb +1 -1
- metadata +6 -4
- data/db/antigravity_seeds.rb +0 -274
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83971fbd3793351ab03991240943191a793a6e77ca81ab81672384a19c8fb176
|
|
4
|
+
data.tar.gz: e199d4d635856cdec45f68c10a2cdf8cbd0964aef04b3cd6691af21c3b473b8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 173d35332f91c1b9bdba15b1a7b0be9b8d2d0eb407bd9c6a93e7598552d75904e05e7a8eab3a312a792f03ddc87aa9375b61a6b436d527f1a00458142efc9e69
|
|
7
|
+
data.tar.gz: 438054a1bc9e478185960e1aea04757dee9f4ab9246b041a7712d53bb392dee0ad0de180f995731dc20ed410c5a1281f9719eb3e88b0c9040ed0518714b7ce78
|
|
@@ -7,40 +7,21 @@ module Dscf
|
|
|
7
7
|
skip_before_action :authenticate_user, only: [ :register ]
|
|
8
8
|
|
|
9
9
|
def register
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# `authenticate_user` is skipped for this action so self-service
|
|
18
|
-
# registration stays public, but `current_user` still resolves
|
|
19
|
-
# whatever Bearer token was sent — an agent onboarding a retailer
|
|
20
|
-
# on their behalf attaches their own token, so we derive the
|
|
21
|
-
# onboarding agent from it rather than trusting a client-supplied
|
|
22
|
-
# agent_id. Guard on current_user first: `find_by(user_id: nil)`
|
|
23
|
-
# matches any Agent row with a null user_id, which silently
|
|
24
|
-
# attributed every anonymous self-signup to a random agent.
|
|
25
|
-
onboarding_agent = current_user && Dscf::Marketplace::Agent.find_by(user_id: current_user.id)
|
|
26
|
-
|
|
27
|
-
retailer = Dscf::Marketplace::Retailer.new(
|
|
28
|
-
name: registration_params[:name],
|
|
29
|
-
phone: registration_params[:phone],
|
|
30
|
-
tin_number: registration_params[:tin_number],
|
|
31
|
-
location: registration_params[:location],
|
|
32
|
-
status: :active,
|
|
33
|
-
agent: onboarding_agent,
|
|
34
|
-
onboarded_at: (Time.current if onboarding_agent)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
user.save!
|
|
38
|
-
assign_default_role(user, "RETAILER")
|
|
39
|
-
retailer.user = user
|
|
40
|
-
retailer.save!
|
|
10
|
+
registration = Dscf::Marketplace::Retailer::Registration.new(
|
|
11
|
+
user_attributes: user_registration_params,
|
|
12
|
+
retailer_attributes: retailer_registration_params,
|
|
13
|
+
address_attributes: address_registration_params,
|
|
14
|
+
actor: current_user,
|
|
15
|
+
obsolete_profile_address: obsolete_profile_address?
|
|
16
|
+
).create!
|
|
41
17
|
|
|
42
|
-
|
|
43
|
-
|
|
18
|
+
render_success(
|
|
19
|
+
"retailers.success.register",
|
|
20
|
+
data: registration.retailer,
|
|
21
|
+
status: :created
|
|
22
|
+
)
|
|
23
|
+
rescue Dscf::Marketplace::Retailer::Registration::InvalidInput => e
|
|
24
|
+
render_error("retailers.errors.register", errors: e.errors, status: :unprocessable_entity)
|
|
44
25
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
45
26
|
errors = e.respond_to?(:record) && e.record ? e.record.errors.full_messages : [ e.message ]
|
|
46
27
|
render_error("retailers.errors.register", errors: errors, status: :unprocessable_entity)
|
|
@@ -63,15 +44,43 @@ module Dscf
|
|
|
63
44
|
|
|
64
45
|
def model_params
|
|
65
46
|
params.require(:retailer).permit(
|
|
66
|
-
:name, :phone, :tin_number, :location, :agent_id
|
|
47
|
+
:name, :phone, :tin_number, :location, :agent_id, :branch_count,
|
|
48
|
+
preferred_category_ids: []
|
|
67
49
|
)
|
|
68
50
|
end
|
|
69
51
|
|
|
70
|
-
def
|
|
52
|
+
def retailer_registration_params
|
|
71
53
|
params.require(:retailer).permit(
|
|
72
|
-
:name, :phone, :tin_number, :location, :
|
|
54
|
+
:name, :phone, :tin_number, :location, :branch_count,
|
|
55
|
+
preferred_category_ids: []
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def user_registration_params
|
|
60
|
+
if params[:user].present?
|
|
61
|
+
params.require(:user).permit(
|
|
62
|
+
:email, :phone, :password, :password_confirmation, :temp_password,
|
|
63
|
+
user_profile_attributes: %i[first_name last_name gender date_of_birth]
|
|
64
|
+
)
|
|
65
|
+
else
|
|
66
|
+
legacy = params.require(:retailer).permit(
|
|
67
|
+
:phone, :password, :password_confirmation
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
legacy.slice(:phone, :password, :password_confirmation)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def address_registration_params
|
|
75
|
+
params.fetch(:address, {}).permit(
|
|
76
|
+
:country, :city, :sub_city, :woreda, :kebele,
|
|
77
|
+
:house_numbers, :po_box, :latitude, :longitude
|
|
73
78
|
)
|
|
74
79
|
end
|
|
80
|
+
|
|
81
|
+
def obsolete_profile_address?
|
|
82
|
+
params.dig(:user, :user_profile_attributes)&.key?(:address)
|
|
83
|
+
end
|
|
75
84
|
end
|
|
76
85
|
end
|
|
77
86
|
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class Retailer
|
|
4
|
+
class Registration
|
|
5
|
+
include Dscf::Core::RoleAssignable
|
|
6
|
+
|
|
7
|
+
class InvalidInput < StandardError
|
|
8
|
+
attr_reader :errors
|
|
9
|
+
|
|
10
|
+
def initialize(*errors)
|
|
11
|
+
@errors = errors.flatten
|
|
12
|
+
super(@errors.join(", "))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :user, :retailer, :address
|
|
17
|
+
|
|
18
|
+
def initialize(user_attributes:, retailer_attributes:, address_attributes: {}, actor: nil,
|
|
19
|
+
obsolete_profile_address: false)
|
|
20
|
+
@user_attributes = user_attributes.to_h.deep_symbolize_keys
|
|
21
|
+
@retailer_attributes = retailer_attributes.to_h.deep_symbolize_keys
|
|
22
|
+
@address_attributes = address_attributes.to_h.deep_symbolize_keys
|
|
23
|
+
@actor = actor
|
|
24
|
+
@obsolete_profile_address = obsolete_profile_address
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create!
|
|
28
|
+
validate_obsolete_profile_address!
|
|
29
|
+
validate_phone!
|
|
30
|
+
|
|
31
|
+
coordinates = registration_coordinates
|
|
32
|
+
categories = preferred_categories
|
|
33
|
+
|
|
34
|
+
ActiveRecord::Base.transaction do
|
|
35
|
+
@user = Dscf::Core::User.create!(@user_attributes)
|
|
36
|
+
assign_default_role(@user, "RETAILER")
|
|
37
|
+
@retailer = create_retailer!(coordinates)
|
|
38
|
+
@address = create_address!(coordinates)
|
|
39
|
+
@retailer.preferred_categories = categories
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_obsolete_profile_address!
|
|
48
|
+
return unless @obsolete_profile_address
|
|
49
|
+
|
|
50
|
+
raise InvalidInput,
|
|
51
|
+
"user.user_profile_attributes.address is unsupported; send a top-level address object"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_phone!
|
|
55
|
+
user_phone = @user_attributes[:phone].presence
|
|
56
|
+
retailer_phone = @retailer_attributes[:phone].presence
|
|
57
|
+
|
|
58
|
+
raise InvalidInput, "user.phone is required for retailer registration" unless user_phone
|
|
59
|
+
return unless retailer_phone && retailer_phone != user_phone
|
|
60
|
+
|
|
61
|
+
raise InvalidInput, "retailer.phone must match user.phone"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def registration_coordinates
|
|
65
|
+
@address_attributes.present? ? coordinates_from_address : coordinates_from_location
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def coordinates_from_address
|
|
69
|
+
latitude = @address_attributes[:latitude]
|
|
70
|
+
longitude = @address_attributes[:longitude]
|
|
71
|
+
|
|
72
|
+
unless latitude.is_a?(Numeric) && longitude.is_a?(Numeric)
|
|
73
|
+
raise InvalidInput, "address.latitude and address.longitude must both be numeric"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
validate_coordinates!(latitude.to_d, longitude.to_d)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def coordinates_from_location
|
|
80
|
+
location = @retailer_attributes[:location].to_s
|
|
81
|
+
parts = location.split(",", -1).map(&:strip)
|
|
82
|
+
|
|
83
|
+
unless parts.size == 2 && parts.all? { |part| decimal_string?(part) }
|
|
84
|
+
raise InvalidInput, "retailer.location must be a valid \"latitude,longitude\" pair"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
validate_coordinates!(BigDecimal(parts.first), BigDecimal(parts.last))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def decimal_string?(value)
|
|
91
|
+
value.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_coordinates!(latitude, longitude)
|
|
95
|
+
errors = []
|
|
96
|
+
errors << "latitude must be between -90 and 90 and cannot be zero" unless latitude.between?(-90, 90) && !latitude.zero?
|
|
97
|
+
errors << "longitude must be between -180 and 180 and cannot be zero" unless longitude.between?(-180, 180) && !longitude.zero?
|
|
98
|
+
raise InvalidInput, errors if errors.any?
|
|
99
|
+
|
|
100
|
+
{latitude: latitude, longitude: longitude}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def preferred_categories
|
|
104
|
+
ids = Array(@retailer_attributes.delete(:preferred_category_ids)).compact.map do |id|
|
|
105
|
+
Integer(id, exception: false)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
raise InvalidInput, "preferred_category_ids must contain integers" if ids.any?(&:nil?)
|
|
109
|
+
|
|
110
|
+
ids = ids.uniq
|
|
111
|
+
categories = Dscf::Marketplace::Category.where(id: ids).index_by(&:id)
|
|
112
|
+
missing_ids = ids - categories.keys
|
|
113
|
+
raise InvalidInput, "Unknown preferred category ids: #{missing_ids.join(", ")}" if missing_ids.any?
|
|
114
|
+
|
|
115
|
+
ids.map { |id| categories.fetch(id) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def create_retailer!(coordinates)
|
|
119
|
+
agent = @actor && Dscf::Marketplace::Agent.find_by(user_id: @actor.id)
|
|
120
|
+
|
|
121
|
+
Dscf::Marketplace::Retailer.create!(
|
|
122
|
+
@retailer_attributes.except(:preferred_category_ids).merge(
|
|
123
|
+
phone: @user.phone,
|
|
124
|
+
location: format_location(coordinates),
|
|
125
|
+
status: :active,
|
|
126
|
+
user: @user,
|
|
127
|
+
agent: agent,
|
|
128
|
+
onboarded_at: (Time.current if agent)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def create_address!(coordinates)
|
|
134
|
+
permitted_attributes = @address_attributes.slice(
|
|
135
|
+
:country, :city, :sub_city, :woreda, :kebele, :house_numbers, :po_box
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
Dscf::Core::Address.create!(
|
|
139
|
+
permitted_attributes.merge(
|
|
140
|
+
user: @user,
|
|
141
|
+
address_type: :shipping,
|
|
142
|
+
country: permitted_attributes[:country].presence || "Ethiopia",
|
|
143
|
+
latitude: coordinates[:latitude],
|
|
144
|
+
longitude: coordinates[:longitude]
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def format_location(coordinates)
|
|
150
|
+
"#{coordinates[:latitude].to_s("F")},#{coordinates[:longitude].to_s("F")}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -3,6 +3,12 @@ module Dscf::Marketplace
|
|
|
3
3
|
# Associations
|
|
4
4
|
belongs_to :user, class_name: "Dscf::Core::User", optional: true
|
|
5
5
|
belongs_to :agent, class_name: "Dscf::Marketplace::Agent", optional: true
|
|
6
|
+
has_many :retailer_category_preferences,
|
|
7
|
+
class_name: "Dscf::Marketplace::RetailerCategoryPreference",
|
|
8
|
+
dependent: :destroy
|
|
9
|
+
has_many :preferred_categories,
|
|
10
|
+
through: :retailer_category_preferences,
|
|
11
|
+
source: :category
|
|
6
12
|
|
|
7
13
|
# Enums
|
|
8
14
|
enum :status, {active: 0, inactive: 1, suspended: 2}, default: :active
|
|
@@ -12,13 +18,18 @@ module Dscf::Marketplace
|
|
|
12
18
|
validates :phone, presence: true
|
|
13
19
|
# TIN is optional — retailers onboarded in the field may not have one yet.
|
|
14
20
|
validates :location, presence: true
|
|
21
|
+
validates :branch_count, numericality: {only_integer: true, greater_than: 0}, allow_nil: true
|
|
15
22
|
|
|
16
23
|
# Scopes
|
|
17
24
|
scope :by_agent, ->(id) { where(agent_id: id) }
|
|
18
25
|
|
|
19
26
|
# Ransack
|
|
20
27
|
def self.ransackable_attributes(_auth_object = nil)
|
|
21
|
-
%w[id name phone tin_number location status agent_id onboarded_at created_at updated_at]
|
|
28
|
+
%w[id name phone tin_number location status agent_id branch_count onboarded_at created_at updated_at]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shipping_address
|
|
32
|
+
user&.addresses&.shipping&.order(:id)&.first
|
|
22
33
|
end
|
|
23
34
|
end
|
|
24
35
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class RetailerCategoryPreference < ApplicationRecord
|
|
4
|
+
belongs_to :retailer, class_name: "Dscf::Marketplace::Retailer"
|
|
5
|
+
belongs_to :category, class_name: "Dscf::Marketplace::Category"
|
|
6
|
+
|
|
7
|
+
validates :category_id, uniqueness: {scope: :retailer_id}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -2,9 +2,11 @@ module Dscf
|
|
|
2
2
|
module Marketplace
|
|
3
3
|
class RetailerSerializer < ActiveModel::Serializer
|
|
4
4
|
attributes :id, :name, :phone, :tin_number, :location, :status,
|
|
5
|
-
:agent_id, :
|
|
5
|
+
:agent_id, :branch_count, :preferred_category_ids,
|
|
6
|
+
:onboarded_at, :created_at, :updated_at
|
|
6
7
|
|
|
7
8
|
belongs_to :agent
|
|
9
|
+
has_one :shipping_address, serializer: Dscf::Core::AddressSerializer
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class AddRegistrationDetailsToDscfMarketplaceRetailers < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :dscf_marketplace_retailers, :branch_count, :integer
|
|
4
|
+
add_check_constraint :dscf_marketplace_retailers,
|
|
5
|
+
"branch_count IS NULL OR branch_count > 0",
|
|
6
|
+
name: "retailers_branch_count_positive"
|
|
7
|
+
|
|
8
|
+
create_table :dscf_marketplace_retailer_category_preferences do |t|
|
|
9
|
+
t.references :retailer,
|
|
10
|
+
null: false,
|
|
11
|
+
index: {name: "retailer_on_dm_category_preferences_idx"},
|
|
12
|
+
foreign_key: {to_table: :dscf_marketplace_retailers}
|
|
13
|
+
t.references :category,
|
|
14
|
+
null: false,
|
|
15
|
+
index: {name: "category_on_dm_retailer_preferences_idx"},
|
|
16
|
+
foreign_key: {to_table: :dscf_marketplace_categories}
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :dscf_marketplace_retailer_category_preferences,
|
|
22
|
+
%i[ retailer_id category_id ],
|
|
23
|
+
unique: true,
|
|
24
|
+
name: "unique_dm_retailer_category_preferences_idx"
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dscf-marketplace
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - "~>"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: 0.3.
|
|
38
|
+
version: 0.3.15
|
|
39
39
|
type: :runtime
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: 0.3.
|
|
45
|
+
version: 0.3.15
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: active_model_serializers
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -467,6 +467,8 @@ files:
|
|
|
467
467
|
- app/models/dscf/marketplace/quotation_item.rb
|
|
468
468
|
- app/models/dscf/marketplace/request_for_quotation.rb
|
|
469
469
|
- app/models/dscf/marketplace/retailer.rb
|
|
470
|
+
- app/models/dscf/marketplace/retailer/registration.rb
|
|
471
|
+
- app/models/dscf/marketplace/retailer_category_preference.rb
|
|
470
472
|
- app/models/dscf/marketplace/rfq_item.rb
|
|
471
473
|
- app/models/dscf/marketplace/sub_supplier.rb
|
|
472
474
|
- app/models/dscf/marketplace/sub_supplier_product.rb
|
|
@@ -537,7 +539,6 @@ files:
|
|
|
537
539
|
- config/environments/production.rb
|
|
538
540
|
- config/locales/en.yml
|
|
539
541
|
- config/routes.rb
|
|
540
|
-
- db/antigravity_seeds.rb
|
|
541
542
|
- db/demo_seeds.rb
|
|
542
543
|
- db/migrate/20250827172043_create_dscf_marketplace_categories.rb
|
|
543
544
|
- db/migrate/20250827173526_update_dscf_marketplace_categories_indexes.rb
|
|
@@ -602,6 +603,7 @@ files:
|
|
|
602
603
|
- db/migrate/20260708000002_make_product_gross_weight_optional.rb
|
|
603
604
|
- db/migrate/20260709000001_remove_supplier_from_dscf_marketplace_sub_suppliers.rb
|
|
604
605
|
- db/migrate/20260725000001_add_parent_order_to_dscf_marketplace_orders.rb
|
|
606
|
+
- db/migrate/20260730000001_add_registration_details_to_dscf_marketplace_retailers.rb
|
|
605
607
|
- db/seeds.rb
|
|
606
608
|
- lib/dscf/marketplace.rb
|
|
607
609
|
- lib/dscf/marketplace/engine.rb
|
data/db/antigravity_seeds.rb
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
2
|
-
# ANTIGRAVITY SEEDS — Clean, collision-free, and fully idempotent demo data
|
|
3
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
4
|
-
# NOTE: This script does NOT delete any existing tables or database records.
|
|
5
|
-
# It safely finds-or-creates our custom test ecosystem.
|
|
6
|
-
|
|
7
|
-
puts "🌱 Initializing Antigravity test data safely..."
|
|
8
|
-
|
|
9
|
-
# ── 1. Find Existing Super Admin ──
|
|
10
|
-
admin = Dscf::Core::User.find_by(email: "admin@dscf.com")
|
|
11
|
-
unless admin
|
|
12
|
-
puts "❌ admin@dscf.com not found! Creating default admin as fallback..."
|
|
13
|
-
admin = Dscf::Core::User.create!(
|
|
14
|
-
email: "admin@dscf.com",
|
|
15
|
-
phone: "+251999000000",
|
|
16
|
-
password: "Admin@2024",
|
|
17
|
-
verified_at: Time.current,
|
|
18
|
-
temp_password: false
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
sa_role = Dscf::Core::Role.find_or_create_by!(code: "SUPER_ADMIN") { |r| r.name = "Super Admin"; r.active = true }
|
|
23
|
-
Dscf::Core::UserRole.find_or_create_by!(user: admin, role: sa_role)
|
|
24
|
-
|
|
25
|
-
# Ensure SUPER_ADMIN has basic orders permissions
|
|
26
|
-
orders_perms = Dscf::Core::Permission.where(resource: "orders")
|
|
27
|
-
orders_perms.each do |perm|
|
|
28
|
-
Dscf::Core::RolePermission.find_or_create_by!(role: sa_role, permission: perm)
|
|
29
|
-
end
|
|
30
|
-
puts "✅ Super Admin configured."
|
|
31
|
-
|
|
32
|
-
# ── 2. Find or Create Custom Antigravity Users safely ──
|
|
33
|
-
retailer_user = Dscf::Core::User.find_by(email: "antigravity.retailer@example.com")
|
|
34
|
-
unless retailer_user
|
|
35
|
-
phone = "+251977111111"
|
|
36
|
-
# Avoid phone collisions if already taken
|
|
37
|
-
while Dscf::Core::User.exists?(phone: phone)
|
|
38
|
-
phone = "+251977111#{rand(100..999)}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
retailer_user = Dscf::Core::User.create!(
|
|
42
|
-
email: "antigravity.retailer@example.com",
|
|
43
|
-
phone: phone,
|
|
44
|
-
password: "password",
|
|
45
|
-
verified_at: Time.current,
|
|
46
|
-
temp_password: false
|
|
47
|
-
)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
supplier_user = Dscf::Core::User.find_by(email: "antigravity.supplier@example.com")
|
|
51
|
-
unless supplier_user
|
|
52
|
-
phone = "+251977222222"
|
|
53
|
-
# Avoid phone collisions if already taken
|
|
54
|
-
while Dscf::Core::User.exists?(phone: phone)
|
|
55
|
-
phone = "+251977222#{rand(100..999)}"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
supplier_user = Dscf::Core::User.create!(
|
|
59
|
-
email: "antigravity.supplier@example.com",
|
|
60
|
-
phone: phone,
|
|
61
|
-
password: "password",
|
|
62
|
-
verified_at: Time.current,
|
|
63
|
-
temp_password: false
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Assign default USER role
|
|
68
|
-
user_role = Dscf::Core::Role.find_or_create_by!(code: "USER") { |r| r.name = "User"; r.active = true }
|
|
69
|
-
Dscf::Core::UserRole.find_or_create_by!(user: retailer_user, role: user_role)
|
|
70
|
-
Dscf::Core::UserRole.find_or_create_by!(user: supplier_user, role: user_role)
|
|
71
|
-
|
|
72
|
-
# Assign marketplace permissions to USER role
|
|
73
|
-
Dscf::Core::Permission.where(engine: "dscf-marketplace").find_each do |perm|
|
|
74
|
-
Dscf::Core::RolePermission.find_or_create_by!(role: user_role, permission: perm)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
puts "✅ Custom users verified/created."
|
|
78
|
-
|
|
79
|
-
# ── 3. Find or Create Custom Businesses safely ──
|
|
80
|
-
bt_manufacturer = Dscf::Core::BusinessType.find_or_create_by!(name: "Manufacturer")
|
|
81
|
-
bt_retailer = Dscf::Core::BusinessType.find_or_create_by!(name: "Retailer")
|
|
82
|
-
|
|
83
|
-
retailer_biz = Dscf::Core::Business.find_by(tin_number: "TIN-AG-RET-001")
|
|
84
|
-
unless retailer_biz
|
|
85
|
-
phone = "+251977111112"
|
|
86
|
-
while Dscf::Core::Business.exists?(contact_phone: phone)
|
|
87
|
-
phone = "+251977111#{rand(100..999)}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
retailer_biz = Dscf::Core::Business.create!(
|
|
91
|
-
name: "Antigravity Supermarket",
|
|
92
|
-
user: retailer_user,
|
|
93
|
-
business_type: bt_retailer,
|
|
94
|
-
contact_phone: phone,
|
|
95
|
-
description: "Antigravity Premium Retailer Store",
|
|
96
|
-
tin_number: "TIN-AG-RET-001"
|
|
97
|
-
)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
supplier_biz = Dscf::Core::Business.find_by(tin_number: "TIN-AG-SUP-001")
|
|
101
|
-
unless supplier_biz
|
|
102
|
-
phone = "+251977222223"
|
|
103
|
-
while Dscf::Core::Business.exists?(contact_phone: phone)
|
|
104
|
-
phone = "+251977222#{rand(100..999)}"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
supplier_biz = Dscf::Core::Business.create!(
|
|
108
|
-
name: "Antigravity Coffee & Textile Wholesaler",
|
|
109
|
-
user: supplier_user,
|
|
110
|
-
business_type: bt_manufacturer,
|
|
111
|
-
contact_phone: phone,
|
|
112
|
-
description: "Premium Antigravity Wholesaler Export",
|
|
113
|
-
tin_number: "TIN-AG-SUP-001"
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
aggregator_biz = Dscf::Core::Business.find_by(tin_number: "TIN-AG-AGG-001")
|
|
118
|
-
unless aggregator_biz
|
|
119
|
-
phone = "+251977333333"
|
|
120
|
-
while Dscf::Core::Business.exists?(contact_phone: phone)
|
|
121
|
-
phone = "+251977333#{rand(100..999)}"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
aggregator_biz = Dscf::Core::Business.create!(
|
|
125
|
-
name: "Antigravity Aggregators",
|
|
126
|
-
user: admin,
|
|
127
|
-
business_type: bt_manufacturer,
|
|
128
|
-
contact_phone: phone,
|
|
129
|
-
description: "Official Antigravity Marketplace Aggregator Hub",
|
|
130
|
-
tin_number: "TIN-AG-AGG-001"
|
|
131
|
-
)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
puts "✅ Custom businesses verified/created."
|
|
135
|
-
|
|
136
|
-
# ── 4. Categories & Units ──
|
|
137
|
-
cat_coffee = Dscf::Marketplace::Category.find_or_create_by!(name: "Antigravity Coffee") do |c|
|
|
138
|
-
c.description = "Specialty roasted Ethiopian coffee"
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
cat_textile = Dscf::Marketplace::Category.find_or_create_by!(name: "Antigravity Textiles") do |c|
|
|
142
|
-
c.description = "Premium handwoven fabrics"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
u_kg = Dscf::Marketplace::Unit.find_or_create_by!(code: "KG") do |u|
|
|
146
|
-
u.name = "Kilogram"
|
|
147
|
-
u.unit_type = :weight
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
u_meter = Dscf::Marketplace::Unit.find_or_create_by!(code: "M") do |u|
|
|
151
|
-
u.name = "Meter"
|
|
152
|
-
u.unit_type = :dimension
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
puts "✅ Categories & Units verified/created."
|
|
156
|
-
|
|
157
|
-
# ── 5. Products ──
|
|
158
|
-
p1 = Dscf::Marketplace::Product.find_or_create_by!(sku: "AG-COF-SID") do |p|
|
|
159
|
-
p.name = "Antigravity Sidamo Roasted Coffee"
|
|
160
|
-
p.description = "Premium washed Sidamo — Medium Roast"
|
|
161
|
-
p.category = cat_coffee
|
|
162
|
-
p.unit = u_kg
|
|
163
|
-
p.base_quantity = 1.0
|
|
164
|
-
p.gross_weight = 1.2
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
p2 = Dscf::Marketplace::Product.find_or_create_by!(sku: "AG-TEX-GAB") do |p|
|
|
168
|
-
p.name = "Antigravity Traditional Gabi Fabric"
|
|
169
|
-
p.description = "Handwoven thick cotton Gabi"
|
|
170
|
-
p.category = cat_textile
|
|
171
|
-
p.unit = u_meter
|
|
172
|
-
p.base_quantity = 1.0
|
|
173
|
-
p.gross_weight = 0.8
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
puts "✅ Products verified/created."
|
|
177
|
-
|
|
178
|
-
# ── 6. Supplier Record ──
|
|
179
|
-
supplier_record = Dscf::Marketplace::Supplier.find_or_create_by!(business_id: supplier_biz.id) do |s|
|
|
180
|
-
s.name = "Antigravity Wholesaler Outlet"
|
|
181
|
-
s.business_type = :manufacturer
|
|
182
|
-
s.contact_phone = supplier_biz.contact_phone
|
|
183
|
-
s.address = "Bole Sub-city, Addis Ababa"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# ── 7. Supplier Products ──
|
|
187
|
-
sp1 = Dscf::Marketplace::SupplierProduct.find_or_create_by!(business_id: supplier_biz.id, product_id: p1.id) do |sp|
|
|
188
|
-
sp.supplier = supplier_record
|
|
189
|
-
sp.supplier_price = 400
|
|
190
|
-
sp.available_quantity = 1000
|
|
191
|
-
sp.minimum_order_quantity = 5
|
|
192
|
-
sp.status = :active
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
sp2 = Dscf::Marketplace::SupplierProduct.find_or_create_by!(business_id: supplier_biz.id, product_id: p2.id) do |sp|
|
|
196
|
-
sp.supplier = supplier_record
|
|
197
|
-
sp.supplier_price = 300
|
|
198
|
-
sp.available_quantity = 500
|
|
199
|
-
sp.minimum_order_quantity = 2
|
|
200
|
-
sp.status = :active
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
puts "✅ Supplier Products verified/created."
|
|
204
|
-
|
|
205
|
-
# ── 8. Aggregator Listings (Sprint 2 Core Target) ──
|
|
206
|
-
al1 = Dscf::Marketplace::AggregatorListing.find_or_create_by!(aggregator_id: aggregator_biz.id, supplier_product_id: sp1.id) do |al|
|
|
207
|
-
al.price = 550
|
|
208
|
-
al.quantity = 200
|
|
209
|
-
al.status = 0 # active/published
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
al2 = Dscf::Marketplace::AggregatorListing.find_or_create_by!(aggregator_id: aggregator_biz.id, supplier_product_id: sp2.id) do |al|
|
|
213
|
-
al.price = 420
|
|
214
|
-
al.quantity = 150
|
|
215
|
-
al.status = 0 # active/published
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
puts "✅ Aggregator Listings verified/created."
|
|
219
|
-
|
|
220
|
-
# ── 9. Direct Listings ──
|
|
221
|
-
Dscf::Marketplace::Listing.find_or_create_by!(business_id: supplier_biz.id, supplier_product_id: sp1.id) do |l|
|
|
222
|
-
l.price = 500
|
|
223
|
-
l.quantity = 300
|
|
224
|
-
l.status = :active
|
|
225
|
-
l.description = "Direct Sidamo Roasted Coffee from Antigravity Outlet"
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
Dscf::Marketplace::Listing.find_or_create_by!(business_id: supplier_biz.id, supplier_product_id: sp2.id) do |l|
|
|
229
|
-
l.price = 380
|
|
230
|
-
l.quantity = 200
|
|
231
|
-
l.status = :active
|
|
232
|
-
l.description = "Direct Traditional Handwoven Gabi from Antigravity Outlet"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
puts "✅ Direct Listings verified/created."
|
|
236
|
-
|
|
237
|
-
# ── 10. Demo Addresses & Agents ──
|
|
238
|
-
address = Dscf::Core::Address.find_by(user_id: retailer_user.id)
|
|
239
|
-
unless address
|
|
240
|
-
address = Dscf::Core::Address.create!(
|
|
241
|
-
user: retailer_user,
|
|
242
|
-
address_type: :shipping,
|
|
243
|
-
country: "Ethiopia",
|
|
244
|
-
city: "Addis Ababa",
|
|
245
|
-
sub_city: "Bole Sub-city",
|
|
246
|
-
woreda: "Woreda 03",
|
|
247
|
-
house_numbers: "Suite 4B"
|
|
248
|
-
)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
agent = Dscf::Marketplace::Agent.find_by(onboarded_by_id: aggregator_biz.id)
|
|
252
|
-
unless agent
|
|
253
|
-
agent_phone = "+251977444444"
|
|
254
|
-
while Dscf::Marketplace::Agent.exists?(phone: agent_phone)
|
|
255
|
-
agent_phone = "+251977444#{rand(100..999)}"
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
Dscf::Marketplace::Agent.create!(
|
|
259
|
-
name: "Antigravity Logistics Agent",
|
|
260
|
-
phone: agent_phone,
|
|
261
|
-
service_area: "Bole Sub-city",
|
|
262
|
-
status: :active,
|
|
263
|
-
verification_status: :verified,
|
|
264
|
-
onboarded_by: aggregator_biz
|
|
265
|
-
)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
puts "🎉 ALL ANTIGRAVITY SEEDS COMPLETED SUCCESSFULLY WITHOUT DELETING ANY DATA."
|
|
269
|
-
puts "--------------------------------------------------------"
|
|
270
|
-
puts "🔑 CREDENTIALS TO USE:"
|
|
271
|
-
puts "Buyer (Retailer): antigravity.retailer@example.com / password"
|
|
272
|
-
puts "Supplier (Wholesaler): antigravity.supplier@example.com / password"
|
|
273
|
-
puts "Super Admin: admin@dscf.com / Admin@2024"
|
|
274
|
-
puts "--------------------------------------------------------"
|