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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be5ac84a6073e85553af278f37aed96cab1d39d3284817361e76060ca789f638
4
- data.tar.gz: 2dfbf53884b07ea2e894e46b8dc3c99f8843b997ff6174a9a154ee1299ce6993
3
+ metadata.gz: 83971fbd3793351ab03991240943191a793a6e77ca81ab81672384a19c8fb176
4
+ data.tar.gz: e199d4d635856cdec45f68c10a2cdf8cbd0964aef04b3cd6691af21c3b473b8e
5
5
  SHA512:
6
- metadata.gz: 3e6c001c6f0af07cda0207acda610c20cb6eabd41c2a3fd24f8c4ea026c46e1b0dc8b6c7a84b5d3fcf22ec4b0bf974c434cdfe668a50049d4320260005d2b34c
7
- data.tar.gz: 5115dad31f5607464892aea0b3dbee5d6761b84f21c81e26cc73febe091a13a1c7358911f89b21c37c618f78cbbd5f32a437ea131627502ae0872f5be486a538
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
- ActiveRecord::Base.transaction do
11
- user = Dscf::Core::User.new(
12
- phone: registration_params[:phone],
13
- password: registration_params[:password],
14
- password_confirmation: registration_params[:password_confirmation]
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
- render_success("retailers.success.register", data: retailer, status: :created)
43
- end
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 registration_params
52
+ def retailer_registration_params
71
53
  params.require(:retailer).permit(
72
- :name, :phone, :tin_number, :location, :password, :password_confirmation
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, :onboarded_at, :created_at, :updated_at
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
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.16.2".freeze
3
+ VERSION = "0.17.0".freeze
4
4
  end
5
5
  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.16.2
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.5
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.5
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
@@ -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 "--------------------------------------------------------"