dscf-marketplace 0.8.9 → 0.9.1
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 +198 -30
- data/app/models/dscf/marketplace/order.rb +56 -7
- data/app/models/dscf/marketplace/order_item.rb +11 -4
- data/app/policies/dscf/marketplace/order_policy.rb +24 -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/demo_seeds.rb +85 -0
- data/db/migrate/20260622000001_add_validation_and_source_fields_to_dscf_marketplace_order_items.rb +13 -0
- data/db/seeds.rb +13 -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: 06e78b71c3cb2e9a47b73bdc71eb3c221e6ef2c00fae135fbe6f4b9f6b76c0ad
|
|
4
|
+
data.tar.gz: f416eef38f577530f81e698f6c0fbccfbca0e18b342866e50736a3a328bd65e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58a193bbca932e849a71adfe6faad3bb2b9b79a8cb312fcb4973aeec8e650b740ae6d10977548b8c94633616fbdeb5089570e332d205ef83e77538e5af6953b6
|
|
7
|
+
data.tar.gz: 727fd25dd31d44c4f52544ecd161096a502ca4168ca934e1bcd4324ac4b9dcff57c37c268607e4f081ede2e88227441aeef35cd021dd8a7333313a451349de23
|
|
@@ -104,47 +104,215 @@ 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
|
+
rescue => e
|
|
115
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_item
|
|
119
|
+
obj = find_record
|
|
120
|
+
authorize obj, :resolve_item?
|
|
121
|
+
|
|
122
|
+
item = obj.order_items.find(params[:order_item_id])
|
|
123
|
+
# Example: for price_changed, accept optional resolved_unit_price from params
|
|
124
|
+
if params[:resolved_unit_price].present?
|
|
125
|
+
item.resolved_unit_price = params[:resolved_unit_price]
|
|
126
|
+
item.validation_status = :validated
|
|
127
|
+
end
|
|
128
|
+
if params[:resolved_quantity].present?
|
|
129
|
+
item.resolved_quantity = params[:resolved_quantity]
|
|
130
|
+
item.validation_status = :validated
|
|
131
|
+
end
|
|
132
|
+
if params[:action_type] == "remove"
|
|
133
|
+
item.validation_status = :validated
|
|
134
|
+
item.quantity = 0 # will be cleaned or marked
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
item.save!
|
|
138
|
+
create_notification_for_order(obj, :item_resolved)
|
|
139
|
+
render_success(data: item)
|
|
140
|
+
rescue => e
|
|
141
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def assign_source
|
|
145
|
+
obj = find_record
|
|
146
|
+
authorize obj, :split?
|
|
147
|
+
|
|
148
|
+
item = obj.order_items.find(params[:order_item_id])
|
|
149
|
+
Dscf::Marketplace::OrderSplittingService.assign_source(item, source_type: params[:source_type], source_id: params[:source_id])
|
|
150
|
+
render_success(data: item)
|
|
151
|
+
rescue => e
|
|
152
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def split
|
|
156
|
+
obj = find_record
|
|
157
|
+
authorize obj, :split?
|
|
158
|
+
|
|
159
|
+
return render_error(errors: "All items must be validated before splitting") unless obj.all_items_validated?
|
|
160
|
+
|
|
161
|
+
Dscf::Marketplace::OrderSplittingService.perform_split(obj)
|
|
162
|
+
create_notification_for_order(obj, :split_ready)
|
|
163
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
164
|
+
rescue => e
|
|
165
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def supplier_confirm
|
|
169
|
+
obj = find_record
|
|
170
|
+
authorize obj, :supplier_confirm?
|
|
171
|
+
|
|
172
|
+
confirmed = params[:confirmed]
|
|
173
|
+
reason = params[:reason]
|
|
174
|
+
Dscf::Marketplace::OrderSplittingService.supplier_confirm(obj, confirmed: confirmed != false, reason: reason)
|
|
175
|
+
|
|
176
|
+
create_notification_for_order(obj, confirmed ? :supplier_confirmed : :supplier_rejected, reason: reason)
|
|
177
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
178
|
+
rescue => e
|
|
179
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def retailer_confirm
|
|
183
|
+
obj = find_record
|
|
184
|
+
authorize obj, :confirm? # reuse existing or add specific
|
|
185
|
+
|
|
186
|
+
return render_error(errors: "Order not ready for retailer confirmation") unless obj.retailer_can_confirm?
|
|
187
|
+
|
|
188
|
+
if params[:confirmed] == false
|
|
189
|
+
reason = params[:reason]
|
|
190
|
+
obj.update!(status: :cancelled)
|
|
191
|
+
obj.order_items.update_all(status: OrderItem.statuses[:cancelled])
|
|
192
|
+
create_notification_for_order(obj, :retailer_rejected, reason: reason)
|
|
193
|
+
else
|
|
194
|
+
obj.update!(status: :confirmed)
|
|
195
|
+
obj.order_items.update_all(status: OrderItem.statuses[:confirmed])
|
|
196
|
+
create_notification_for_order(obj, :retailer_confirmed)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
render_success(data: obj, serializer_options: { include: default_serializer_includes[:show] || [] })
|
|
200
|
+
rescue => e
|
|
201
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def create_notification_for_order(order, action, reason: nil)
|
|
206
|
+
recipient = order.ordered_by || order.user
|
|
207
|
+
return unless recipient
|
|
208
|
+
|
|
209
|
+
title = case action
|
|
210
|
+
when :validation_completed then "Order ##{order.id} validation complete"
|
|
211
|
+
when :item_resolved then "Item resolved in order ##{order.id}"
|
|
212
|
+
when :split_ready then "Order ##{order.id} ready for supplier confirmation"
|
|
213
|
+
when :supplier_confirmed then "Supplier confirmed order ##{order.id}"
|
|
214
|
+
when :supplier_rejected then "Supplier rejected item in order ##{order.id}"
|
|
215
|
+
when :retailer_confirmed then "Retailer confirmed order ##{order.id}"
|
|
216
|
+
when :retailer_rejected then "Retailer cancelled order ##{order.id}"
|
|
217
|
+
else "Order ##{order.id} update"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
body = case action
|
|
221
|
+
when :supplier_rejected, :retailer_rejected then "Reason: #{reason}" if reason
|
|
222
|
+
else "Status: #{order.status}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
notification = Dscf::Core::Notification.create!(
|
|
226
|
+
notifiable: order,
|
|
227
|
+
recipient: recipient,
|
|
228
|
+
notification_type: :general,
|
|
229
|
+
title: title,
|
|
230
|
+
body: body.to_s
|
|
231
|
+
)
|
|
232
|
+
Dscf::Core::NotificationService.deliver(notification)
|
|
233
|
+
end
|
|
234
|
+
|
|
107
235
|
private
|
|
108
236
|
|
|
109
237
|
def create_direct_listing_order
|
|
110
|
-
listing =
|
|
111
|
-
|
|
238
|
+
listing = nil
|
|
239
|
+
aggregator_listing = nil
|
|
240
|
+
|
|
241
|
+
if model_params[:listing_type] == "AggregatorListing"
|
|
242
|
+
aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: model_params[:listing_id])
|
|
243
|
+
return render_error(errors: "Listing is not available", status: :unprocessable_entity) unless aggregator_listing
|
|
244
|
+
else
|
|
245
|
+
listing = Dscf::Marketplace::Listing.active.find_by(id: model_params[:listing_id])
|
|
246
|
+
if listing.blank? && model_params[:listing_type].blank?
|
|
247
|
+
aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: model_params[:listing_id])
|
|
248
|
+
end
|
|
249
|
+
return render_error(errors: "Listing is not available", status: :unprocessable_entity) if listing.blank? && aggregator_listing.blank?
|
|
250
|
+
end
|
|
112
251
|
|
|
113
252
|
quantity = direct_listing_quantity
|
|
114
253
|
return render_error(errors: "Quantity must be greater than 0", status: :unprocessable_entity) unless quantity.positive?
|
|
115
254
|
|
|
116
|
-
|
|
255
|
+
limit_quantity = listing ? listing.quantity : aggregator_listing.quantity
|
|
256
|
+
if quantity > limit_quantity
|
|
117
257
|
return render_error(errors: "Requested quantity exceeds available listing quantity", status: :unprocessable_entity)
|
|
118
258
|
end
|
|
119
259
|
|
|
120
260
|
order = nil
|
|
121
261
|
ActiveRecord::Base.transaction do
|
|
122
|
-
listing
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
262
|
+
if listing
|
|
263
|
+
listing.lock!
|
|
264
|
+
|
|
265
|
+
if quantity > listing.quantity
|
|
266
|
+
listing.errors.add(:base, "Requested quantity exceeds available listing quantity")
|
|
267
|
+
raise ActiveRecord::RecordInvalid.new(listing)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
order = @clazz.new(model_params.except(:order_items_attributes))
|
|
271
|
+
order.order_type = :direct_listing
|
|
272
|
+
order.status = :pending
|
|
273
|
+
order.listing = listing
|
|
274
|
+
order.ordered_to = listing.business
|
|
275
|
+
|
|
276
|
+
product = listing.supplier_product.product
|
|
277
|
+
order.order_items.build(
|
|
278
|
+
listing: listing,
|
|
279
|
+
product: product,
|
|
280
|
+
unit: product.unit,
|
|
281
|
+
quantity: quantity,
|
|
282
|
+
unit_price: listing.price,
|
|
283
|
+
status: :pending
|
|
284
|
+
)
|
|
285
|
+
order.save!
|
|
286
|
+
|
|
287
|
+
new_quantity = listing.quantity - quantity
|
|
288
|
+
listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : listing.status))
|
|
289
|
+
else
|
|
290
|
+
aggregator_listing.lock!
|
|
291
|
+
|
|
292
|
+
if quantity > aggregator_listing.quantity
|
|
293
|
+
aggregator_listing.errors.add(:base, "Requested quantity exceeds available listing quantity")
|
|
294
|
+
raise ActiveRecord::RecordInvalid.new(aggregator_listing)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
order = @clazz.new(model_params.except(:order_items_attributes, :listing_id))
|
|
298
|
+
order.order_type = :direct_listing
|
|
299
|
+
order.status = :pending
|
|
300
|
+
order.ordered_to = aggregator_listing.aggregator
|
|
301
|
+
|
|
302
|
+
product = aggregator_listing.supplier_product.product
|
|
303
|
+
order.order_items.build(
|
|
304
|
+
source: aggregator_listing,
|
|
305
|
+
product: product,
|
|
306
|
+
unit: product.unit,
|
|
307
|
+
quantity: quantity,
|
|
308
|
+
unit_price: aggregator_listing.price,
|
|
309
|
+
status: :pending
|
|
310
|
+
)
|
|
311
|
+
order.save!
|
|
312
|
+
|
|
313
|
+
new_quantity = aggregator_listing.quantity - quantity
|
|
314
|
+
aggregator_listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : aggregator_listing.status))
|
|
127
315
|
end
|
|
128
|
-
|
|
129
|
-
order = @clazz.new(model_params.except(:order_items_attributes))
|
|
130
|
-
order.order_type = :direct_listing
|
|
131
|
-
order.status = :pending
|
|
132
|
-
order.listing = listing
|
|
133
|
-
order.ordered_to = listing.business
|
|
134
|
-
|
|
135
|
-
product = listing.supplier_product.product
|
|
136
|
-
order.order_items.build(
|
|
137
|
-
listing: listing,
|
|
138
|
-
product: product,
|
|
139
|
-
unit: product.unit,
|
|
140
|
-
quantity: quantity,
|
|
141
|
-
unit_price: listing.price,
|
|
142
|
-
status: :pending
|
|
143
|
-
)
|
|
144
|
-
order.save!
|
|
145
|
-
|
|
146
|
-
new_quantity = listing.quantity - quantity
|
|
147
|
-
listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : listing.status))
|
|
148
316
|
end
|
|
149
317
|
|
|
150
318
|
order = @clazz.includes(eager_loaded_associations).find(order.id) if eager_loaded_associations.present?
|
|
@@ -170,7 +338,7 @@ module Dscf
|
|
|
170
338
|
def model_params
|
|
171
339
|
@model_params ||= params.require(:order).permit(
|
|
172
340
|
:quotation_id, :listing_id, :user_id, :ordered_by_id, :ordered_to_id, :delivery_order_id, :dropoff_address_id,
|
|
173
|
-
:order_type, :status, :fulfillment_type, :payment_method, :received_bank_name, :transaction_reference,
|
|
341
|
+
:order_type, :status, :fulfillment_type, :payment_method, :received_bank_name, :transaction_reference, :listing_type,
|
|
174
342
|
order_items_attributes: [ :id, :quotation_item_id, :listing_id, :product_id, :unit_id, :quantity, :unit_price, :status, :_destroy ]
|
|
175
343
|
)
|
|
176
344
|
@model_params[:payment_method] = normalize_payment_method(@model_params[:payment_method]) if @model_params.key?(:payment_method)
|
|
@@ -183,7 +351,7 @@ module Dscf
|
|
|
183
351
|
|
|
184
352
|
def default_serializer_includes
|
|
185
353
|
{
|
|
186
|
-
index: [ :user, :ordered_by, :ordered_to, :quotation, order_items
|
|
354
|
+
index: [ :user, :ordered_by, :ordered_to, :quotation, :order_items ],
|
|
187
355
|
show: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :delivery_order, :order_items ],
|
|
188
356
|
create: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :order_items ],
|
|
189
357
|
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
|
|
@@ -117,7 +132,7 @@ module Dscf::Marketplace
|
|
|
117
132
|
if rfq_based?
|
|
118
133
|
quotation&.business
|
|
119
134
|
elsif direct_listing?
|
|
120
|
-
listing&.business
|
|
135
|
+
listing&.business || ordered_to
|
|
121
136
|
end
|
|
122
137
|
end
|
|
123
138
|
|
|
@@ -157,10 +172,44 @@ 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
|
|
163
|
-
unless quotation.present? || listing.present?
|
|
212
|
+
unless quotation.present? || listing.present? || order_items.any? { |item| item.source_type == "Dscf::Marketplace::AggregatorListing" }
|
|
164
213
|
errors.add(:base, "Either quotation or listing must be present")
|
|
165
214
|
end
|
|
166
215
|
end
|
|
@@ -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
|
|
@@ -49,8 +56,8 @@ module Dscf::Marketplace
|
|
|
49
56
|
private
|
|
50
57
|
|
|
51
58
|
def quotation_item_or_listing_present
|
|
52
|
-
unless quotation_item.present? || listing.present?
|
|
53
|
-
errors.add(:base, "Either quotation_item or listing must be present")
|
|
59
|
+
unless quotation_item.present? || listing.present? || (source_type == "Dscf::Marketplace::AggregatorListing" && source_id.present?)
|
|
60
|
+
errors.add(:base, "Either quotation_item, listing, or aggregator listing source must be present")
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
end
|
|
@@ -20,6 +20,30 @@ 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
|
|
43
|
+
|
|
44
|
+
def show?
|
|
45
|
+
user.has_permission?("orders.show") || user.has_permission?("orders.filter") || user.super_admin?
|
|
46
|
+
end
|
|
23
47
|
end
|
|
24
48
|
end
|
|
25
49
|
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/demo_seeds.rb
CHANGED
|
@@ -22,6 +22,7 @@ Dscf::Marketplace::RfqItem.delete_all
|
|
|
22
22
|
Dscf::Marketplace::RequestForQuotation.delete_all
|
|
23
23
|
Dscf::Marketplace::AggregatorListing.delete_all
|
|
24
24
|
Dscf::Marketplace::Listing.delete_all
|
|
25
|
+
Dscf::Marketplace::ProductInclusionRequest.delete_all
|
|
25
26
|
Dscf::Marketplace::Agent.delete_all
|
|
26
27
|
Dscf::Marketplace::SupplierProduct.delete_all
|
|
27
28
|
Dscf::Marketplace::Supplier.delete_all
|
|
@@ -41,6 +42,13 @@ end
|
|
|
41
42
|
sa_role = Dscf::Core::Role.find_or_create_by!(code: "SUPER_ADMIN") { |r| r.name = "Super Admin"; r.active = true }
|
|
42
43
|
Dscf::Core::UserRole.find_or_create_by!(user: admin, role: sa_role)
|
|
43
44
|
|
|
45
|
+
# Ensure SUPER_ADMIN has basic orders permissions (show/filter etc) for detail views etc.
|
|
46
|
+
orders_perms = Dscf::Core::Permission.where(resource: "orders")
|
|
47
|
+
orders_perms.each do |perm|
|
|
48
|
+
Dscf::Core::RolePermission.find_or_create_by!(role: sa_role, permission: perm)
|
|
49
|
+
end
|
|
50
|
+
puts " Granted orders permissions to SUPER_ADMIN"
|
|
51
|
+
|
|
44
52
|
puts "🌱 Seeding demo data..."
|
|
45
53
|
|
|
46
54
|
# ── Businesses ──
|
|
@@ -113,8 +121,85 @@ Dscf::Marketplace::AggregatorListing.create!(aggregator: biz_agg, supplier_produ
|
|
|
113
121
|
Dscf::Marketplace::AggregatorListing.create!(aggregator: biz_agg, supplier_product: sp6b, price: 420, quantity: 80, status: 0)
|
|
114
122
|
puts " Aggregator Listings: 3"
|
|
115
123
|
|
|
124
|
+
# ── Demo Address ──
|
|
125
|
+
address = Dscf::Core::Address.first || Dscf::Core::Address.create!(addressable: admin, address_line_1: "Bole Road, Building 12", city: "Addis Ababa", country: "Ethiopia")
|
|
126
|
+
|
|
127
|
+
# ── Orders (Direct Purchases) ──
|
|
128
|
+
l1 = Dscf::Marketplace::Listing.first
|
|
129
|
+
if l1
|
|
130
|
+
order1 = Dscf::Marketplace::Order.create!(
|
|
131
|
+
order_type: :direct_listing,
|
|
132
|
+
status: :pending,
|
|
133
|
+
fulfillment_type: :self_pickup,
|
|
134
|
+
payment_method: :cash,
|
|
135
|
+
user: admin,
|
|
136
|
+
ordered_by: admin,
|
|
137
|
+
ordered_to: l1.business,
|
|
138
|
+
listing: l1,
|
|
139
|
+
total_amount: l1.price * 5
|
|
140
|
+
)
|
|
141
|
+
order1.order_items.create!(
|
|
142
|
+
listing: l1,
|
|
143
|
+
product: l1.supplier_product.product,
|
|
144
|
+
unit: l1.supplier_product.product.unit,
|
|
145
|
+
quantity: 5,
|
|
146
|
+
unit_price: l1.price,
|
|
147
|
+
status: :pending
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
l2 = Dscf::Marketplace::Listing.last
|
|
152
|
+
if l2 && l2 != l1
|
|
153
|
+
order2 = Dscf::Marketplace::Order.create!(
|
|
154
|
+
order_type: :direct_listing,
|
|
155
|
+
status: :processing,
|
|
156
|
+
fulfillment_type: :delivery,
|
|
157
|
+
payment_method: :bank_transfer,
|
|
158
|
+
user: admin,
|
|
159
|
+
ordered_by: admin,
|
|
160
|
+
ordered_to: l2.business,
|
|
161
|
+
listing: l2,
|
|
162
|
+
dropoff_address: address,
|
|
163
|
+
total_amount: l2.price * 10
|
|
164
|
+
)
|
|
165
|
+
order2.order_items.create!(
|
|
166
|
+
listing: l2,
|
|
167
|
+
product: l2.supplier_product.product,
|
|
168
|
+
unit: l2.supplier_product.product.unit,
|
|
169
|
+
quantity: 10,
|
|
170
|
+
unit_price: l2.price,
|
|
171
|
+
status: :pending
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
puts " Orders: 2"
|
|
175
|
+
|
|
176
|
+
# ── RFQs (Request For Quotations) ──
|
|
177
|
+
rfq1 = Dscf::Marketplace::RequestForQuotation.create!(
|
|
178
|
+
user: admin,
|
|
179
|
+
status: :sent,
|
|
180
|
+
notes: "Looking for 500kg of premium washed Yirgacheffe coffee in bulk for export."
|
|
181
|
+
)
|
|
182
|
+
rfq1.rfq_items.create!(
|
|
183
|
+
product: p1,
|
|
184
|
+
unit: p1.unit,
|
|
185
|
+
quantity: 500
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
rfq2 = Dscf::Marketplace::RequestForQuotation.create!(
|
|
189
|
+
user: admin,
|
|
190
|
+
status: :draft,
|
|
191
|
+
notes: "Checking prices for traditional Shema fabrics."
|
|
192
|
+
)
|
|
193
|
+
rfq2.rfq_items.create!(
|
|
194
|
+
product: p4,
|
|
195
|
+
unit: p4.unit,
|
|
196
|
+
quantity: 100
|
|
197
|
+
)
|
|
198
|
+
puts " RFQs: 2"
|
|
199
|
+
|
|
116
200
|
puts ""
|
|
117
201
|
puts "✅ DONE."
|
|
118
202
|
puts " 6 visible: Yirgacheffe, Sidamo(650), Shema(promoted), Habesha(580), Berbere, Harrar"
|
|
119
203
|
puts " 2 hidden: Sidamo(720), Habesha(650)"
|
|
120
204
|
puts " 1 pending approval (sp6), 2 pending KYC, 1 inactive agent"
|
|
205
|
+
puts " 2 test orders, 2 test RFQs added for UI validation."
|
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
|
|
@@ -39,3 +39,15 @@ marketplace_permissions.find_each do |permission|
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
puts "✅ Seeded #{marketplace_permissions.count} marketplace permissions and assigned to USER role."
|
|
42
|
+
|
|
43
|
+
# Also grant key orders permissions (incl. show for detail views) to SUPER_ADMIN if exists
|
|
44
|
+
sa_role = Dscf::Core::Role.find_by(code: "SUPER_ADMIN")
|
|
45
|
+
if sa_role
|
|
46
|
+
orders_actions = %i[show filter validate resolve_item assign_source split supplier_confirm retailer_confirm]
|
|
47
|
+
orders_actions.each do |action|
|
|
48
|
+
if perm = Dscf::Core::Permission.find_by(resource: "orders", action: action)
|
|
49
|
+
Dscf::Core::RolePermission.find_or_create_by!(role: sa_role, permission: perm)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
puts " Also granted key orders.* to SUPER_ADMIN"
|
|
53
|
+
end
|
|
@@ -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.1
|
|
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-25 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
|