dscf-marketplace 0.8.9 → 0.9.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/orders_controller.rb +117 -1
- data/app/models/dscf/marketplace/order.rb +54 -5
- data/app/models/dscf/marketplace/order_item.rb +9 -2
- data/app/policies/dscf/marketplace/order_policy.rb +20 -0
- data/app/serializers/dscf/marketplace/order_item_serializer.rb +19 -1
- data/app/serializers/dscf/marketplace/order_serializer.rb +10 -1
- data/app/services/dscf/marketplace/order_splitting_service.rb +49 -0
- data/app/services/dscf/marketplace/order_validation_service.rb +68 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20260622000001_add_validation_and_source_fields_to_dscf_marketplace_order_items.rb +13 -0
- data/db/seeds.rb +1 -1
- data/lib/dscf/marketplace/engine.rb +1 -1
- data/lib/dscf/marketplace/version.rb +1 -1
- data/spec/factories/dscf/marketplace/order_items.rb +6 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28fd6ca06416aafd1e185ec3c143cbca9ad794a793e93b809be54bbee9779cae
|
|
4
|
+
data.tar.gz: 01fdbf80d424f8d053be1b3bba44780758d52efb21e8ad7a6bf0a4bd761f2e68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2935f0370099d113ee36f2f2722357ad66115557043957a9a6152e94a20080d0c16044e57f3d101819d3ab231d7911317589598db6a947ba90e19ef40e5b0aa1
|
|
7
|
+
data.tar.gz: 3e7d3ce883999d57799693ecfc4ec9a52559f198de99ed512b9b80ded38d4a001afdd3e90f986d582ca34ebc9df34ff7580e4968ab04e6da3ba4943fac5ac184
|
|
@@ -104,6 +104,122 @@ module Dscf
|
|
|
104
104
|
render_success("orders.success.index", data: orders, serializer_options: options)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
def validate
|
|
108
|
+
obj = find_record
|
|
109
|
+
authorize obj, :validate?
|
|
110
|
+
|
|
111
|
+
Dscf::Marketplace::OrderValidationService.validate(obj)
|
|
112
|
+
create_notification_for_order(obj, :validation_completed)
|
|
113
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_item
|
|
117
|
+
obj = find_record
|
|
118
|
+
authorize obj, :resolve_item?
|
|
119
|
+
|
|
120
|
+
item = obj.order_items.find(params[:order_item_id])
|
|
121
|
+
# Example: for price_changed, accept optional resolved_unit_price from params
|
|
122
|
+
if params[:resolved_unit_price].present?
|
|
123
|
+
item.resolved_unit_price = params[:resolved_unit_price]
|
|
124
|
+
item.validation_status = :validated
|
|
125
|
+
end
|
|
126
|
+
if params[:resolved_quantity].present?
|
|
127
|
+
item.resolved_quantity = params[:resolved_quantity]
|
|
128
|
+
item.validation_status = :validated
|
|
129
|
+
end
|
|
130
|
+
if params[:action_type] == "remove"
|
|
131
|
+
item.validation_status = :validated
|
|
132
|
+
item.quantity = 0 # will be cleaned or marked
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
item.save!
|
|
136
|
+
create_notification_for_order(obj, :item_resolved)
|
|
137
|
+
render_success(data: item)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def assign_source
|
|
141
|
+
obj = find_record
|
|
142
|
+
authorize obj, :split?
|
|
143
|
+
|
|
144
|
+
item = obj.order_items.find(params[:order_item_id])
|
|
145
|
+
Dscf::Marketplace::OrderSplittingService.assign_source(item, source_type: params[:source_type], source_id: params[:source_id])
|
|
146
|
+
render_success(data: item)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def split
|
|
150
|
+
obj = find_record
|
|
151
|
+
authorize obj, :split?
|
|
152
|
+
|
|
153
|
+
return render_error(errors: "All items must be validated before splitting") unless obj.all_items_validated?
|
|
154
|
+
|
|
155
|
+
Dscf::Marketplace::OrderSplittingService.perform_split(obj)
|
|
156
|
+
create_notification_for_order(obj, :split_ready)
|
|
157
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def supplier_confirm
|
|
161
|
+
obj = find_record
|
|
162
|
+
authorize obj, :supplier_confirm?
|
|
163
|
+
|
|
164
|
+
confirmed = params[:confirmed]
|
|
165
|
+
reason = params[:reason]
|
|
166
|
+
Dscf::Marketplace::OrderSplittingService.supplier_confirm(obj, confirmed: confirmed != false, reason: reason)
|
|
167
|
+
|
|
168
|
+
create_notification_for_order(obj, confirmed ? :supplier_confirmed : :supplier_rejected, reason: reason)
|
|
169
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def retailer_confirm
|
|
173
|
+
obj = find_record
|
|
174
|
+
authorize obj, :confirm? # reuse existing or add specific
|
|
175
|
+
|
|
176
|
+
return render_error(errors: "Order not ready for retailer confirmation") unless obj.retailer_can_confirm?
|
|
177
|
+
|
|
178
|
+
if params[:confirmed] == false
|
|
179
|
+
reason = params[:reason]
|
|
180
|
+
obj.update!(status: :cancelled)
|
|
181
|
+
obj.order_items.update_all(status: OrderItem.statuses[:cancelled])
|
|
182
|
+
create_notification_for_order(obj, :retailer_rejected, reason: reason)
|
|
183
|
+
else
|
|
184
|
+
obj.update!(status: :confirmed)
|
|
185
|
+
obj.order_items.update_all(status: OrderItem.statuses[:confirmed])
|
|
186
|
+
create_notification_for_order(obj, :retailer_confirmed)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def create_notification_for_order(order, action, reason: nil)
|
|
194
|
+
recipient = order.ordered_by || order.user
|
|
195
|
+
return unless recipient
|
|
196
|
+
|
|
197
|
+
title = case action
|
|
198
|
+
when :validation_completed then "Order ##{order.id} validation complete"
|
|
199
|
+
when :item_resolved then "Item resolved in order ##{order.id}"
|
|
200
|
+
when :split_ready then "Order ##{order.id} ready for supplier confirmation"
|
|
201
|
+
when :supplier_confirmed then "Supplier confirmed order ##{order.id}"
|
|
202
|
+
when :supplier_rejected then "Supplier rejected item in order ##{order.id}"
|
|
203
|
+
when :retailer_confirmed then "Retailer confirmed order ##{order.id}"
|
|
204
|
+
when :retailer_rejected then "Retailer cancelled order ##{order.id}"
|
|
205
|
+
else "Order ##{order.id} update"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
body = case action
|
|
209
|
+
when :supplier_rejected, :retailer_rejected then "Reason: #{reason}" if reason
|
|
210
|
+
else "Status: #{order.status}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
notification = Dscf::Core::Notification.create!(
|
|
214
|
+
notifiable: order,
|
|
215
|
+
recipient: recipient,
|
|
216
|
+
notification_type: :general,
|
|
217
|
+
title: title,
|
|
218
|
+
body: body.to_s
|
|
219
|
+
)
|
|
220
|
+
Dscf::Core::NotificationService.deliver(notification)
|
|
221
|
+
end
|
|
222
|
+
|
|
107
223
|
private
|
|
108
224
|
|
|
109
225
|
def create_direct_listing_order
|
|
@@ -183,7 +299,7 @@ module Dscf
|
|
|
183
299
|
|
|
184
300
|
def default_serializer_includes
|
|
185
301
|
{
|
|
186
|
-
index: [ :user, :ordered_by, :ordered_to, :quotation, order_items
|
|
302
|
+
index: [ :user, :ordered_by, :ordered_to, :quotation, :order_items ],
|
|
187
303
|
show: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :delivery_order, :order_items ],
|
|
188
304
|
create: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :order_items ],
|
|
189
305
|
update: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :order_items ]
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
module Dscf::Marketplace
|
|
2
2
|
class Order < ApplicationRecord
|
|
3
3
|
enum :order_type, {rfq_based: 0, direct_listing: 1}
|
|
4
|
-
|
|
4
|
+
# Workflow statuses aligned with requirements doc for Sprint 2 order management
|
|
5
|
+
enum :status, {
|
|
6
|
+
pending: 0, # initial / validating
|
|
7
|
+
validating: 1,
|
|
8
|
+
splitting: 2,
|
|
9
|
+
waiting_supplier_confirmation: 3,
|
|
10
|
+
waiting_retailer_confirmation: 4,
|
|
11
|
+
confirmed: 5,
|
|
12
|
+
processing: 6, # legacy / fulfillment
|
|
13
|
+
completed: 7,
|
|
14
|
+
cancelled: 8
|
|
15
|
+
}
|
|
5
16
|
enum :fulfillment_type, {self_pickup: 0, delivery: 1}
|
|
6
17
|
enum :payment_method, {cash: 0, credit: 1, bank_transfer: 2}, default: :cash
|
|
7
18
|
|
|
@@ -46,7 +57,7 @@ module Dscf::Marketplace
|
|
|
46
57
|
|
|
47
58
|
attributes = {
|
|
48
59
|
order_type: :rfq_based,
|
|
49
|
-
status: :
|
|
60
|
+
status: :validating,
|
|
50
61
|
fulfillment_type: dropoff_address.present? ? :delivery : :self_pickup,
|
|
51
62
|
quotation: quotation,
|
|
52
63
|
user: quotation.request_for_quotation.user, # Keep for backward compatibility
|
|
@@ -66,19 +77,21 @@ module Dscf::Marketplace
|
|
|
66
77
|
unit: item.unit,
|
|
67
78
|
quantity: item.quantity,
|
|
68
79
|
unit_price: item.unit_price,
|
|
69
|
-
status: :pending
|
|
80
|
+
status: :pending,
|
|
81
|
+
validation_status: :validated # initial from accepted quote; validation can re-run
|
|
70
82
|
)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
order
|
|
74
86
|
end
|
|
75
87
|
|
|
88
|
+
|
|
76
89
|
def self.create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil)
|
|
77
90
|
return nil unless listing.visible? && quantity <= listing.quantity
|
|
78
91
|
|
|
79
92
|
attributes = {
|
|
80
93
|
order_type: :direct_listing,
|
|
81
|
-
status: :
|
|
94
|
+
status: :validating,
|
|
82
95
|
fulfillment_type: dropoff_address.present? ? :delivery : :self_pickup,
|
|
83
96
|
listing: listing,
|
|
84
97
|
user: user, # Keep for backward compatibility
|
|
@@ -97,12 +110,14 @@ module Dscf::Marketplace
|
|
|
97
110
|
unit: listing.supplier_product.product.unit,
|
|
98
111
|
quantity: quantity,
|
|
99
112
|
unit_price: listing.price,
|
|
100
|
-
status: :pending
|
|
113
|
+
status: :pending,
|
|
114
|
+
validation_status: :validated
|
|
101
115
|
)
|
|
102
116
|
|
|
103
117
|
order
|
|
104
118
|
end
|
|
105
119
|
|
|
120
|
+
|
|
106
121
|
def total_amount
|
|
107
122
|
# Return stored value if it exists and is greater than 0, otherwise calculate
|
|
108
123
|
stored_value = super
|
|
@@ -157,6 +172,40 @@ module Dscf::Marketplace
|
|
|
157
172
|
self.total_amount = order_items.sum { |item| item.quantity * item.unit_price }
|
|
158
173
|
end
|
|
159
174
|
|
|
175
|
+
# Workflow helpers for Sprint 2 order validation + splitting
|
|
176
|
+
def validating?
|
|
177
|
+
pending? || status.to_s == "processing"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def all_items_validated?
|
|
181
|
+
order_items.all? { |i| i.validation_status == "validated" }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validation_summary
|
|
185
|
+
order_items.group_by(&:validation_status).transform_values(&:count)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def mark_splitting!
|
|
189
|
+
update!(status: :splitting)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def mark_waiting_supplier_confirmation!
|
|
193
|
+
update!(status: :waiting_supplier_confirmation)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def mark_waiting_retailer_confirmation!
|
|
197
|
+
update!(status: :waiting_retailer_confirmation)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def retailer_can_confirm?
|
|
201
|
+
waiting_retailer_confirmation? || confirmed? # allow re-confirm safety
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def supplier_confirmation_complete?
|
|
205
|
+
# simplistic: all items have source and are confirmed or cancelled
|
|
206
|
+
order_items.all? { |i| i.source_id.present? && (i.status.to_s == "confirmed" || i.status.to_s == "cancelled") }
|
|
207
|
+
end
|
|
208
|
+
|
|
160
209
|
private
|
|
161
210
|
|
|
162
211
|
def quotation_or_listing_present
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
module Dscf::Marketplace
|
|
2
2
|
class OrderItem < ApplicationRecord
|
|
3
3
|
enum :status, {pending: 0, confirmed: 1, processing: 2, fulfilled: 3, cancelled: 4}
|
|
4
|
+
enum :validation_status, {
|
|
5
|
+
validated: 0,
|
|
6
|
+
no_longer_listed: 1,
|
|
7
|
+
price_changed: 2,
|
|
8
|
+
low_quantity: 3
|
|
9
|
+
}, default: :validated
|
|
4
10
|
|
|
5
11
|
delegate :name, :description, :thumbnail_url, :images_urls, to: :product, allow_nil: true
|
|
6
12
|
|
|
@@ -9,6 +15,7 @@ module Dscf::Marketplace
|
|
|
9
15
|
belongs_to :listing, optional: true
|
|
10
16
|
belongs_to :product
|
|
11
17
|
belongs_to :unit
|
|
18
|
+
belongs_to :source, polymorphic: true, optional: true
|
|
12
19
|
|
|
13
20
|
validates :quantity, presence: true, numericality: {greater_than: 0}
|
|
14
21
|
validates :unit_price, presence: true, numericality: {greater_than_or_equal_to: 0}
|
|
@@ -19,11 +26,11 @@ module Dscf::Marketplace
|
|
|
19
26
|
|
|
20
27
|
# Ransack configuration for secure filtering
|
|
21
28
|
def self.ransackable_attributes(_auth_object = nil)
|
|
22
|
-
%w[id order_id quotation_item_id listing_id product_id unit_id quantity unit_price status created_at updated_at]
|
|
29
|
+
%w[id order_id quotation_item_id listing_id product_id unit_id quantity unit_price status validation_status validation_note resolved_unit_price resolved_quantity source_type source_id created_at updated_at]
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def self.ransackable_associations(_auth_object = nil)
|
|
26
|
-
%w[order quotation_item listing product unit]
|
|
33
|
+
%w[order quotation_item listing product unit source]
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def subtotal
|
|
@@ -20,6 +20,26 @@ module Dscf
|
|
|
20
20
|
def my_orders?
|
|
21
21
|
user.has_permission?("orders.my_orders")
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
def validate?
|
|
25
|
+
user.has_permission?("orders.validate")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def split?
|
|
29
|
+
user.has_permission?("orders.split")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve_item?
|
|
33
|
+
user.has_permission?("orders.resolve_item")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supplier_confirm?
|
|
37
|
+
user.has_permission?("orders.supplier_confirm")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def retailer_confirm?
|
|
41
|
+
user.has_permission?("orders.confirm") # or dedicated permission
|
|
42
|
+
end
|
|
23
43
|
end
|
|
24
44
|
end
|
|
25
45
|
end
|
|
@@ -3,7 +3,8 @@ module Dscf
|
|
|
3
3
|
class OrderItemSerializer < ActiveModel::Serializer
|
|
4
4
|
attributes :id, :order_id, :quotation_item_id, :listing_id,
|
|
5
5
|
:product_id, :unit_id, :quantity, :unit_price,
|
|
6
|
-
:status, :
|
|
6
|
+
:status, :validation_status, :validation_note, :resolved_unit_price, :resolved_quantity,
|
|
7
|
+
:source_type, :source_id, :source_name, :created_at, :updated_at,
|
|
7
8
|
:subtotal, :product_name, :unit_name, :thumbnail_url, :images_urls
|
|
8
9
|
|
|
9
10
|
belongs_to :order
|
|
@@ -11,6 +12,23 @@ module Dscf
|
|
|
11
12
|
belongs_to :listing
|
|
12
13
|
belongs_to :product
|
|
13
14
|
belongs_to :unit
|
|
15
|
+
|
|
16
|
+
def source_name
|
|
17
|
+
return nil unless object.source
|
|
18
|
+
case object.source_type
|
|
19
|
+
when 'Dscf::Marketplace::SubSupplier'
|
|
20
|
+
object.source.try(:business_name) || object.source.try(:name)
|
|
21
|
+
when 'Dscf::Marketplace::Supplier'
|
|
22
|
+
object.source.try(:name)
|
|
23
|
+
when 'Dscf::Core::Business'
|
|
24
|
+
object.source.try(:name)
|
|
25
|
+
else
|
|
26
|
+
object.source.try(:name) || object.source.try(:business_name)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Note: source association is polymorphic; use source_name + source_* attrs for most clients.
|
|
31
|
+
# belongs_to :source would be possible but we keep it attribute-based for simplicity.
|
|
14
32
|
end
|
|
15
33
|
end
|
|
16
34
|
end
|
|
@@ -2,7 +2,7 @@ module Dscf
|
|
|
2
2
|
module Marketplace
|
|
3
3
|
class OrderSerializer < ActiveModel::Serializer
|
|
4
4
|
attributes :id, :quotation_id, :listing_id, :user_id, :ordered_by_id, :ordered_to_id, :delivery_order_id, :dropoff_address_id,
|
|
5
|
-
:order_type, :status, :fulfillment_type, :payment_method, :received_bank_name, :transaction_reference, :total_amount,
|
|
5
|
+
:order_type, :status, :workflow_status, :has_validation_issues, :fulfillment_type, :payment_method, :received_bank_name, :transaction_reference, :total_amount,
|
|
6
6
|
:buyer_phone, :buyer_email, :seller_name, :seller_phone, :seller_email,
|
|
7
7
|
:created_at, :updated_at
|
|
8
8
|
|
|
@@ -34,6 +34,15 @@ module Dscf
|
|
|
34
34
|
def seller_email
|
|
35
35
|
object.ordered_to&.contact_email
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
# Sprint 2 workflow helpers for new order handling (validation + splitting)
|
|
39
|
+
def workflow_status
|
|
40
|
+
object.status
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def has_validation_issues
|
|
44
|
+
object.order_items.any? { |i| i.validation_status.present? && i.validation_status.to_s != 'validated' }
|
|
45
|
+
end
|
|
37
46
|
end
|
|
38
47
|
end
|
|
39
48
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class OrderSplittingService
|
|
4
|
+
# Handles source assignment and the split operation per requirements.
|
|
5
|
+
# Sources can be:
|
|
6
|
+
# - Aggregator self (via aggregator listing or direct)
|
|
7
|
+
# - Sub-supplier
|
|
8
|
+
# - Other supplier
|
|
9
|
+
|
|
10
|
+
def self.assign_source(order_item, source_type:, source_id:)
|
|
11
|
+
order_item.source_type = source_type
|
|
12
|
+
order_item.source_id = source_id
|
|
13
|
+
order_item.save!
|
|
14
|
+
order_item
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.perform_split(order)
|
|
18
|
+
raise "Cannot split order that is not ready" unless order.all_items_validated?
|
|
19
|
+
|
|
20
|
+
# Mark items ready for source assignment / supplier confirmation
|
|
21
|
+
order.order_items.each do |item|
|
|
22
|
+
if item.status.to_s == "pending" || item.status.to_s == "confirmed"
|
|
23
|
+
item.status = :processing
|
|
24
|
+
end
|
|
25
|
+
item.save! if item.changed?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
order.mark_waiting_supplier_confirmation!
|
|
29
|
+
|
|
30
|
+
order
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.supplier_confirm(order, confirmed: true, reason: nil)
|
|
34
|
+
if confirmed
|
|
35
|
+
# In full impl, mark specific allocation as confirmed
|
|
36
|
+
# For now advance whole if appropriate
|
|
37
|
+
else
|
|
38
|
+
order.order_items.where(status: :processing).update_all(status: OrderItem.statuses[:cancelled])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if supplier side is done
|
|
42
|
+
if order.supplier_confirmation_complete?
|
|
43
|
+
order.mark_waiting_retailer_confirmation!
|
|
44
|
+
end
|
|
45
|
+
order
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class OrderValidationService
|
|
4
|
+
# Validates all items in an order against current listings / availability.
|
|
5
|
+
# Sets validation_status on each OrderItem per the requirements doc:
|
|
6
|
+
# validated, no_longer_listed, price_changed, low_quantity
|
|
7
|
+
# Also populates resolved_* fields where applicable.
|
|
8
|
+
def self.validate(order)
|
|
9
|
+
order.order_items.each do |item|
|
|
10
|
+
validate_item(item)
|
|
11
|
+
end
|
|
12
|
+
# Transition order to splitting ready if all validated
|
|
13
|
+
if order.order_items.all? { |i| i.validation_status == "validated" }
|
|
14
|
+
order.update!(status: :splitting) if order.validating?
|
|
15
|
+
end
|
|
16
|
+
order.reload
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def self.validate_item(item)
|
|
21
|
+
current_listing = find_current_active_listing(item)
|
|
22
|
+
return mark_no_longer_listed(item) unless current_listing
|
|
23
|
+
|
|
24
|
+
current_price = current_listing.price
|
|
25
|
+
current_qty = current_listing.quantity
|
|
26
|
+
|
|
27
|
+
if current_price != item.unit_price
|
|
28
|
+
item.validation_status = :price_changed
|
|
29
|
+
item.validation_note = "Price changed (listed: #{current_price})"
|
|
30
|
+
item.resolved_unit_price = item.unit_price # keep original order price by default (user can choose listed later)
|
|
31
|
+
elsif item.quantity > current_qty
|
|
32
|
+
item.validation_status = :low_quantity
|
|
33
|
+
item.validation_note = "Low quantity available (listed: #{current_qty})"
|
|
34
|
+
item.resolved_quantity = [ item.quantity, current_qty ].min
|
|
35
|
+
else
|
|
36
|
+
item.validation_status = :validated
|
|
37
|
+
item.validation_note = nil
|
|
38
|
+
item.resolved_unit_price = nil
|
|
39
|
+
item.resolved_quantity = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
item.save! if item.changed?
|
|
43
|
+
item
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private_class_method def self.find_current_active_listing(item)
|
|
47
|
+
# Prefer the original listing if still active
|
|
48
|
+
if item.listing && Listing.active.exists?(id: item.listing.id)
|
|
49
|
+
return item.listing
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fallback: find cheapest active listing for the product
|
|
53
|
+
Listing.active
|
|
54
|
+
.joins(:supplier_product)
|
|
55
|
+
.where(supplier_products: {product_id: item.product_id})
|
|
56
|
+
.order(price: :asc)
|
|
57
|
+
.first
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private_class_method def self.mark_no_longer_listed(item)
|
|
61
|
+
item.validation_status = :no_longer_listed
|
|
62
|
+
item.validation_note = "Product no longer listed"
|
|
63
|
+
item.save! if item.changed?
|
|
64
|
+
item
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -125,6 +125,12 @@ Dscf::Marketplace::Engine.routes.draw do
|
|
|
125
125
|
post "cancel"
|
|
126
126
|
post "complete"
|
|
127
127
|
get "invoice"
|
|
128
|
+
post "validate"
|
|
129
|
+
post "resolve_item"
|
|
130
|
+
post "assign_source"
|
|
131
|
+
post "split"
|
|
132
|
+
post "supplier_confirm"
|
|
133
|
+
post "retailer_confirm"
|
|
128
134
|
end
|
|
129
135
|
collection do
|
|
130
136
|
get "my_orders"
|
data/db/migrate/20260622000001_add_validation_and_source_fields_to_dscf_marketplace_order_items.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class AddValidationAndSourceFieldsToDscfMarketplaceOrderItems < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :dscf_marketplace_order_items, :validation_status, :integer, default: 0, null: false
|
|
4
|
+
add_column :dscf_marketplace_order_items, :validation_note, :text
|
|
5
|
+
add_column :dscf_marketplace_order_items, :resolved_unit_price, :decimal, precision: 15, scale: 6
|
|
6
|
+
add_column :dscf_marketplace_order_items, :resolved_quantity, :decimal, precision: 15, scale: 6
|
|
7
|
+
add_column :dscf_marketplace_order_items, :source_type, :string
|
|
8
|
+
add_column :dscf_marketplace_order_items, :source_id, :bigint
|
|
9
|
+
|
|
10
|
+
add_index :dscf_marketplace_order_items, :validation_status, name: "validation_status_on_dm_order_items_idx"
|
|
11
|
+
add_index :dscf_marketplace_order_items, [:source_type, :source_id], name: "source_on_dm_order_items_idx"
|
|
12
|
+
end
|
|
13
|
+
end
|
data/db/seeds.rb
CHANGED
|
@@ -11,7 +11,7 @@ Dscf::Core::PermissionRegistry.register("dscf-marketplace") do
|
|
|
11
11
|
resource :rfq_items, actions: %i[index show create update destroy]
|
|
12
12
|
resource :quotations, actions: %i[index show create update destroy accept reject send_quotation my_quotes filter]
|
|
13
13
|
resource :quotation_items, actions: %i[index show create update destroy]
|
|
14
|
-
resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter]
|
|
14
|
+
resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter validate resolve_item assign_source split supplier_confirm retailer_confirm]
|
|
15
15
|
resource :order_items, actions: %i[index show create update destroy]
|
|
16
16
|
resource :delivery_orders, actions: %i[
|
|
17
17
|
index show create update destroy pickup start_delivery complete_delivery mark_failed accept summary
|
|
@@ -33,7 +33,7 @@ module Dscf
|
|
|
33
33
|
resource :rfq_items, actions: %i[index show create update destroy]
|
|
34
34
|
resource :quotations, actions: %i[index show create update destroy accept reject send_quotation my_quotes filter]
|
|
35
35
|
resource :quotation_items, actions: %i[index show create update destroy]
|
|
36
|
-
resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter]
|
|
36
|
+
resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter validate resolve_item assign_source split supplier_confirm retailer_confirm]
|
|
37
37
|
resource :order_items, actions: %i[index show create update destroy]
|
|
38
38
|
resource :delivery_orders, actions: %i[
|
|
39
39
|
index show create update destroy pickup start_delivery complete_delivery mark_failed accept summary
|
|
@@ -4,6 +4,7 @@ FactoryBot.define do
|
|
|
4
4
|
quantity { 5 }
|
|
5
5
|
unit_price { 10.0 }
|
|
6
6
|
status { :pending }
|
|
7
|
+
validation_status { :validated }
|
|
7
8
|
quotation_item { nil } # Don't create by default
|
|
8
9
|
listing { nil } # Don't create by default
|
|
9
10
|
|
|
@@ -45,5 +46,10 @@ FactoryBot.define do
|
|
|
45
46
|
trait :cancelled do
|
|
46
47
|
status { :cancelled }
|
|
47
48
|
end
|
|
49
|
+
|
|
50
|
+
trait :price_changed do
|
|
51
|
+
validation_status { :price_changed }
|
|
52
|
+
validation_note { "Price changed" }
|
|
53
|
+
end
|
|
48
54
|
end
|
|
49
55
|
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.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -525,6 +525,8 @@ files:
|
|
|
525
525
|
- app/services/dscf/marketplace/gebeta_service.rb
|
|
526
526
|
- app/services/dscf/marketplace/invoice_pdf_generator.rb
|
|
527
527
|
- app/services/dscf/marketplace/my_resource_service.rb
|
|
528
|
+
- app/services/dscf/marketplace/order_splitting_service.rb
|
|
529
|
+
- app/services/dscf/marketplace/order_validation_service.rb
|
|
528
530
|
- app/services/dscf/marketplace/rfq_response_service.rb
|
|
529
531
|
- app/services/dscf/marketplace/role_service.rb
|
|
530
532
|
- app/services/dscf/marketplace/route_optimization_service.rb
|
|
@@ -586,6 +588,7 @@ files:
|
|
|
586
588
|
- db/migrate/20260616000010_add_code_to_dscf_marketplace_categories_and_gender_to_dscf_marketplace_agents.rb
|
|
587
589
|
- db/migrate/20260619210001_add_user_id_to_dscf_marketplace_agents.rb
|
|
588
590
|
- db/migrate/20260619210002_add_user_id_to_dscf_marketplace_retailers.rb
|
|
591
|
+
- db/migrate/20260622000001_add_validation_and_source_fields_to_dscf_marketplace_order_items.rb
|
|
589
592
|
- db/seeds.rb
|
|
590
593
|
- lib/dscf/marketplace.rb
|
|
591
594
|
- lib/dscf/marketplace/engine.rb
|