dscf-marketplace 0.1.6 → 0.1.8

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/dscf/marketplace/delivery_order.rb +107 -0
  3. data/app/models/dscf/marketplace/delivery_order_item.rb +192 -0
  4. data/app/models/dscf/marketplace/delivery_vehicle.rb +61 -0
  5. data/app/models/dscf/marketplace/order.rb +131 -0
  6. data/app/models/dscf/marketplace/order_item.rb +38 -0
  7. data/app/models/dscf/marketplace/quotation.rb +37 -0
  8. data/db/migrate/20250828115509_create_dscf_marketplace_orders.rb +19 -0
  9. data/db/migrate/20250828115525_create_dscf_marketplace_order_items.rb +22 -0
  10. data/db/migrate/20250831110907_create_dscf_marketplace_delivery_orders.rb +21 -0
  11. data/db/migrate/20250831110921_create_dscf_marketplace_delivery_order_items.rb +27 -0
  12. data/db/migrate/20250831110933_create_dscf_marketplace_delivery_vehicles.rb +17 -0
  13. data/db/migrate/20250901074509_add_delivery_handoff_fields_to_delivery_order_items.rb +16 -0
  14. data/db/migrate/20250901074746_add_delivery_order_to_orders.rb +7 -0
  15. data/db/migrate/20250901075511_add_delivery_vehicle_to_delivery_orders.rb +7 -0
  16. data/db/migrate/20250901080134_add_fulfillment_type_to_orders.rb +6 -0
  17. data/lib/dscf/marketplace/version.rb +1 -1
  18. data/spec/factories/dscf/core/addresses.rb +15 -0
  19. data/spec/factories/dscf/marketplace/delivery_order_items.rb +46 -0
  20. data/spec/factories/dscf/marketplace/delivery_orders.rb +14 -0
  21. data/spec/factories/dscf/marketplace/delivery_vehicles.rb +37 -0
  22. data/spec/factories/dscf/marketplace/listings.rb +16 -4
  23. data/spec/factories/dscf/marketplace/order_items.rb +49 -0
  24. data/spec/factories/dscf/marketplace/orders.rb +45 -0
  25. data/spec/factories/dscf/marketplace/{dscf/marketplace/quotation_items.rb → quotation_items.rb} +1 -1
  26. data/spec/factories/dscf/marketplace/{dscf/marketplace/quotations.rb → quotations.rb} +4 -4
  27. metadata +26 -7
  28. data/README.md +0 -28
  29. /data/spec/factories/dscf/marketplace/{dscf/marketplace/request_for_quotations.rb → request_for_quotations.rb} +0 -0
  30. /data/spec/factories/dscf/marketplace/{dscf/marketplace/rfq_items.rb → rfq_items.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82b1976ad1b3d99364b621d51ce06c5ac51e8eb818e10527580419d61f5aeb3d
4
- data.tar.gz: 6264cf89e35f2f24cc756f0eeaa48336af30b2e20473bab859453faf3964691e
3
+ metadata.gz: 9bbdcc2258fee7a63ff11ba8c87e73a47923e8dbb6d9737e55d6e3e9d7002822
4
+ data.tar.gz: 184a09306eb3c9de61c2e9edc82cdd49ed735e7bf91acae09b5ace7cd7e9f3a1
5
5
  SHA512:
6
- metadata.gz: 9a4e05d5b18d0bc545d036b9f623eaa65345b3e641cc069a1bde5fc0a7179fa0a0aa7bc4cd2d3243406c32dd4d5e0ee6ed3fbb05133770f9da638161a7a7341d
7
- data.tar.gz: 0eaa365be89d92f5320e5a7fca4ee74f300f19b4c5ad325740428bf456126cad807f9fbc700565262c289a912c2fe23661246308b9c9a192631a3a4388d9211d
6
+ metadata.gz: 503e6dcdb6f209cbc278219572beb1deadbbeff8910e979914b4cd316e176528bc91130b4917bc874e3de36afb9e0f4c56978ecc3aa0dd54f4e175ee8cd2a071
7
+ data.tar.gz: e2790cb38d3a76cff511631c8b4fc7ae4b277e8faa2459241379ba3127438b242449438d363e5976dda72aa90a4d3b354e091ebf3d699bf4afb3e48a504689ef
@@ -0,0 +1,107 @@
1
+ module Dscf::Marketplace
2
+ class DeliveryOrder < ApplicationRecord
3
+ enum :status, {assigned: 0, picked_up: 1, in_transit: 2, delivered: 3, failed: 4}
4
+ enum :vehicle_type, {motorcycle: 0, car: 1, van: 2, truck: 3}
5
+
6
+ belongs_to :driver, class_name: "Dscf::Core::User"
7
+ belongs_to :pickup_address, class_name: "Dscf::Core::Address"
8
+ belongs_to :delivery_vehicle, optional: true
9
+ has_many :delivery_order_items, dependent: :destroy
10
+ has_many :marketplace_orders, class_name: "Dscf::Marketplace::Order", dependent: :nullify
11
+
12
+ validates :driver_id, presence: true
13
+ validates :pickup_address_id, presence: true
14
+ validates :status, presence: true
15
+ validates :vehicle_type, presence: true
16
+ validates :estimated_delivery_price, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
17
+ validates :actual_delivery_price, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
18
+
19
+ before_save :calculate_actual_delivery_price, if: :status_changed?
20
+
21
+ def pickup!
22
+ return false unless assigned?
23
+ update!(status: :picked_up, actual_delivery_time: Time.current)
24
+ true
25
+ end
26
+
27
+ def start_delivery!
28
+ return false unless picked_up?
29
+ update!(status: :in_transit)
30
+ true
31
+ end
32
+
33
+ def complete_delivery!
34
+ return false unless in_transit?
35
+ update!(status: :delivered, actual_delivery_time: Time.current)
36
+ update_associated_orders
37
+ true
38
+ end
39
+
40
+ def mark_failed!(reason = nil)
41
+ update!(status: :failed, delivery_notes: reason)
42
+ true
43
+ end
44
+
45
+ def total_items
46
+ delivery_order_items.sum(:quantity)
47
+ end
48
+
49
+ def total_orders
50
+ marketplace_orders.count
51
+ end
52
+
53
+ def delivery_duration
54
+ return nil unless actual_delivery_time && estimated_delivery_time
55
+ (actual_delivery_time - estimated_delivery_time).to_i
56
+ end
57
+
58
+ def on_time?
59
+ return nil unless actual_delivery_time && estimated_delivery_time
60
+ actual_delivery_time <= estimated_delivery_time
61
+ end
62
+
63
+ # Handoff tracking methods
64
+ def handoff_completion_status
65
+ items = delivery_order_items
66
+ {
67
+ total_items: items.count,
68
+ confirmed_items: items.count(&:receiver_confirmed?),
69
+ pending_confirmation: items.count { |item| item.delivered? && !item.receiver_confirmed? },
70
+ issues_reported: items.count(&:requires_resolution?),
71
+ completion_percentage: items.present? ? (items.count(&:receiver_confirmed?).to_f / items.count * 100).round(2) : 0
72
+ }
73
+ end
74
+
75
+ def all_handoffs_confirmed?
76
+ delivery_order_items.all?(&:receiver_confirmed?)
77
+ end
78
+
79
+ def items_requiring_attention
80
+ delivery_order_items.select(&:requires_resolution?)
81
+ end
82
+
83
+ def generate_handoff_summary_report
84
+ {
85
+ delivery_order_id: id,
86
+ driver: driver&.name,
87
+ vehicle: vehicle_type,
88
+ total_destinations: delivery_order_items.distinct.pluck(:dropoff_address_id).count,
89
+ handoff_status: handoff_completion_status,
90
+ issues: items_requiring_attention.map(&:comprehensive_damage_report),
91
+ generated_at: Time.current
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def calculate_actual_delivery_price
98
+ self.actual_delivery_price ||= estimated_delivery_price
99
+ end
100
+
101
+ def update_associated_orders
102
+ marketplace_orders.each do |order|
103
+ order.update!(status: :completed)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,192 @@
1
+ module Dscf::Marketplace
2
+ class DeliveryOrderItem < ApplicationRecord
3
+ enum :status, {pending: 0, picked_up: 1, delivered: 2, receiver_confirmed: 3, damaged: 4, missing: 5, disputed: 6}
4
+
5
+ belongs_to :delivery_order
6
+ belongs_to :order_item, class_name: "Dscf::Marketplace::OrderItem"
7
+ belongs_to :pickup_address, class_name: "Dscf::Core::Address"
8
+ belongs_to :dropoff_address, class_name: "Dscf::Core::Address"
9
+
10
+ validates :delivery_order_id, presence: true
11
+ validates :order_item_id, presence: true
12
+ validates :quantity, presence: true, numericality: {greater_than: 0}
13
+ validates :status, presence: true
14
+ validates :pickup_verified_quantity, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
15
+ validates :dropoff_verified_quantity, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
16
+ validates :damaged_quantity, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
17
+ validates :missing_quantity, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
18
+ validates :receiver_confirmed_by, presence: true, if: :receiver_action_taken?
19
+ validates :receiver_confirmed_at, presence: true, if: :receiver_action_taken?
20
+
21
+ before_save :calculate_verification_totals
22
+
23
+ def pending?
24
+ status == "pending"
25
+ end
26
+
27
+ def picked_up?
28
+ status == "picked_up"
29
+ end
30
+
31
+ def delivered?
32
+ status == "delivered"
33
+ end
34
+
35
+ def damaged?
36
+ status == "damaged"
37
+ end
38
+
39
+ def missing?
40
+ status == "missing"
41
+ end
42
+
43
+ def receiver_confirmed?
44
+ status == "receiver_confirmed"
45
+ end
46
+
47
+ def disputed?
48
+ status == "disputed"
49
+ end
50
+
51
+ def pickup!
52
+ return false unless pending?
53
+ update!(status: :picked_up, pickup_verified_quantity: quantity)
54
+ true
55
+ end
56
+
57
+ def deliver!
58
+ return false unless picked_up?
59
+ update!(status: :delivered, dropoff_verified_quantity: pickup_verified_quantity.to_f - (damaged_quantity || 0) - (missing_quantity || 0))
60
+ true
61
+ end
62
+
63
+ # Handoff workflow methods
64
+ def receiver_confirm!(confirmed_quantity, notes = nil, confirmed_by = nil)
65
+ return false unless delivered? || picked_up?
66
+
67
+ update!(
68
+ status: :receiver_confirmed,
69
+ dropoff_verified_quantity: confirmed_quantity,
70
+ receiver_notes: notes,
71
+ receiver_confirmed_at: Time.current,
72
+ receiver_confirmed_by: confirmed_by,
73
+ handoff_checklist_completed: true
74
+ )
75
+ notify_receiver_confirmation
76
+ true
77
+ end
78
+
79
+ def report_issue!(issue_type, quantity, description, reported_by)
80
+ return false unless delivered? || picked_up?
81
+
82
+ update!(
83
+ status: issue_type.to_sym, # :damaged or :missing
84
+ damaged_quantity: ((issue_type == "damaged") ? quantity : 0),
85
+ missing_quantity: ((issue_type == "missing") ? quantity : 0),
86
+ receiver_notes: description,
87
+ receiver_confirmed_at: Time.current,
88
+ receiver_confirmed_by: reported_by,
89
+ handoff_checklist_completed: true
90
+ )
91
+ notify_platform_of_issue
92
+ true
93
+ end
94
+
95
+ def dispute_delivery!(reason, disputed_by)
96
+ return false unless delivered? || receiver_confirmed?
97
+
98
+ update!(
99
+ status: :disputed,
100
+ receiver_notes: reason,
101
+ receiver_confirmed_at: Time.current,
102
+ receiver_confirmed_by: disputed_by
103
+ )
104
+ create_dispute_record
105
+ true
106
+ end
107
+
108
+ def mark_damaged!(quantity)
109
+ update!(status: :damaged, damaged_quantity: quantity)
110
+ true
111
+ end
112
+
113
+ def mark_missing!(quantity)
114
+ update!(status: :missing, missing_quantity: quantity)
115
+ true
116
+ end
117
+
118
+ def verification_complete?
119
+ pickup_verified_quantity.present? && dropoff_verified_quantity.present?
120
+ end
121
+
122
+ def has_discrepancy?
123
+ return false unless verification_complete?
124
+ pickup_verified_quantity != dropoff_verified_quantity
125
+ end
126
+
127
+ def discrepancy_amount
128
+ return 0 unless has_discrepancy?
129
+ (pickup_verified_quantity - dropoff_verified_quantity).abs
130
+ end
131
+
132
+ def product_name
133
+ order_item&.product_name
134
+ end
135
+
136
+ def unit_name
137
+ order_item&.unit_name
138
+ end
139
+
140
+ # Loss/Damage Reporting Methods
141
+ def comprehensive_damage_report
142
+ {
143
+ item_id: id,
144
+ product_name: order_item&.product&.name,
145
+ expected_quantity: quantity,
146
+ received_quantity: dropoff_verified_quantity,
147
+ damaged_quantity: damaged_quantity,
148
+ missing_quantity: missing_quantity,
149
+ total_discrepancy: discrepancy_amount,
150
+ issue_description: receiver_notes,
151
+ reported_at: receiver_confirmed_at,
152
+ reported_by: receiver_confirmed_by,
153
+ requires_resolution: requires_resolution?
154
+ }
155
+ end
156
+
157
+ def requires_resolution?
158
+ has_discrepancy? || damaged? || missing? || disputed?
159
+ end
160
+
161
+ private
162
+
163
+ def receiver_action_taken?
164
+ receiver_confirmed? || damaged? || missing? || disputed?
165
+ end
166
+
167
+ def notify_receiver_confirmation
168
+ # Placeholder for notification logic
169
+ # This will be implemented when we add notification system
170
+ Rails.logger.info("Receiver confirmation completed for delivery order item #{id}")
171
+ end
172
+
173
+ def notify_platform_of_issue
174
+ # Placeholder for platform notification
175
+ # This will be implemented when we add notification system
176
+ Rails.logger.info("Issue reported for delivery order item #{id}: #{status}")
177
+ end
178
+
179
+ def create_dispute_record
180
+ # Placeholder for dispute record creation
181
+ # This will be implemented when we add dispute management
182
+ Rails.logger.info("Dispute created for delivery order item #{id}")
183
+ end
184
+
185
+ def calculate_verification_totals
186
+ if pickup_verified_quantity.present? && damaged_quantity.present? && missing_quantity.present?
187
+ expected_dropoff = pickup_verified_quantity - damaged_quantity - missing_quantity
188
+ self.dropoff_verified_quantity ||= expected_dropoff
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,61 @@
1
+ module Dscf
2
+ module Marketplace
3
+ class DeliveryVehicle < ApplicationRecord
4
+ enum :vehicle_type, {motorcycle: 0, car: 1, van: 2, truck: 3}
5
+
6
+ belongs_to :driver, class_name: "Dscf::Core::User"
7
+ has_many :delivery_orders, dependent: :nullify
8
+
9
+ validates :driver_id, presence: true
10
+ validates :plate_number, presence: true, uniqueness: true
11
+ validates :vehicle_type, presence: true
12
+ validates :year, numericality: {greater_than: 1900, less_than_or_equal_to: Time.current.year + 1}, allow_nil: true
13
+ validates :brand, length: {maximum: 50}, allow_blank: true
14
+ validates :model, length: {maximum: 50}, allow_blank: true
15
+ validates :color, length: {maximum: 30}, allow_blank: true
16
+
17
+ def motorcycle?
18
+ vehicle_type == "motorcycle"
19
+ end
20
+
21
+ def car?
22
+ vehicle_type == "car"
23
+ end
24
+
25
+ def van?
26
+ vehicle_type == "van"
27
+ end
28
+
29
+ def truck?
30
+ vehicle_type == "truck"
31
+ end
32
+
33
+ def display_name
34
+ brand_and_model = [ brand.presence, model.presence ].compact.join(" ")
35
+ brand_and_model = nil if brand_and_model.blank?
36
+ "#{brand_and_model} (#{plate_number})".strip
37
+ end
38
+
39
+ def active_deliveries
40
+ delivery_orders.where.not(status: [ :delivered, :failed ])
41
+ end
42
+
43
+ def completed_deliveries
44
+ delivery_orders.where(status: :delivered)
45
+ end
46
+
47
+ def failed_deliveries
48
+ delivery_orders.where(status: :failed)
49
+ end
50
+
51
+ def total_deliveries
52
+ delivery_orders.count
53
+ end
54
+
55
+ def success_rate
56
+ return 0 if total_deliveries.zero?
57
+ (completed_deliveries.count.to_f / total_deliveries * 100).round(2)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,131 @@
1
+ module Dscf::Marketplace
2
+ class Order < ApplicationRecord
3
+ enum :order_type, {rfq_based: 0, direct_listing: 1}
4
+ enum :status, {pending: 0, confirmed: 1, processing: 2, completed: 3, cancelled: 4}
5
+ enum :fulfillment_type, {self_pickup: 0, delivery: 1}
6
+
7
+ belongs_to :quotation, optional: true
8
+ belongs_to :listing, optional: true
9
+ belongs_to :user, class_name: "Dscf::Core::User"
10
+ belongs_to :delivery_order, optional: true
11
+ has_many :order_items, dependent: :destroy
12
+
13
+ validates :order_type, presence: true
14
+ validates :status, presence: true
15
+ validates :fulfillment_type, presence: true
16
+ validates :user, presence: true
17
+ validate :quotation_or_listing_present
18
+
19
+ before_save :calculate_total_amount
20
+
21
+ def self.create_from_quotation(quotation)
22
+ return nil unless quotation.accepted?
23
+
24
+ order = create!(
25
+ order_type: :rfq_based,
26
+ status: :pending,
27
+ quotation: quotation,
28
+ user: quotation.request_for_quotation.user,
29
+ total_amount: quotation.total_price
30
+ )
31
+
32
+ quotation.quotation_items.each do |item|
33
+ order.order_items.create!(
34
+ quotation_item: item,
35
+ product: item.product,
36
+ unit: item.unit,
37
+ quantity: item.quantity,
38
+ unit_price: item.unit_price,
39
+ status: :pending
40
+ )
41
+ end
42
+
43
+ order
44
+ end
45
+
46
+ def self.create_from_listing(listing, user, quantity)
47
+ return nil unless listing.status == "active" && quantity <= listing.quantity
48
+
49
+ order = create!(
50
+ order_type: :direct_listing,
51
+ status: :pending,
52
+ listing: listing,
53
+ user: user,
54
+ total_amount: listing.price * quantity
55
+ )
56
+
57
+ order.order_items.create!(
58
+ listing: listing,
59
+ product: listing.supplier_product.product,
60
+ unit: listing.supplier_product.product.unit,
61
+ quantity: quantity,
62
+ unit_price: listing.price,
63
+ status: :pending
64
+ )
65
+
66
+ order
67
+ end
68
+
69
+ def total_amount
70
+ # Return stored value if it exists and is greater than 0, otherwise calculate
71
+ stored_value = super
72
+ calculated_value = order_items.sum { |item| item.quantity * item.unit_price }
73
+
74
+ # Return stored value if it's set and valid, otherwise return calculated value
75
+ return stored_value if stored_value.present? && stored_value > 0
76
+ calculated_value
77
+ end
78
+
79
+ def supplier
80
+ if rfq_based?
81
+ quotation&.business
82
+ elsif direct_listing?
83
+ listing&.business
84
+ end
85
+ end
86
+
87
+ # Fulfillment type methods
88
+ def self_pickup?
89
+ fulfillment_type == "self_pickup"
90
+ end
91
+
92
+ def delivery?
93
+ fulfillment_type == "delivery"
94
+ end
95
+
96
+ def requires_delivery_order?
97
+ delivery? && !completed?
98
+ end
99
+
100
+ def can_be_completed?
101
+ if self_pickup?
102
+ # Self-pickup orders can be completed immediately after confirmation
103
+ confirmed? || processing?
104
+ else
105
+ # Delivery orders require delivery_order association
106
+ delivery_order.present? && delivery_order.delivered?
107
+ end
108
+ end
109
+
110
+ def confirm!
111
+ return false unless pending?
112
+
113
+ update!(status: :confirmed)
114
+ # Update order items without reloading to avoid association issues
115
+ order_items.update_all(status: OrderItem.statuses[:confirmed])
116
+ true
117
+ end
118
+
119
+ def calculate_total_amount
120
+ self.total_amount = order_items.sum { |item| item.quantity * item.unit_price }
121
+ end
122
+
123
+ private
124
+
125
+ def quotation_or_listing_present
126
+ unless quotation.present? || listing.present?
127
+ errors.add(:base, "Either quotation or listing must be present")
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,38 @@
1
+ module Dscf::Marketplace
2
+ class OrderItem < ApplicationRecord
3
+ enum :status, {pending: 0, confirmed: 1, processing: 2, fulfilled: 3, cancelled: 4}
4
+
5
+ belongs_to :order, optional: true
6
+ belongs_to :quotation_item, optional: true
7
+ belongs_to :listing, optional: true
8
+ belongs_to :product
9
+ belongs_to :unit
10
+
11
+ validates :quantity, presence: true, numericality: {greater_than: 0}
12
+ validates :unit_price, presence: true, numericality: {greater_than_or_equal_to: 0}
13
+ validates :status, presence: true
14
+ validates :product, presence: true
15
+ validates :unit, presence: true
16
+ validate :quotation_item_or_listing_present
17
+
18
+ def subtotal
19
+ quantity * unit_price
20
+ end
21
+
22
+ def product_name
23
+ product&.name
24
+ end
25
+
26
+ def unit_name
27
+ unit&.name
28
+ end
29
+
30
+ private
31
+
32
+ def quotation_item_or_listing_present
33
+ unless quotation_item.present? || listing.present?
34
+ errors.add(:base, "Either quotation_item or listing must be present")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -5,6 +5,7 @@ module Dscf
5
5
  belongs_to :business, class_name: "Dscf::Core::Business", optional: true if defined?(Dscf::Core)
6
6
 
7
7
  has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy
8
+ has_one :order, class_name: "Dscf::Marketplace::Order", dependent: :destroy
8
9
 
9
10
  validates :request_for_quotation_id, presence: true
10
11
  validates :business_id, presence: true
@@ -79,9 +80,19 @@ module Dscf
79
80
 
80
81
  update_columns(status: "accepted")
81
82
  request_for_quotation.update!(status: "selected", selected_quotation: self)
83
+
84
+ # Create order from accepted quotation
85
+ create_order_from_quotation
86
+
82
87
  true
83
88
  end
84
89
 
90
+ def create_order_from_quotation
91
+ return if order.present?
92
+
93
+ Order.create_from_quotation(self)
94
+ end
95
+
85
96
  def reject!
86
97
  return false unless sent?
87
98
 
@@ -113,6 +124,32 @@ module Dscf
113
124
  quotation_items.exists? && quotation_items.all? { |item| item.unit_price.present? }
114
125
  end
115
126
 
127
+ def create_order_from_quotation
128
+ return if order.present?
129
+
130
+ order = Dscf::Marketplace::Order.create!(
131
+ order_type: :rfq_based,
132
+ status: :pending,
133
+ quotation: self,
134
+ user: request_for_quotation.user,
135
+ total_amount: total_price
136
+ )
137
+
138
+ quotation_items.each do |item|
139
+ Dscf::Marketplace::OrderItem.create!(
140
+ order: order,
141
+ quotation_item: item,
142
+ product: item.product,
143
+ unit: item.unit,
144
+ quantity: item.quantity,
145
+ unit_price: item.unit_price,
146
+ status: :pending
147
+ )
148
+ end
149
+
150
+ order
151
+ end
152
+
116
153
  private
117
154
 
118
155
  def calculate_total_price
@@ -0,0 +1,19 @@
1
+ class CreateDscfMarketplaceOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_marketplace_orders do |t|
4
+ t.integer :order_type
5
+ t.integer :status
6
+ t.references :quotation, null: true, foreign_key: {to_table: :dscf_marketplace_quotations}
7
+ t.references :listing, null: true, foreign_key: {to_table: :dscf_marketplace_listings}
8
+ t.references :user, null: false, foreign_key: {to_table: :dscf_core_users}
9
+ t.decimal :total_amount
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :dscf_marketplace_orders, :user_id, name: "user_id_on_dm_orders_idx"
15
+ add_index :dscf_marketplace_orders, :quotation_id, name: "quotation_id_on_dm_orders_idx"
16
+ add_index :dscf_marketplace_orders, :listing_id, name: "listing_id_on_dm_orders_idx"
17
+ add_index :dscf_marketplace_orders, :status, name: "status_on_dm_orders_idx"
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ class CreateDscfMarketplaceOrderItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_marketplace_order_items do |t|
4
+ t.references :order, null: false, foreign_key: {to_table: :dscf_marketplace_orders}
5
+ t.references :quotation_item, null: true, foreign_key: {to_table: :dscf_marketplace_quotation_items}
6
+ t.references :listing, null: true, foreign_key: {to_table: :dscf_marketplace_listings}
7
+ t.references :product, null: false, foreign_key: {to_table: :dscf_marketplace_products}
8
+ t.references :unit, null: false, foreign_key: {to_table: :dscf_marketplace_units}
9
+ t.decimal :quantity, precision: 15, scale: 6
10
+ t.decimal :unit_price
11
+ t.integer :status
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :dscf_marketplace_order_items, :order_id, name: "order_id_on_dm_order_items_idx"
17
+ add_index :dscf_marketplace_order_items, :quotation_item_id, name: "quotation_item_id_on_dm_order_items_idx"
18
+ add_index :dscf_marketplace_order_items, :listing_id, name: "listing_id_on_dm_order_items_idx"
19
+ add_index :dscf_marketplace_order_items, :product_id, name: "product_id_on_dm_order_items_idx"
20
+ add_index :dscf_marketplace_order_items, :unit_id, name: "unit_id_on_dm_order_items_idx"
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ class CreateDscfMarketplaceDeliveryOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_marketplace_delivery_orders do |t|
4
+ t.bigint :driver_id, null: false
5
+ t.bigint :pickup_address_id, null: false
6
+ t.integer :status
7
+ t.text :delivery_notes
8
+ t.datetime :estimated_delivery_time
9
+ t.datetime :actual_delivery_time
10
+ t.decimal :estimated_delivery_price
11
+ t.decimal :actual_delivery_price
12
+ t.json :optimized_route
13
+ t.integer :vehicle_type
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_foreign_key :dscf_marketplace_delivery_orders, :dscf_core_users, column: :driver_id
19
+ add_foreign_key :dscf_marketplace_delivery_orders, :dscf_core_addresses, column: :pickup_address_id
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ class CreateDscfMarketplaceDeliveryOrderItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_marketplace_delivery_order_items do |t|
4
+ t.bigint :delivery_order_id, null: false
5
+ t.bigint :order_item_id, null: false
6
+ t.bigint :pickup_address_id, null: false
7
+ t.bigint :dropoff_address_id, null: false
8
+ t.decimal :quantity
9
+ t.integer :status
10
+ t.text :notes
11
+ t.decimal :pickup_verified_quantity
12
+ t.decimal :dropoff_verified_quantity
13
+ t.decimal :damaged_quantity
14
+ t.decimal :missing_quantity
15
+ t.text :pickup_notes
16
+ t.text :dropoff_notes
17
+ t.integer :position
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_foreign_key :dscf_marketplace_delivery_order_items, :dscf_marketplace_delivery_orders, column: :delivery_order_id
23
+ add_foreign_key :dscf_marketplace_delivery_order_items, :dscf_marketplace_order_items, column: :order_item_id
24
+ add_foreign_key :dscf_marketplace_delivery_order_items, :dscf_core_addresses, column: :pickup_address_id
25
+ add_foreign_key :dscf_marketplace_delivery_order_items, :dscf_core_addresses, column: :dropoff_address_id
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ class CreateDscfMarketplaceDeliveryVehicles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_marketplace_delivery_vehicles do |t|
4
+ t.bigint :driver_id, null: false
5
+ t.string :plate_number
6
+ t.integer :vehicle_type
7
+ t.string :brand
8
+ t.string :model
9
+ t.integer :year
10
+ t.string :color
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_foreign_key :dscf_marketplace_delivery_vehicles, :dscf_core_users, column: :driver_id
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ class AddDeliveryHandoffFieldsToDeliveryOrderItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Add new handoff fields
4
+ add_column :dscf_marketplace_delivery_order_items, :receiver_confirmed_at, :datetime
5
+ add_column :dscf_marketplace_delivery_order_items, :receiver_confirmed_by, :integer
6
+ add_column :dscf_marketplace_delivery_order_items, :receiver_notes, :text
7
+ add_column :dscf_marketplace_delivery_order_items, :handoff_checklist_completed, :boolean, default: false
8
+
9
+ # Add indexes for performance
10
+ add_index :dscf_marketplace_delivery_order_items, :receiver_confirmed_by
11
+ add_index :dscf_marketplace_delivery_order_items, :handoff_checklist_completed
12
+
13
+ # Note: Status enum migration handled separately to avoid data loss
14
+ # The status values will be updated in the model after this migration
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ class AddDeliveryOrderToOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_marketplace_orders, :delivery_order_id, :bigint
4
+ add_foreign_key :dscf_marketplace_orders, :dscf_marketplace_delivery_orders, column: :delivery_order_id
5
+ add_index :dscf_marketplace_orders, :delivery_order_id
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class AddDeliveryVehicleToDeliveryOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_marketplace_delivery_orders, :delivery_vehicle_id, :bigint
4
+ add_foreign_key :dscf_marketplace_delivery_orders, :dscf_marketplace_delivery_vehicles, column: :delivery_vehicle_id
5
+ add_index :dscf_marketplace_delivery_orders, :delivery_vehicle_id
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class AddFulfillmentTypeToOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_marketplace_orders, :fulfillment_type, :integer, default: 0
4
+ add_index :dscf_marketplace_orders, :fulfillment_type
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.1.6".freeze
3
+ VERSION = "0.1.8".freeze
4
4
  end
5
5
  end
@@ -0,0 +1,15 @@
1
+ FactoryBot.define do
2
+ factory :dscf_core_address, class: "Dscf::Core::Address" do
3
+ user
4
+ address_type { 0 } # home
5
+ country { "Ethiopia" }
6
+ city { "Addis Ababa" }
7
+ sub_city { "Bole" }
8
+ woreda { "03" }
9
+ kebele { "07" }
10
+ house_numbers { "123" }
11
+ po_box { "1234" }
12
+ latitude { 9.1450 }
13
+ longitude { 38.7525 }
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_delivery_order_item, class: "Dscf::Marketplace::DeliveryOrderItem" do
3
+ association :delivery_order, factory: :dscf_marketplace_delivery_order
4
+ association :order_item, factory: :dscf_marketplace_order_item
5
+ pickup_address { create(:dscf_core_address) }
6
+ dropoff_address { create(:dscf_core_address) }
7
+ quantity { 10.0 }
8
+ status { :pending }
9
+ notes { "Test delivery item" }
10
+ pickup_verified_quantity { nil }
11
+ dropoff_verified_quantity { nil }
12
+ damaged_quantity { nil }
13
+ missing_quantity { nil }
14
+ pickup_notes { nil }
15
+ dropoff_notes { nil }
16
+ position { 1 }
17
+
18
+ trait :picked_up do
19
+ status { :picked_up }
20
+ pickup_verified_quantity { 10.0 }
21
+ end
22
+
23
+ trait :delivered do
24
+ status { :delivered }
25
+ pickup_verified_quantity { 10.0 }
26
+ dropoff_verified_quantity { 10.0 }
27
+ end
28
+
29
+ trait :receiver_confirmed do
30
+ status { :receiver_confirmed }
31
+ pickup_verified_quantity { 10.0 }
32
+ dropoff_verified_quantity { 10.0 }
33
+ receiver_confirmed_at { Time.current }
34
+ receiver_confirmed_by { create(:dscf_core_user).id }
35
+ handoff_checklist_completed { true }
36
+ end
37
+
38
+ trait :with_damage do
39
+ status { :damaged }
40
+ pickup_verified_quantity { 10.0 }
41
+ dropoff_verified_quantity { 8.0 }
42
+ damaged_quantity { 2.0 }
43
+ receiver_notes { "Package damaged during transport" }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_delivery_order, class: "Dscf::Marketplace::DeliveryOrder" do
3
+ association :driver, factory: :dscf_core_user
4
+ association :pickup_address, factory: :dscf_core_address
5
+ status { :assigned }
6
+ delivery_notes { "Test delivery order" }
7
+ estimated_delivery_time { 2.hours.from_now }
8
+ actual_delivery_time { nil }
9
+ estimated_delivery_price { 50.0 }
10
+ actual_delivery_price { nil }
11
+ optimized_route { {} }
12
+ vehicle_type { :motorcycle }
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_delivery_vehicle, class: "Dscf::Marketplace::DeliveryVehicle" do
3
+ association :driver, factory: :dscf_core_user
4
+ sequence(:plate_number) { |n| "ABC#{n.to_s.rjust(3, "0")}" }
5
+ vehicle_type { :motorcycle }
6
+ brand { "Honda" }
7
+ model { "CBR" }
8
+ year { 2020 }
9
+ color { "Red" }
10
+
11
+ trait :car do
12
+ vehicle_type { :car }
13
+ brand { "Toyota" }
14
+ model { "Corolla" }
15
+ end
16
+
17
+ trait :van do
18
+ vehicle_type { :van }
19
+ brand { "Ford" }
20
+ model { "Transit" }
21
+ end
22
+
23
+ trait :truck do
24
+ vehicle_type { :truck }
25
+ brand { "Volvo" }
26
+ model { "FH16" }
27
+ end
28
+
29
+ trait :old_vehicle do
30
+ year { 2010 }
31
+ end
32
+
33
+ trait :future_vehicle do
34
+ year { Time.current.year + 2 }
35
+ end
36
+ end
37
+ end
@@ -1,10 +1,22 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_listing, class: 'Dscf::Marketplace::Listing' do
3
- business { create(:dscf_core_business) }
4
- supplier_product { create(:dscf_marketplace_supplier_product) }
2
+ factory :dscf_marketplace_listing, class: "Dscf::Marketplace::Listing" do
3
+ association :business, factory: :dscf_core_business
4
+ association :supplier_product, factory: :dscf_marketplace_supplier_product
5
5
  price { 500.00 }
6
6
  quantity { 100.0 }
7
7
  description { "High quality product listing" }
8
- status { :draft }
8
+ # status defaults to :draft from model
9
+
10
+ trait :active do
11
+ status { :active }
12
+ end
13
+
14
+ trait :draft do
15
+ status { :draft }
16
+ end
17
+
18
+ trait :paused do
19
+ status { :paused }
20
+ end
9
21
  end
10
22
  end
@@ -0,0 +1,49 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_order_item, class: "Dscf::Marketplace::OrderItem" do
3
+ association :order, factory: :dscf_marketplace_order
4
+ quantity { 5 }
5
+ unit_price { 10.0 }
6
+ status { :pending }
7
+ quotation_item { nil } # Don't create by default
8
+ listing { nil } # Don't create by default
9
+
10
+ # Set product and unit based on quotation_item or listing
11
+ after(:build) do |item|
12
+ if item.quotation_item
13
+ item.product ||= item.quotation_item.product
14
+ item.unit ||= item.quotation_item.unit
15
+ elsif item.listing
16
+ item.product ||= item.listing.supplier_product.product
17
+ item.unit ||= item.listing.supplier_product.product.unit
18
+ else
19
+ item.product ||= create(:dscf_marketplace_product)
20
+ item.unit ||= create(:dscf_marketplace_unit)
21
+ end
22
+ end
23
+
24
+ trait :from_quotation do
25
+ association :quotation_item, factory: :dscf_marketplace_quotation_item
26
+ end
27
+
28
+ trait :from_listing do
29
+ association :listing, factory: :dscf_marketplace_listing
30
+ quotation_item { nil }
31
+ end
32
+
33
+ trait :confirmed do
34
+ status { :confirmed }
35
+ end
36
+
37
+ trait :processing do
38
+ status { :processing }
39
+ end
40
+
41
+ trait :fulfilled do
42
+ status { :fulfilled }
43
+ end
44
+
45
+ trait :cancelled do
46
+ status { :cancelled }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_order, class: "Dscf::Marketplace::Order" do
3
+ order_type { :rfq_based }
4
+ status { :pending }
5
+ fulfillment_type { :self_pickup }
6
+ association :user, factory: :dscf_core_user
7
+ association :quotation, factory: :dscf_marketplace_quotation
8
+ listing { nil }
9
+
10
+ trait :rfq_based do
11
+ order_type { :rfq_based }
12
+ association :quotation, factory: :dscf_marketplace_quotation
13
+ end
14
+
15
+ trait :direct_listing do
16
+ order_type { :direct_listing }
17
+ association :listing, factory: :dscf_marketplace_listing
18
+ quotation { nil }
19
+ end
20
+
21
+ trait :self_pickup do
22
+ fulfillment_type { :self_pickup }
23
+ end
24
+
25
+ trait :delivery do
26
+ fulfillment_type { :delivery }
27
+ end
28
+
29
+ trait :confirmed do
30
+ status { :confirmed }
31
+ end
32
+
33
+ trait :processing do
34
+ status { :processing }
35
+ end
36
+
37
+ trait :completed do
38
+ status { :completed }
39
+ end
40
+
41
+ trait :cancelled do
42
+ status { :cancelled }
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_quotation_item, class: 'Dscf::Marketplace::QuotationItem' do
2
+ factory :dscf_marketplace_quotation_item, class: "Dscf::Marketplace::QuotationItem" do
3
3
  association :quotation, factory: :dscf_marketplace_quotation
4
4
  association :rfq_item, factory: :dscf_marketplace_rfq_item
5
5
  product { rfq_item.product }
@@ -1,11 +1,11 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_quotation, class: 'Dscf::Marketplace::Quotation' do
2
+ factory :dscf_marketplace_quotation, class: "Dscf::Marketplace::Quotation" do
3
3
  association :request_for_quotation, factory: :dscf_marketplace_request_for_quotation
4
4
  association :business, factory: :dscf_core_business
5
5
  total_price { nil }
6
6
  delivery_date { 5.days.from_now.to_date }
7
7
  valid_until { 15.days.from_now }
8
- status { "draft" }
8
+ status { :draft }
9
9
  notes { "Sample quotation notes" }
10
10
 
11
11
  trait :expired do
@@ -15,11 +15,11 @@ FactoryBot.define do
15
15
  end
16
16
 
17
17
  trait :sent do
18
- status { "sent" }
18
+ status { :sent }
19
19
  end
20
20
 
21
21
  trait :accepted do
22
- status { "accepted" }
22
+ status { :accepted }
23
23
  end
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-marketplace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-28 00:00:00.000000000 Z
10
+ date: 2025-09-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -415,14 +415,18 @@ extensions: []
415
415
  extra_rdoc_files: []
416
416
  files:
417
417
  - MIT-LICENSE
418
- - README.md
419
418
  - Rakefile
420
419
  - app/controllers/dscf/marketplace/application_controller.rb
421
420
  - app/jobs/dscf/marketplace/application_job.rb
422
421
  - app/mailers/dscf/marketplace/application_mailer.rb
423
422
  - app/models/dscf/marketplace/application_record.rb
424
423
  - app/models/dscf/marketplace/category.rb
424
+ - app/models/dscf/marketplace/delivery_order.rb
425
+ - app/models/dscf/marketplace/delivery_order_item.rb
426
+ - app/models/dscf/marketplace/delivery_vehicle.rb
425
427
  - app/models/dscf/marketplace/listing.rb
428
+ - app/models/dscf/marketplace/order.rb
429
+ - app/models/dscf/marketplace/order_item.rb
426
430
  - app/models/dscf/marketplace/product.rb
427
431
  - app/models/dscf/marketplace/quotation.rb
428
432
  - app/models/dscf/marketplace/quotation_item.rb
@@ -445,20 +449,35 @@ files:
445
449
  - db/migrate/20250828090300_create_dscf_marketplace_quotation_items.rb
446
450
  - db/migrate/20250828090400_change_rfq_status_to_integer.rb
447
451
  - db/migrate/20250828090500_change_quotation_status_to_integer.rb
452
+ - db/migrate/20250828115509_create_dscf_marketplace_orders.rb
453
+ - db/migrate/20250828115525_create_dscf_marketplace_order_items.rb
454
+ - db/migrate/20250831110907_create_dscf_marketplace_delivery_orders.rb
455
+ - db/migrate/20250831110921_create_dscf_marketplace_delivery_order_items.rb
456
+ - db/migrate/20250831110933_create_dscf_marketplace_delivery_vehicles.rb
457
+ - db/migrate/20250901074509_add_delivery_handoff_fields_to_delivery_order_items.rb
458
+ - db/migrate/20250901074746_add_delivery_order_to_orders.rb
459
+ - db/migrate/20250901075511_add_delivery_vehicle_to_delivery_orders.rb
460
+ - db/migrate/20250901080134_add_fulfillment_type_to_orders.rb
448
461
  - lib/dscf/marketplace.rb
449
462
  - lib/dscf/marketplace/engine.rb
450
463
  - lib/dscf/marketplace/version.rb
451
464
  - lib/tasks/dscf/marketplace_tasks.rake
465
+ - spec/factories/dscf/core/addresses.rb
452
466
  - spec/factories/dscf/core/business_types.rb
453
467
  - spec/factories/dscf/core/businesses.rb
454
468
  - spec/factories/dscf/core/users.rb
455
469
  - spec/factories/dscf/marketplace/categories.rb
456
- - spec/factories/dscf/marketplace/dscf/marketplace/quotation_items.rb
457
- - spec/factories/dscf/marketplace/dscf/marketplace/quotations.rb
458
- - spec/factories/dscf/marketplace/dscf/marketplace/request_for_quotations.rb
459
- - spec/factories/dscf/marketplace/dscf/marketplace/rfq_items.rb
470
+ - spec/factories/dscf/marketplace/delivery_order_items.rb
471
+ - spec/factories/dscf/marketplace/delivery_orders.rb
472
+ - spec/factories/dscf/marketplace/delivery_vehicles.rb
460
473
  - spec/factories/dscf/marketplace/listings.rb
474
+ - spec/factories/dscf/marketplace/order_items.rb
475
+ - spec/factories/dscf/marketplace/orders.rb
461
476
  - spec/factories/dscf/marketplace/products.rb
477
+ - spec/factories/dscf/marketplace/quotation_items.rb
478
+ - spec/factories/dscf/marketplace/quotations.rb
479
+ - spec/factories/dscf/marketplace/request_for_quotations.rb
480
+ - spec/factories/dscf/marketplace/rfq_items.rb
462
481
  - spec/factories/dscf/marketplace/supplier_products.rb
463
482
  - spec/factories/dscf/marketplace/unit_conversions.rb
464
483
  - spec/factories/dscf/marketplace/units.rb
data/README.md DELETED
@@ -1,28 +0,0 @@
1
- # Dscf::Marketplace
2
- Short description and motivation.
3
-
4
- ## Usage
5
- How to use my plugin.
6
-
7
- ## Installation
8
- Add this line to your application's Gemfile:
9
-
10
- ```ruby
11
- gem "dscf-marketplace"
12
- ```
13
-
14
- And then execute:
15
- ```bash
16
- $ bundle
17
- ```
18
-
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install dscf-marketplace
22
- ```
23
-
24
- ## Contributing
25
- Contribution directions go here.
26
-
27
- ## License
28
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).